JavaScript之async/await实现
前言
yield在平时使用的是真的真的非常非常的少,他的出现频率对于大多人来说不亚于在Virtual Studio中随便打一个字符然后出现一个超级长的自动补全内容,而且这个超长自动补全恰好是你需要的内容一样的低概率。
但是,如果对于一些特殊情况,而且你恰好有适用场景,那么这个yield可以说是一把利剑。
而Babel中,把async转化成原生代码的插件transform-async-to-generator也是利用了yield进行。所以,在理解await之前,必须要先理解yield。
YIELD
首先我们要理解,yield是什么,有什么用,能干什么,怎么才能去获取Promise的控制权。想要了解这些问题,最好的办法就是看个例子。
let ge = (function* (){
let c = yield 5 // ge.next()=>
console.log(c)// =>15
})()
console.log(ge.next()) //=>{ value: 5, done: false }
console.log(ge.next(15)) //=>{ value: undefined, done: true }
如果是聪明的小伙伴,到这可能就已经明白了await/async他们的转换原理。如果没看懂也没关系,我们这里慢慢讲解。
首先上面发生了几件事情:
第一件是执行了一个IIFE,然后function ()就生成了一个iterable。注意,function ()中的这个星号(),是写在function关键词之后的,不是写在括号前面的。千万不要认为这个星号是一个函数名称。有些人喜欢写成function (),这些人要么就是不知道这是啥东西,要么就是要故意让你感到迷惑。都是些坏家伙
在获得一个迭代器后,当程序第一次调用iterable.next(),程序会立刻执行,知道遇到yield,同时next()的返回结构为{value:any,done:bool}。如果done为true,那么value将会是undefined,这就证明迭代器中的内容已经完全的执行完成了。同时,如果迭代器中显式return,同样的也算执行完成。你可以无限次的执行next(),但是,只要迭代器已经执行完成,那么一定会返回{value:undefined,done:true}。
当然,上述代码中,第一次next并没有执行完成,所以他会返回{ value: 5, done: false }。同时,当你继续调用next(),程序将会继续执行,不过,上述代码调用的时候传入了一个参数,值为15,那么,这个值会被当成上个yield的结果返回到迭代器中继续执行,所以此时c == 15。
BSYNC
了解了yield后,我们就来尝试实现一个async/await。当然,我们在代码中并没有直接修改js关键词和解析器的能力。所以我们这里会使用一些类似的方法来模拟。我们会创建一个新的函数bsync来模拟async,同时我们用yield来替代await
我们先尝试实现一个简单的。
function bsync(fn){
let ge = fn()
return ge.next().value
}
console.log(bsync(function* (){
yield 5
}))
好了,我们成功的实现了一个毫无作用的bsync函数。 很明显,我们需要yield的是一个Promise,而不是一个立即数,所以说我们需要yield <Thenable>。
function bsync(fn){
let ge = fn()
return ge.next().value
}
console.log(bsync(function* (){
yield new Promise((resolve)=>{
console.log(10)
resolve(10)
})
}))
//10
//Promise {10}
很明显,我们想使用async的目的不是为了这个。如果为了达到多个Promise顺序执行。我们需要加一点细节。
function bsync(fn){
let ge = fn()
let next = function(result){
if(result.done == true) return
next(ge.next())
}
return next(ge.next())
}
bsync(function* (){
yield new Promise(r =>{
console.log(10)
r(10)
})
yield new Promise(r =>{
console.log(20)
r(20)
})
})
//output
//10
//20
但是这并没涉及到Promise的异步内容,因为构造Promise的函数是会被立刻执行的。所以说我们的bsync其实没有发挥任何作用。那么我们来试试多加一个then
function bsync(fn){
let ge = fn()
let next = function(result){
if(result.done == true) return
next(ge.next())
}
return next(ge.next())
}
bsync(function* (){
yield new Promise(r =>{
console.log(10)
r(10)
}).then(v=>{
console.log(v + 5)// expect 20
})
yield new Promise(r =>{
console.log(20)
r(20)
})
})
//output
//10
//20
//15
bad news,我们的bsync并不能达到预期效果,所以我们还需要修改一下,我们需要让next的调用时机在上一个result的Promise结束后才调用。所以我们套一个Promise上去。为什么只需要套一个Promise?还记得Promise的return结果么,无论then有多长多少,你永远会获得最后一个then产生的Promise。
function bsync(fn){
let ge = fn()
let next = function(result){
if(result.done == true) return
Promise.resolve(result).then(()=>{
next(ge.next())
})
}
return next(ge.next())
}
bsync(function* (){
yield new Promise(r =>{
console.log(10)
r(10)
}).then(v=>{
console.log(v + 5)// expect 20
})
yield new Promise(r =>{
console.log(20)
r(20)
})
})
//output
//10
//15
//20
great!我们成功的实现了async/await!但是我们需要多添加一些测试用例,来验证我们的代码。
function bsync(fn){
let ge = fn()
let next = function(result){
if(result.done == true) return
Promise.resolve(result).then(()=>{
next(ge.next())
})
}
return next(ge.next())
}
bsync(function* (){
yield new Promise(r =>{
console.log(10)
r(10)
}).then(v=>{
console.log(v + 5)// expect 20
})
console.log('tmp')
yield new Promise(r =>{
console.log(20)
r(20)
}).then(()=>{
console.log(40)
})
let ten = yield Promise.resolve(10)
console.log(ten)
})
/*output
10
15
tmp
20
40
undefined
*/
大部分情况还是符合预期的,但是最后一个就出了点小问题,最后一个应该为10,而不是undefined。所以我们再次修改bsync
function bsync(fn){
let ge = fn()
let next = function(result){
if(result.done == true) return
Promise.resolve(result.value).then((value)=>{
next(ge.next(value))
})
}
return next(ge.next())
}
bsync(function* (){
yield new Promise(r =>{
console.log(10)
r(10)
}).then(v=>{
console.log(v + 5)// expect 20
})
console.log('tmp')
yield new Promise(r =>{
console.log(20)
r(20)
}).then(()=>{
console.log(40)
})
let ten = yield Promise.resolve(10)
console.log(ten)
})
/*output
10
15
tmp
20
40
10
*/
啊哈!完美! 也不对,那么如果我throw new Error或者Promise.reject会发生什么?
function bsync(fn){
let ge = fn()
let next = function(result){
if(result.done == true) return
Promise.resolve(result.value).then((value)=>{
next(ge.next(value))
})
}
return next(ge.next())
}
bsync(function* (){
try{
yield new Promise(r =>{
console.log(10)
r(10)
}).then(v=>{
console.log(v + 5)// expect 20
})
console.log('tmp')
yield new Promise(r =>{
console.log(20)
r(20)
}).then(()=>{
console.log(40)
throw new Error('boom')
})
let ten = yield Promise.resolve(10)
console.log(ten)
}catch(e){
console.log(e.message)
}
})
/* output
10
15
tmp
20
40
(node:34117) UnhandledPromiseRejectionWarning: Error: boom ....
*/
bad~,很明显,核弹捕获失败了。所以我们需要在bsync中加入关于错误处理的部分代码。
function bsync(fn){
let ge = fn()
let next = function(result){
if(result.done == true) return
Promise.resolve(result.value).then((value)=>{
next(ge.next(value))
},reason=>{
next(ge.throw(reason))
})
}
return next(ge.next())
}
bsync(function* (){
try{
yield new Promise(r =>{
console.log(10)
r(10)
}).then(v=>{
console.log(v + 5)// expect 20
})
console.log('tmp')
yield new Promise(r =>{
console.log(20)
r(20)
}).then(()=>{
console.log(40)
throw new Error('boom')
})
let ten = yield Promise.resolve(10)
console.log(ten)
}catch(e){
console.log(`onError:${e.message}`)
}
})
/* output
10
15
tmp
20
40
onError:boom
*/
啊哈!我们成功啦。我们成功的完成了async/await的所有功能!cheers!! 最后简单的修复一下最后返回结果的问题。
function bsync(fn){
let ge = fn()
let next = function(result){
if(result.done == true) return Promise.resolve(result.value)
Promise.resolve(result.value).then((value)=>{
next(ge.next(value))
},reason=>{
next(ge.throw(reason))
})
}
return next(ge.next())
}
这样以来,我们就成功的创建了一个能应付大多数情况的伪async了。
结语
其实async的原理和yield的原理是差不多的。我觉得他只是一个对yield进行封装的高阶语法糖。但是,async的性能是要远远高出连续then的,最重要的原因是,async只开辟了两个栈空间,而直接使用Promise.then,v8就需要保存之前的栈环境。当然了,更加具体的优化,就是由v8来进行处理了。