async、await的实现原理
数组和类数组的迭代
先看一个例子
const likeArray = {0:'a',1:'b',2:'c',3: 'd',length: 4}
console.log('打印:', [...likeArray])
// 输出结果: TypeError: likeArray is not iterable console.log(Array.from(likeArray))
// [ 'a', 'b', 'c', 'd' ]
结论
一个类数组的对象在通常状态下,无法被解构,因为这个对象上没有迭代器,除了使用Array.from方法可以将类数组变为一个数组。
想要让一个类数组可以迭代,我们可以手动改写原生的遍历器。给对象增加一个遍历器接口,必须返回一个对象包含next方法,next方法返回的对象里,当done为true,就停止遍历
const likeArray = {0:'a',1:'b',2:'c',3: 'd',length: 4}
likeArray[Symbol.iterator] = function (params) {
let i = 0
return {
next:() => {
return {value: this[i], done: i++ === this.length}
}
}
}
console.log('打印:', [...likeArray])
/**
* { value: 'a', done: false }
{ value: 'b', done: false }
{ value: 'c', done: false }
{ value: 'd', done: false }
{ value: undefined, done: true }
打印: [ 'a', 'b', 'c', 'd' ]
*/
使用generator简化遍历的操作
likeArray[Symbol.iterator] = function *() {
let i = 0
while(i !== this.length) {
yield this[i++]
}
}
console.log('打印:', [...likeArray])
// 打印: [ 'a', 'b', 'c', 'd' ]
分析
在形式上generator函数是一个普通函数,但是有两个特征,一是,function关键字与函数名之间有一个*号;二是,函数体内部使用yield表达式,定义不同的状态,意思是产出。
generator的使用
先看一个例子
function *read() {
yield 1
yield 2
yield 3
}
let it = read() // it就是迭代器,迭代器上有next方法 console.dir(it.next()) // { value: 1, done: false } console.dir(it.next()) // { value: 2, done: false } console.dir(it.next()) // { value: 3, done: false } console.dir(it.next()) // { value: undefined, done: true }
console.dir(it.next()) // { value: undefined, done: true }
// 使用dowhile简化
let flag = false
do {
let {value, done} = it.next()
console.log(value)
flag = done
} while (!flag)
分析
上述代码中read是一个generator函数,执行read函数返回一个遍历器,遍历器上有next方法。
每次执行next方法,只会走到下一个yield关键字,返回包含value和done的对象
当执行next方法后没有yield关键字了,就会返回{ value: undefined, done: true }
再看一种next传参的例子
function *read() {
let a = yield 1
console.log(a)
let b = yield 2
console.log(b)
let c = yield 3
console.log(c)
return c
}
// 蛇形执行,除了第一次之外的next方法,都是把next中的参数传递给上一次yield的返回结果
let it = read()
console.log(it.next()) // 第一次的next传递参数没有意义,因为第一次之前并没有yield,返回{ value: 1, done: false }
console.log(it.next(2)) // 这次打印a,并且走到yield 2, 返回{ value: 2, done: false }停止 console.log(it.next(3)) // 这次打印b,并且走到yield 3, 返回{ value: 3, done: false }停止
console.log(it.next(4)) // 这次打印4,返回{ value: 4, done: true }结束
分析
当执行第一次next,代码走到yield 1,此时没有传参,因为第一次之前没有yield,返回{ value: 1, done: false }
遍历器的执行顺序是:蛇形执行,除了第一次之外的next方法,都是把next中的参数传递给上一次yield的返回结果
第二次执行it.next(2)相当于给yield 1返回值a赋值2,并且指针走到yield 2,所以打印2,{ value: 2, done: false }
后面依次类推,直到it.next(4),遇到到关键字return,遍历结束,返回4,{ value: 4, done: true }
结论
可以发现,单用generator实现功能,其实并没有方便多少,反而运行的思路,会让人非常难受,唯一的好处就是,函数是可以中断执行的,怎么利用好这种特性是关键
Async、await的由来
来看一个例子
我们先从根目录读取name.txt,再根据name.txt的返回值,读取age.txt,获取age.txt的返回值
const fs = require('fs').promises
function *read() {
// 支持trycatch
try {
let name = yield fs.readFile('name.txt', 'utf8')
console.log('存在name.txt中的值', name)
let age = yield fs.readFile(name, 'utf8')
return age
} catch (error) {
console.log(error)
}
}
let it = read()
let { value, done } = it.next()
value.then(data => {
let {value, done } = it.next(data)
value.then(data => {
let {value, done } = it.next(data)
console.log('存在age.txt中的值', value)
// it.throw('error')
})
})
// 存在name.txt中的值 age.txt
// 存在age.txt中的值 28岁
分析
可以看见generator函数支持trycatch,而且可以把异步调用的代码,写成同步的形式,只是我们获取值的方法,还是太恶心了,产生了回调地狱,但是可以发现,有很多重复的地方。
实现co库,优化回调地狱
// let it = read()
// let { value, done } = it.next()
// value.then(data => {
// let {value, done } = it.next(data)
// value.then(data => {
// let {value, done } = it.next(data)
// console.log('存在age.txt中的值', value)
// // it.throw('error')
// })
// })
// 将上述方法,改为下面的写法
const co = (it) => {
return new Promise((resolve, reject) => {
function next(data) {
let {value, done} = it.next(data)
if(done) {
resolve(value)
} else {
Promise.resolve(value).then(next, reject)
}
}
next()
})
}
co(read()).then(data => {
console.log(data)
})
思路
可以看见co包装read函数后,返回一个promise,并在遍历器遍历结束后, 将结果resolve出来,这就是async+await的原理了。
更多源码解析内容欢迎关注在线客服软件海豚客服资深工程师Porschebz
下一篇: 男人基础护肤 不可忘四小窍门