本文将参考ES6文档一步一步实现一个async 函数。
首先、我们看一段关于async调用的代码
async function task() {
let resultOne = await new Promise((resolve) => {
setTimeout(resolve.bind(null, 1), 2000);
})
let resultTwo = await new Promise((resolve) => {
setTimeout(resolve.bind(null, 2), 2000);
})
return resultOne + resultTwo;
}
const now = Date.now();
task().then(res => {
console.warn(`res = ${res}; duration = ${Date.now() - now}`);
});
输出结果是res = 3, duration 约等于 4000,也就是说后面的task要等前面的task执行完毕才能开始运行。 而我们知道一个function一旦调用了、代码就会依次往下执行、如何实现等待的效果?
如果我们能将函数拆开,控制一段一段、甚至一行一行的执行就可以实现了,Generator 函数就是用来解决这个问题的。
Generator 函数简介
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。Generator 函数的使用方式可以参考代码:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next();
// { value: 'hello', done: false }
hw.next();
// { value: 'world', done: false }
hw.next();
// { value: 'ending', done: true }
hw.next();
// { value: undefined, done: true }
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。这两个特征就对应的async和await。
调用 Generator 函数后,该函数并不执行、而且返回一个遍历器、然后必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
开始实现
有了 Generator 函数函数后、我们就可以尝试实现一个async了。首先第一步就是去掉async关键字,封装一个内部Generator 函数,将async函数内部的代码全部复制过来扔进Generator 函数、但是我们需要把await改成成yield、最后返回一个Promise。
就拿上面的task函数来进行举例:
function task() {
function *innerTask() {
// async函数方法体拷贝过来、把await改成 yield
let resultOne = yield new Promise((resolve) => {
setTimeout(resolve.bind(null, 1), 2000);
})
let resultTwo = yield new Promise((resolve) => {
setTimeout(resolve.bind(null, 2), 2000);
})
return resultOne + resultTwo;
}
// 返回一个Promise
return new Promise((resolve, reject) => {
})
}
我们已经成功去掉了async 和await,剩下就是利用Generator 函数的next方法去按顺序执行任务就可以了。
function task() {
function *innerTask() {
// async函数方法体拷贝过来、把await改成 yield
let resultOne = yield new Promise((resolve) => {
setTimeout(resolve.bind(null, 1), 2000);
})
let resultTwo = yield new Promise((resolve) => {
setTimeout(resolve.bind(null, 2), 2000);
})
return resultOne + resultTwo;
}
// 返回一个Promise
return new Promise((resolve, reject) => {
let g = innerTask();
function run(next) {
let result;
try {
result = next();
} catch(e) {
return reject(e);
}
if (!result.done) {
run(() => g.next(result.value));
} else {
resolve(result.value);
}
}
run(() => g.next(undefined));
})
}
const now = Date.now();
task().then(res => {
console.warn(`res = ${res}; duration = ${Date.now() - now}`);
});
本质上是一个递归依次调度任务执行,但是输出结果res = [object Promise][object Promise]; duration = 1,不符合预期。问题出在哪里了呢?
是因为我们没有等第一个任务结束了就开始了第二个任务、那么如何实现任务的挂起排队呢,很简单、只要把第二个任务注册到第一个任务的then方法里就可以了,所以我们只需要稍微调整下代码即可:
function task() {
function *innerTask() {
// async函数方法体拷贝过来、把await改成 yield
let resultOne = yield new Promise((resolve) => {
setTimeout(resolve.bind(null, 1), 2000);
})
let resultTwo = yield new Promise((resolve) => {
setTimeout(resolve.bind(null, 2), 2000);
})
return resultOne + resultTwo;
}
// 返回一个Promise
return new Promise((resolve, reject) => {
let g = innerTask();
function run(next) {
let result;
try {
result = next();
} catch(e) {
return reject(e);
}
if (!result.done) {
Promise.resolve(result.value).then((res) => {run(() => g.next(res))}, (e) => g.throw(e));
} else {
resolve(result.value);
}
}
run(() => g.next(undefined));
})
}
const now = Date.now();
task().then(res => {
console.warn(`res = ${res}; duration = ${Date.now() - now}`);
});
这样一个自定义async函数就基本成型了。
上述代码调整的原理是利用了promise.reslove的特性,如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例, 如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved。
最后一点小猜想
为什么用了await后try-catch就能捕获promise内部的错误了?
本质原因是Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
function *gen() {
try {
yield 1;
yield 2;
} catch(e) {
console.log('catch error = ' + e);
}
}
var g = gen();
g.next();
// g.throw('mistake!');
g.next();
g.throw('mistake!');
在Generator 函数执行一次next之后,直至状态变为done之前,我们在任何地方调用g.throw都会被内部的try-catch捕获。
结合这个特性,我们只需要在每个任务后面加上一个catch然后调用g.throw就可以触发外侧的try-catch了。这里补充一句:then(func1, func2) 本质上等价于then(func1).catch(func2),后者是前者的语法糖。
最后总结一下,async,await本质上就是Generator 函数和yield的语法糖,以后看见async,await直接用Generator 和yield替换即可。