同系列文章: JavaScript异步编程:async/await
快速回顾 promise 与 generator
我们都知道,async/await 只是 promise 的语法糖🍬。
在进入正文前,让我们来快速回顾一下 promise 和 generator。
Promise
promise 的出现就是为了解决回调地狱。在 promise 出现以前,为了拿到异步事件处理结果从而开展后续工作,需要使用回调函数。当我们需要串行执行很多有依赖关系的异步事件时,就会形成回调地狱:
promise 相当于给了我们一个异步事件结果的承诺,允许我们后续添加处理函数。将以上代码用 promise 可以改写为:
js
loadScript("1.js")
.then(function(script) {
return loadScript("2.js");
})
.then(function(script) {
return loadScript("3.js");
})
.then(function(script) {
// ...
})
.catch(function(error) {
// ...
})
是不是优雅了很多!
promise 初始状态是 pending
,如果 resolve(value)
执行,则变为 fulfilled
;如果 reject(error)
执行,则变为 rejected
。
执行函数中 resolve
和 reject
只会执行一个,其余的会被忽略。
js
new Promise((resolve, reject) => {
setTimeout(() => resolve("value"), 2000);
})
.finally(() => alert("Promise ready")) // triggers first
.then(result => alert(result)); // <-- .then shows "value"
promise 的链式调用中,每个 then
都可以返回一个 promise 或 thenable 对象(有 .then
方法的任意对象),继续传递给下一个 then
,而 finally
并不参与结果的传递,返回的任何结果都会被忽略,此前的 promise 结果将会穿过 finally
直接向下传递。在链式调用中,只需要在最后添加一个 catch
来捕获错误,之后可以继续添加 then
来处理。未被 catch
捕获的错误可以通过监听全局事件 unhandledrejection
捕获。
一个 settled promise 状态将不会再发生改变,并且 promise 是无法取消的。
Promise API:
Promise.all
:并行处理多个 promise,只要有一个被 reject 了,则整个 promise 立刻被 reject,其他结果将会被忽略。Promise.allSettled
:并行处理多个 promise,不论结果如何都会等待所有 promise,返回结果对于成功的则为{status:"fulfilled", value:result}
;失败的则为{status:"rejected", reason:error}
。Promise.race
:返回第一个结果,无论成功与否。Promise.any
:返回第一个成功结果。Promise.resolve/reject
:返回一个 resolved / rejected 的结果。
Generator
调用 generator function 可以创建一个generator 对象,通过 yield
关键词可以让函数返回多个结果。每次调用 generator 的 next
方法,都会执行到最近的 yield <value>
,然后函数执行暂停,value
被返回。调用 next
方法返回一个形如 {value: any, done: false}
的对象,最后一个 yield
执行完毕后 done
将变为 true
。调用已结束的 generator 只会返回 {value: undefined, done: true}
。
js
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "generator function" creates "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]
let one = generator.next(); // {value: 1, done: false}
let two = generator.next(); // {value: 2, done: false}
let three = generator.next(); // {value: 3, done: true}
可以遍历 generator 直至 done
为 true
。
js
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, then 2
}
let sequence = [...generateSequence()]; // 1, 2
generator 也可以进行组合代理,使用 yield*
将另一个 generator 嵌入进来。
js
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
yield* generateSequence(48, 57); // 0..9
yield* generateSequence(65, 90); // A..Z
yield* generateSequence(97, 122); // a..z
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
yield
是个双向通道,既可以向外返回值,也可以向内输送值。从第二个 next()
起,可以传递参数到 generator 内部。
js
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
let generator = gen();
alert( generator.next().value ); // "2 + 2 = ?"
alert( generator.next(4).value ); // "3 * 3 = ?"
alert( generator.next(9).done ); // true
generator 也可以支持异步。
js
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
(async () => {
let generator = generateSequence(1, 5);
for await (let value of generator) {
alert(value); // 1, then 2, then 3, then 4, then 5 (with delay between)
}
})();
next
调用方法:let result = await generator.next()
。
其他 API:
generator.throw
:抛出一个错误,可以在函数内通过 try/catch 捕获,也可以在函数外捕获。generator.return(value)
:强行结束 generator,返回{value: 传入的value, done: true}
。
Generator + Promise = async/await?
先来看一个例子,假设我们可以通过 foo
来根据参数获取一些特定的数据,返回一个 promise:
js
function foo(x,y) {
return request("http://some.url.1/?x=" + x + "&y=" + y);
}
按照一般的方式,我们通常会这样调用:
js
foo(11, 31)
.then(
function(text){
console.log( text );
},
function(err){
console.error( err );
}
);
现在把它放到一个 generator function 里:
js
function *main() {
try {
var text = yield foo(11, 31);
console.log(text);
} catch (err) {
console.error(err);
}
}
已知 foo(11, 31)
返回的是一个 promise,如果想要成功拿到它的值,根据 generator 的特性,就需要这么做:
js
var it = main();
var p = it.next().value;
p.then(
function(text){
it.next(text);
},
function(err){
it.throw(err);
}
);
看下发生了什么?
- 在 generator function 里
yield
一个 promise; - 调用
next
拿到这个 promise; - 等待 promise 完成,并在
then
里拿到最终的结果; - 再次调用
next
并通过传参将 promise 的结果传回 generator function,此时text
就会拿到想要的结果; - 如果发生了错误就在外面
throw
回去,并被函数里面的catch
捕获。
是不是有点眼熟?这个过程正好就是 async/await
做的事!
js
async function main() {
try {
var text = await foo(11, 31);
console.log( text );
} catch (err) {
console.error( err );
}
}
更进一步,我们可以把以上的手动操作通过一个 helper 函数自动化。毕竟,如果每次 yield
一个 promise 都需要在外面监听 promise 的最终结果,再回传给函数内部,还是挺繁琐的。
在下面的代码中,run
接收一个 generator function 作为参数,并自动执行里面的任务。
js
function run(gen) {
var args = [].slice.call(arguments, 1); // 收集除 gen 以外的额外传递的参数
var it = gen.apply(this, args); // 调用 generator function 得到 generator 对象
return Promise.resolve().then(function handleNext(value){ // 确保返回的是promise
var next = it.next(value); // 把值传递回 generator function 内部,拿到下一次需要处理的值
return (function handleResult(next){
if (next.done) { // 如果所有值都 yield 完毕,返回最终的值
return next.value;
} else { // 如果 yield 没有执行完,则继续迭代
return Promise.resolve(next.value)
.then(handleNext, function handleErr(err) { // 如果 promise 成功,则继续处理,并把值返回给 generator function;如果失败,则把错误扔回 generator function 内部,然后继续处理剩下的
return Promise.resolve(it.throw(err)).then(handleResult);
});
}
})(next);
});
}
这个自动执行 generator funtion 的 helper 函数的执行流程如下:
- 调用传入的 generator function 得到 generator 对象。
- 接下来将循环处理所有
yield
的值。 - 第一次调用
next
方法,传入的value
被忽略,拿到结果。 - 如果
done
为true
,说明已经处理完所有值,直接返回最终值。 - 否则等待该值(promise)的结果,如果 promise 被 resolve,则将成功值通过
next
传回 generator function 内部,并针对next
的返回值开启新一轮的处理(第 4 步);如果 promise 被 reject,则将错误抛回 generator function,然后继续处理接下来的值。
写一个小例子来测试一下:
js
function asyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
});
}
function* gen() {
let result = yield asyncTask();
return result;
}
// async/await
async function run1() {
let result = await asyncTask();
console.log(result); // ---> test 1: expected to be 1
}
run1();
// promise + generator
run(gen).then(v => console.log(v)); // ---> test 2: expected to be 1
直接使用 async/await
和使用 promise + generator 的方式得到了相同的结果。
延伸:事件循环 Event loop
在事件循环模型中,JS 引擎执行的任务可以分为宏任务和微任务。在每个宏任务结束后,在执行下一个宏任务之前,JS 引擎都会扫描一下当前的微任务队列中是否有微任务,如果有的话就会执行这些微任务。promise 的处理函数.then
/ .catch
/ .finally
都是微任务。
以下是一个常见的面试题,考察代码的执行顺序:
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
可以先试着自己回答一下。
了解了 async/await
的内部机制后,就很容易作答了。await
之前是同步代码,自然会按顺序执行,紧跟在 await
后面的函数也会立刻执行(想象 yield
后跟一个产生 promise 的函数,会立即执行,只是返回的 promise 的状态是 pending
)。接着函数被暂停,所以函数外的同步代码会继续执行。而由于需要等待 promise 变为 settled 之后,才会回到函数内部继续执行,恢复函数执行的动作发生在 promise 的 then/catch
的处理函数内,这一步是异步的,会被放在微任务队列中,所以会等待所有函数外部的同步代码都执行完,才会恢复函数内部的执行。
下面揭晓答案,你答对了吗?
text
async1 start
async2
start
async1 end
timer2
timer3
timer1