这篇手写async函数及原理|博客系列
# 正文
ES8推出的async/await允许我们用更加优雅的方式来实现异步代码,从某种角度上来说,它是Generator函数的语法糖,就像我们经常说class是构造函数的语法糖一样。
# async案例
基本用法啥的我就不说了,这里直接上一个简单的案例,后面一步步的转换过程都是以此案例作为基础。
const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))
async function test () {
const data = await getData();
console.log('data: ', data);
const data2 = await getData();
console.log('data2: ', data2);
return 'success';
}
test().then(res => console.log(res));
针对于上面的这段代码,相信大家都没有什么疑问,很快我们就能说出答案:
// 1s 后打印出
'data ' 'data'
// 1s 后打印出
'data2: ' 'data'
'success'
# Generator案例
让我们先来回顾一下Generator最基本的一些概念:
function后面加上*表示这是一个Generator函数,如function* testG(){}- 函数内部可以使用
yield来中断函数的执行,即当每次执行到yield语句的时候,函数暂停执行 - 暂停执行之后,需要调用
next()才会继续执行Generator函数,直到碰到函数内下一个yield又会暂停 - 以此循环,直到函数内有
return或者函数内代码全部执行完
其中还有很重要的一个知识点,就是每次调用next()的返回值,它是一个对象,这个对象会有两个属性:
value:yield语句后的表达式的结果done:当前的Generator对象的逻辑块是否执行完成
如果说我们把上面的案例转换为Generator来实现的话,我们想的可能会是这样来写:
案例一:
const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))
function* testG () {
const data = yield getData();
console.log('data: ', data);
const data2 = yield getData();
console.log('data2: ', data2);
return 'success';
}
var gen = testG();
// 之后手动调用3次
gen.next();
gen.next();
gen.next();
可是上面👆的代码真的会和async案例中的执行结果一样吗?
当我打开控制台的时候,结果却出乎我的意料:
data: undefined
data2: undefined
What…?
你的suceess没有打印就算了,你的data和data2竟然都还是undefined?这就有点难以理解了。
难道说是我的调用姿势不对吗…
本着良好的职业素养,我对Generator研究了一波,然后修改了一下上面的代码:
案例二:
const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))
function* testG () {
const data = yield getData();
console.log('data: ', data);
const data2 = yield getData();
console.log('data2: ', data2);
return 'success';
}
var gen = testG();
// 手动调用3次且把每次的返回值打印出来看看
var dataPromise= gen.next();
console.log(dataPromise);
var dataPromise2 = gen.next('这个参数才会被赋给data变量');
console.log(dataPromise2);
var dataPromise3 = gen.next('这个参数才会被赋给data2变量');
console.log(dataPromise3);
可以看到不同之处在于我把调用三次的返回值用了一个变量来盛放,并且在后面两次调用gen.next()的时候传递了参数进去。
现在的输出结果为:
{value: Promise, done: false}
'data:', '这个参数才会被赋给data变量'
{value: Promise, done: false}
'data2:', '这个参数才会被赋给data2变量'
{value: Promise, done: false}
What X 2…?
哈哈哈,是不是有点摸不着头脑了,这data和data2的返回值难道不是yield getData()的结果吗?
我这里的代码明明就是const data = yield getData()呀,可是怎么会变成了'这个参数才会被赋给data变量'呢?
原来有些的你以为并不是真的你以为,这里详细把每一步都分析一下:

OK👌,相信聪明的你现在一定弄懂Generator的执行机制了,它和我们的async函数是有一些区别的。
在async函数中,const data = await getData(),这个data是getData()中resolve()的结果。
而generator函数中,const data = yield getData(),其实只执行了yield getData()函数而已并且会把这个值作为第一次调用gen.next()的返回值,也就是被dataPromise所接收。
data需要等到下一次调用gen.next()时才会被赋值,这也就是为什么我们在案例一中data会是undefined,因为那时候我们调用gen.next()是没有传递任何参数的。
# Generator案例转换async案例
显然,上面👆两个案例的执行结果和async案例中的结果是不一样的,我只是将其做了一些拆分以便让你更好的理解接下来我要做的事情。
嘻嘻😁,那么如何让案例二能打印出和async案例一样的结果呢?
细心的小伙伴可能已经有了一些想法,dataPromise中会有调用返回的Promise对象,那么我们也就能拿到这个Promise的返回值'data'了,只需要使用.then()来进行一个链式调用,就像下面这样:
const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))
function* testG () {
const data = yield getData();
console.log('data: ', data);
const data2 = yield getData();
console.log('data2: ', data2);
return 'success';
}
var gen = testG();
var dataPromise = gen.next();
dataPromise.value.then((value1) => {
var dataPromise2 = gen.next(value1);
dataPromise2.value.then((value2) => {
var dataPromise3 = gen.next(value2)
console.log(dataPromise3.value)
})
})
现在的结果就是和async案例的结果一样咯:
(如果你对这道题的结果还是比较模糊的话请再仔细看一下我在案例二的那一大坨代码注释哦)
// 1s 后打印出
'data ' 'data'
// 1s 后打印出
'data2: ' 'data'
'success'
(注意⚠️,在每次调用gen.next()的时候,它的返回值是一个{ value: {}, done: false }这样的对象,所以我们想要使用返回的Promise的时候,需要用dataPromise.value来获取)
# throw()
在正式讲解之前,让我们再来认识一下Generator的另一个实例方法throw(),之所以说到它,是因为我们最终的代码需要用到它。
它和next()一样,都是属于Generator.prototype上的方法,且返回值也是和next()一样。
让我们来看一个简单的案例了解一下它是怎样使用的哈:
(利用while(true){}循环,我们创建了一个可以无限调用的Generator函数)
function* gen () {
while (true) {
try {
yield 'LinDaiDai'
} catch (e) {
console.log(e)
}
}
}
var g = gen();
g.next(); // { value: 'LinDaiDai', done: false }
g.next(); // { value: 'LinDaiDai', done: false }
g.throw(new Error('错误')); // Error: '错误'
而且你会发现,并不是throw中一定要传一个new Error()才会被里面的catch捕获,你就算是传递一个别的类型的值进去,也会,例如我直接传递字符串'错误':
g.throw('错误'); // '错误'
它也会被捕获。
# Generator实现async?
我们在搞懂了Generator的执行机制之后,就可以来看看,async是怎样用Generator来实现的了。
首先,让我们确定一下我们要做的事情:
const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))
function* testG () { // 这个就是上面的那个案例
const data = yield getData();
console.log('data: ', data);
const data2 = yield getData();
console.log('data2: ', data2);
return 'success';
}
// 我们需要设计一个转换函数
function asyncToGenerator (genFunc) {}
var test = asyncToGenerator(testG);
test().then(res => console.log(res));
那么可以看到,现在的关键就是在于实现一个asyncToGenerator转换函数,它有以下特点:
- 接收一个
Generator函数 - 返回一个
Promise
而Generator你们也看到了,它是很懒的,需要我们每调用一次next()函数它才走一步,所以如何让它自动的执行成为了我们需要思考的点。
