1、Promise
开篇首先设想一个日常开发常常会遇到的需求:在多个接口异步请求数据,然后利用这些数据来进行一系列的操作。一般会这样去写:
$.ajax({
url: '......',
success: function (data) {
$.ajax({
// 要在第一个请求成功后才可以执行下一步
url: '......',
success: function (data) {
// ......
}
});
}
});
这样的写法的原理是,当执行一些异步操作时,我们需要知道操作是否已经完成,所有当执行完成的时候会返回一个回调函数,表示操作已经完成。
使用回调函数的形式理解起来并不困难,但是实际的应用当中会有以下的缺点:
在需要多个操作的时候,会导致多个回调函数嵌套,导致代码不够直观,就是常说的 Callback Hell。
如果几个异步操作之间并没有前后顺序之分(例如不需要前一个请求的结果作为后一个请求的参数)时,同样需要等待上一个操作完成再实行下一个操作。
为了解决上述的问题,Promise 对象应运而生,ES6将其写进了语言标准。
什么是 Promise
一个 Promise 对象可以理解为一次将要执行的操作(常常被用于异步操作),使用了 Promise 对象之后可以用一种链式调用的方式来组织代码,让代码更加直观。而且由于 Promise.all 这样的方法存在,可以让同时执行多个操作变得简单。接下来就来简单介绍 Promise 对象。
resolve 和 reject
首先来看一段使用了 Promise 对象的代码。
function helloWorld (ready) {
return new Promise(function (resolve, reject) {
if (ready) {
resolve("Hello World!");
} else {
reject("Good bye!");
}
});
}
helloWorld(true).then(function (message) {
alert(message);
}, function (error) {
alert(error);
});
上面的代码实现的功能非常简单,helloWord 函数接受一个参数,如果为 true 就打印 “Hello World!”,如果为 false 就打印错误的信息。helloWord 函数返回的是一个 Promise 对象。
在 Promise 对象当中有两个重要方法————resolve 和 reject。
resolve
方法可以使 Promise 对象的状态改变成成功,同时传递一个参数用于后续成功后的操作,在这个例子当中就是 Hello World! 字符串。
reject
方法则是将 Promise 对象的状态改变为失败,同时将错误的信息传递到后续错误处理的操作。
Promise 的三种状态
上面提到了 resolve 和 reject 可以改变 Promise 对象的状态,那么它究竟有哪些状态呢?
Promise 对象有三种状态:
Fulfilled
可以理解为成功的状态
Rejected
可以理解为失败的状态
Pending
既不是 Fulfilld
也不是 Rejected
的状态,可以理解为 Promise 对象实例创建时候的初始状态
helloWorld 的例子中的 then
方法就是根据 Promise 对象的状态来确定执行的操作,resolve
时执行第一个函数(onFulfilled
),reject
时执行第二个函数(onRejected
)。
then 和 catch
then
helloWorld 的例子当中利用了 then(onFulfilld, onRejected)
方法来执行一个任务打印 “Hello World!”,在多个任务的情况下 then 方法同样可以用一个清晰的方式完成。
function printHello (ready) {
return new Promise(function (resolve, reject) {
if (ready) {
resolve("Hello");
} else {
reject("Good bye!");
}
});
}
function printWorld () {
alert("World");
}
function printExclamation () {
alert("!");
}
printHello(true)
.then(function(message){
alert(message);
})
.then(printWorld)
.then(printExclamation);
上述例子通过链式调用的方式,按顺序打印出了相应的内容。then 可以使用链式调用的写法原因在于,每一次执行该方法时总是会返回一个 Promise 对象。另外,在 then onFulfilled
的函数当中的返回值,可以作为后续操作的参数,因此上面的例子也可以写成:
printHello(true).then(function (message) {
return message;
}).then(function (message) {
return message + ' World';
}).then(function (message) {
return message + '!';
}).then(function (message) {
alert(message);
});
同样可以打印出正确的内容。
catch
catch
方法是 then(onFulfilled, onRejected)
方法当中 onRejected
函数的一个简单的写法,也就是说可以写成 then(fn).catch(fn)
,相当于 then(fn).then(null, fn)
。使用 catch 的写法比一般的写法更加清晰明确。
Promise.all 和 Promise.race
Promise.all 可以接收一个元素为 Promise 对象的数组作为参数,当这个数组里面所有的 Promise 对象都变为 resolve 时,该方法才会返回。
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
let p1 = getJSON("https://api.github.com/search/users?q=1")
let p2 = getJSON("https://api.github.com/search/users?q=2")
Promise.all([p1,p2])
.then(function([post1,post2]) {
let a = String(post1.items[0].id).substr(0,1);
let b = String(post2.items[0].id).substr(0,1);
getJSON("https://api.github.com/search/users?q="+a+b)
.then(function (response) {
console.log(response)
})
}).catch(function(error){
console.log('出错啦!', error);
});
上面的例子传输的两个数据可能需要不同的时长,即使 p2 的速度比 p1 要快,但是 Promise.all 方法会按照数组里面的顺序将结果返回。
日常开发中经常会遇到这样的需求,在不同的接口请求数据然后拼合成自己所需的数据,通常这些接口之间没有关联(例如不需要前一个接口的数据作为后一个接口的参数),这个时候 Promise.all 方法就可以派上用场了。
还有一个和 Promise.all 相类似的方法 Promise.race,它同样接收一个数组,不同的是只要该数组中的 Promise 对象的状态发生变化(无论是 resolve 还是 reject)该方法都会返回。
Generator
什么是Generator?
当你调用一个generator时,它将返回一个迭代器对象。这个迭代器对象拥有一个叫做next的方法来帮助你重启generator函数并得到下一个值。
next方法不仅返回值,它返回的对象具有两个属性:done和value。value是你获得的值,done用来表明你的generator是否已经停止提供值。
function* test(p){
console.log(p);
var a = yield p + 1;
console.log(a);
}
var g = test(1);
var ret;
ret = g.next();
console.log(ret);
ret = g.next(ret.value + 1);
console.log(ret);
//1
//Object {value: 2, done: false}
//3
//Object {value: undefined, done: true}
yield
关键字可以让当前函数暂停执行并保存现场,并跳出到调用此函数的代码处继续执行。
可以利用函数执行时的返回句柄的next方法回到之前暂停处继续执行
next执行的返回值的value即是yield关键字后面部分的表达式结果
下一个next的唯一参数值可以作为yield的整体返回值,并赋值给a变量
看下执行顺序就能比较清楚Generator是怎么工作的了
var g = test(1);//1
var ret;//2
ret = g.next();//3
console.log(p);//4 输出1
yield p + 1;//5
console.log(ret);//6 输出{value: 2, done: false}
ret = g.next(ret.value + 1);//7
var a = yield p + 1;//8 ,此时yield p + 1 表达式的值为ret.value + 1,即为3
console.log(a);//9 输出3
console.log(ret);//10 输出 {value: undefined, done: true}
同步场景下生成器的使用
function * Square(){
for(var i=1;;i++){
yield i*i;
}
}
var square = Square();
square.next(); // 1
square.next(); // 4
square.next(); // 9
......
我们在循环中并没有设中止条件,因为调用一个square.next()方法,它才会执行一次,不调用则不执行,所以不用担心死循环的问题。
异步场景下的生成器使用
如何用生成器解决异步场景下的回调金字塔
从前面的例子中,其实已经可以体会出来了,生成器的用法中并不包含对异步的处理,所以其实没有办法帮助我们对异步回调进行封闭。那么为什么大家将它视为解决回调嵌套的神器呢?在翻阅了不少资料后找到这篇文章,文章作者一开始也认为生成器并不能解决回调嵌套的问题,但下面自己做了解释,如果生成器的返回的是一系列的Promise对象的话,情况就会不一样了,举个粟子:
function myAjax(){
return fetch('https://api.github.com/search/users?q=1');
}
我们使用window.fetch方法来处理ajax请求,这个方法会返回一个Promise对象。然后,我们使用一个生成器来包装这个操作:
function * MyLogic(){
var serverData = yield myAjax();
console.log('MyLogic after myAjax');
console.log('serverStatus:%s',serverData.status);
}
使用的时候这样用:
var myLogic = MyLogic();
var promise = myLogic.next().value;
promise.then(function(serverData){
myLogic.next(serverData);
});
可以看到,我们这里的myAjax()
以及MyLogic()
函数中,并没有使用回调,就完成了异步操作。
这里有几个值得注意的点:
myAjax()
函数返回的是一个Promise对象
myLogic
中的第一个语句,返回的是 {value: Promise, done: false}
等外界再次调用next()方法时将Promise对象数据传进来,赋值给serverDate
promise的状态是由第三段代码,在外部进行处理,完成的时候调用myLogic.next()方法并将serverData再传回MyLogic()中
你一定会问,下面这个promise.done不就是回调操作么?Bingo!这正是精华所在!我们来看一下这段代码做了什么:
首先,myLogic.next().value返回了一个Promise对象(promise),然后,promise.then中的回调函数所做的事情就是调用myLogic.next()方法并将serverData传入就行了,除了调用next()方法,其它的什么事情都没有。
异步封装
首先,我们保持myAjax()
和MyLogic
定义不变,而将myLogic.next()
放到一个函数来调用,这个函数专门负责调用myLogic.next()
,得到返回的Promise对象,然后在Promise被resolve
的时候再次调用myLogic.next()
:
var myLogic = MyLogic();
function genRunner(){
// 调用next()获取promise
var yieldValue = myLogic.next();
var promise = yieldValue.value;
if(promise){
promise.then(function(data){
// promise被resolve的时候再次调用genRunner
// 以继续执行MyLogic中后面的逻辑
genRunner();
});
}
}
这样我们就把不停地调用myLogic.next()
和不停地promise.then()
的过程进行了封装。运行genRunner()
跑一下:
MyLogic after myAjax
Uncaught (in promise) TypeError: Cannot read property 'status' of undefined(…)
可见MyLogic在yield
后的语句的确被执行了,但是serverData
却没有值,这是因为我们在调用myLogic.next()
的时候没有把值传回去。稍微修改下代码:
// diff1: genRunner接受参数val
function genRunner(val){
// diff2: .next调用时把参数传过去,yield左边可以被赋值
var yieldValue = myLogic.next(val);
var promise = yieldValue.value;
if(promise){
promise.then(function(data){
// diff3: 调用genRunner时传递参数
genRunner(data);
});
}
}
这次一切都对了:
MyLogic after myAjax
serverStatus:200
至此我们已经把封装最核心的部分抽离出来了,我们的业务代码MyLogic()已经是“异步操作,同步写法”,再封装得更通用一些
var genRunner = function(GenFunc){
return new Promise(function(resolve, reject){
var gen = GenFunc();
var innerRun = function(val){
var val = gen.next(val);
// 如果已经跑完了,则resolve
if(val.done){
resolve(val.value);
return;
}
// 如果有返回值,则调用`.then`
// 否则直接调用下一次innerRun()
// 为简单起见,假设有值的时候永远是promise
if(val.value){
val.value.then(function(data){
innerRun(data);
});
}else{
innerRun(val.value);
}
}
innerRun();
});
};
这里我们将刚刚看过的封装改成了innerRun()
,并加上了自动调用。外面再封装了一层genRunner()
,返回一个Promise。在GenFunc()
全程调用完之后,Promise被resolve。
用起来是这样:
genRunner(function*(){
var serverData = yield myAjax();
console.log('MyLogic after myAjax');
console.log('serverStatus:%s',serverData.status);
}).then(function(message){
console.log(message);
});
Async/Await
Async/Await应该是目前最简单的异步方案了,首先来看个例子。
这里我们要实现一个暂停功能,输入N毫秒,则停顿N毫秒后才继续往下执行。
const sleep = time => {
return new Promise((resolve, reject)=> {
setTimeout( ()=> {
resolve();
}, time);
})
};
(async ()=> {
// 在这里使用起来就像同步代码那样直观
console.log('start');
await sleep(3000);
console.log('end');
})();
控制台先输出start,稍等3秒后,输出了end。
基本规则
async 表示这是一个async函数,await只能用在这个函数里面。
await 表示在这里等待promise返回结果了,再继续执行。
await 后面跟着的应该是一个promise对象(当然,其他返回值也没关系,只是会立即执行,不过那样就没有意义了…)
获得返回值
await等待的虽然是promise对象,但不必写.then(..),直接可以得到返回值。
const sleep = time => {
return new Promise( (resolve, reject) => {
setTimeout( () => {
// 返回 ‘ok’
resolve('ok');
}, time);
})
};
(async function () {
let result = await sleep(3000);
console.log(result); // 收到 ‘ok’
})();
捕捉错误
既然.then(..)不用写了,那么.catch(..)也不用写,可以直接用标准的try catch语法捕捉错误。
const sleep = time => {
return new Promise( (resolve, reject) => {
setTimeout( () => {
// 模拟出错了,返回 ‘error’
reject('error');
}, time);
})
};
(async () => {
try {
console.log('start');
await sleep(3000); // 这里得到了一个返回错误
// 所以以下代码不会被执行了
console.log('end');
} catch (err) {
console.log(err); // 这里捕捉到错误 `error`
}
})();
循环多个await
await看起来就像是同步代码,所以可以理所当然的写在for循环里,不必担心以往需要闭包才能解决的问题。
const sleep = time => {
return new Promise( (resolve, reject) => {
setTimeout( () => {
resolve();
}, time);
})
};
(async function () {
for (var i = 1; i <= 10; i++) {
console.log(`当前是第${i}次等待..`);
await sleep(1000);
}
})();
值得注意的是,await必须在async函数的上下文中的。
..省略以上代码
// 错误示范
(async function(){
let arr = [1,2,3,4]
arr.forEach(function (v) {
console.log(`当前是第${v}次等待..`);
await sleep(1000); // 错误!! await只能在async函数中运行
});
})()
// 正确示范
(async function(){
let arr = [1,2,3,4]
for(var v of arr) {
console.log(`当前是第${v}次等待..`);
await sleep(1000); // 正确, for循环的上下文还在async函数中
}
})()
多个await并发
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
const getdata = async function() {
let p1 = getJSON("https://api.github.com/search/users?q=1")
let p2 = getJSON("https://api.github.com/search/users?q=2")
// let foo = await p1;
// let bar = await p2;
//这样比较耗时,因为只有p1完成以后,才会执行p2,完全可以让它们同时触发。
let [foo, bar] = await Promise.all([p1,p2]);
let a = String(foo.items[0].id).substr(0,1);
let b = String(bar.items[0].id).substr(0,1);
return await getJSON("https://api.github.com/search/users?q="+a+b)
}
getdata().catch(err => {
console.log(err);
})
Async/Await与Promise
使用Async/Await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。