Iterator&Generator&await/async
前言
从 Iterator 出发到 Generator 再到 await/async。
Iterator
-
基本用法
这里只展示 Iterator 的最基本用法,不会过多赘述 Iterator 的基础
1 let a = new Set().add(1).add(2).add(3) 2 let aIterator = a[Symbol.iterator]() 3 console.log(aIterator.next()) 4 console.log(aIterator.next()) 5 console.log(aIterator.next())
这里主要是主动调用 Set 的遍历器生产器(Symbol.iterator)生成一个遍历器对象,再用遍历器对象的 next 方法一一取值。for....of 、展开运算符等都是会自动调用遍历器生成函数。
根据 Iterator 的规范可知:
1:一个对象的遍历器生成函数部署在 Symbol.iterator 上;
2:生成的遍历器对象有 next、return、throw 等方法。
-
实现一个 Iterator
根据上面两点,实现一个基本的 Iterator
1 const obj = { 2 it: function () { 3 return { 4 next: function () { 5 return { 6 value: 1, 7 done: true 8 }; 9 } 10 }; 11 } 12 }; 13 14 let aIt = obj.it() 15 console.log(aIt.next())
上面 obj 对象的 it 方法,返回一个对象,返回的对象有 next 方法,而 next 方法返回值。
上面代码结构基本符合一个 Iterator,有生成遍历器对象的函数,也有 next 返回值。
但是上面代码,如果用 for...of 运行是不行的,因为 Iterator 要部署到 Symbol.iterator 上,表示该对象是一个可遍历的对象,把 it 改为 Symbol.iterator 即可。
1 for (const it of obj) { 2 console.log(it) 3 }
使用 for..of 运行上面还没改过的代码会直接报错:TypeError: obj is not iterable 。
1 const obj = { 2 [Symbol.iterator]: function () { 3 return { 4 next: function () { 5 return { 6 value: 1, 7 done: true 8 }; 9 } 10 }; 11 } 12 } 13 14 for (const it of obj) { 15 console.log(it) 16 }
上面 obj 改过后,再用 for..of 成功运行,但由于返回值是 done: true,说明已经是最后一个,所以没有遍历出值。
1 const obj = { 2 data: [1, 2, 3, 4], 3 [Symbol.iterator]: function () { 4 let index = -1 5 let self = this 6 return { 7 next: function () { 8 index++ 9 if (index < self.data.length) { 10 return { 11 value: self.data[index], 12 done: false 13 }; 14 } 15 return { 16 done: true 17 }; 18 19 } 20 }; 21 } 22 } 23 24 for (const it of obj) { 25 console.log(it) 26 }
再看以上代码,成功遍历出 1 2 3 4,其核心是用 index 获取数据,next 控制index
Generator
-
Generator 实现原理
Generator 原理跟 Iterator 非常像,都是以某种方式控制指针指向,以运行返回特定的值。
1 function GenObj() { 2 3 this.context = { 4 next: 0, 5 done: false 6 } 7 8 9 10 } 11 12 GenObj.prototype = { 13 _gen: function (context) { 14 15 switch (context.next) { 16 case 0: 17 context.next = 2 18 return 1; 19 20 case 2: 21 context.next = 4 22 return 2; 23 case 4: 24 context.next = 6 25 return 3; 26 case 6: 27 context.next = 8 28 return 4; 29 case 8: 30 context.done = true 31 return 32 33 } 34 35 }, 36 37 next: function () { 38 let self = this 39 return { 40 value: self.context.done ? undefined : self._gen(self.context), 41 done: self.context.done 42 } 43 } 44 } 45 46 let g = new GenObj() 47 console.log(g.next().value ,g.next().value ,g.next().value ,g.next().value)
Generator 使用 switch 来匹配当前的指针指向,然后运行相应的代码并改变指针指向。Generator 使用 context 来保存当前的指向。
-
Iterator 与 Generator
根据上面可以看出,Iterator 与 Generator 非常像,这里我用自己的观点说说区别:
1:Iterator 是特指可遍历的,里面的数据是线性的,而 Generator 是一段一段的;
2:Generator 本身就是一个生成器,而 Iterator 是某个对象身上的,部署在 Symbol.iterator 上。
await/async
await/async 其实就是 Generator 的语法糖。但只是 Generator 还不够,还需要解决以下问题:
-
问题1:何时执行
如果 Generator 的 yield 后面执行的是异步任务,那么在 next 的时候仍然不是同步的,需要上一个的 next 的异步执行完,才执行下一个的 next,所以需要明确何时执行下一步:
一般异步函数,要么是 Promise,要么有执行完后的回调,那么 Promise 的 resolve,reject 可以知道何时执行下一步,而回调本身就是异步执行完后调用的,所以在回调中执行下一个 next 即可。
为了让 yield 后面执行完后返回一个可以执行回调的函数,所以就用到了 Thunk。
1 const fs = require('fs') 2 3 const Thunk = function (fn) { 4 return function (...args) { 5 return function (callback) { 6 return fn.call(this, ...args, callback) 7 } 8 } 9 } 10 11 function* gen() { 12 let readFile = Thunk(fs.readFile) 13 let a = yield readFile('./a.txt') // (a.txt里的数据是 1) 14 yield a + 1 15 } 16 g.next().value(function(err, data){ 17 if (err) return 18 let r = parseInt(data.toString()) // 1 19 console.log(g.next(r).value) 20 })
上面代码需要根据 fs.readFile 读取的结果进行相加,所以需要先等 fs.readFile 执行完成才能执行 a + 1。
上面代码经过 Thunk 包装后,使得第一个 yield 返回的值是一个函数,这个函数的参数就是 fs.readFile 的回调,使之能够在外部控制何时执行下一步 next,所以在执行第二个 next 的时候,返回值为 2。
如果没有包装 Thunk ,那么代码如下:
1 function* gen() { 2 let a = yield fs.readFile('./a.txt', function(err, data){ 3 if(err) return 4 console.log(data.toString()) 5 }) 6 yield a + 1 7 } 8 let g = gen() 9 console.log(g.next().value) 10 console.log(g.next().value)
因为不知道何时执行下一个 next ,所以只能连续执行,结果也不是期望的。
下面看看 Promise 的方式:
1 function* gen() { 2 let a = yield new Promise((r) => { 3 setTimeout(() => { 4 r(1) 5 }, 3000) 6 }) 7 yield a + 1 8 } 9 10 let g = gen() 11 g.next().value.then(data => { 12 console.log(g.next(data).value) 13 })
使用 Promise 就简便许多,Promise 的 resolve 确认了何时执行 then 里的回调,所以在 then 里的回调执行 g.next,这样就达到了同步的目的。
Generator 能够同步执行的关键在于 Generator 可以一步一步执行代码,就是 next,而 Thunk 和 Promise 明确了何时可以执行下一个 next ,两者结合就能达到同步目的。
-
问题2:不能自动执行
在实现自动执行之前先看看手动执行的样子:
1 function* gen() { 2 let a = yield new Promise((r) => { 3 setTimeout(() => { 4 r(1) 5 }, 1000) 6 }) 7 let b = yield new Promise((r) => { 8 setTimeout(() => { 9 r(a + 1) 10 }, 1000) 11 }) 12 let c = yield new Promise((r) => { 13 setTimeout(() => { 14 r(b + 1) 15 }, 1000) 16 }) 17 yield c + 1 18 } 19 20 let g = gen() 21 g.next().value.then(data => { 22 return g.next(data).value 23 }).then(data => { 24 return g.next(data).value 25 }).then(data => { 26 console.log(g.next(data).value) 27 })
手动执行的缺陷是需要明确知道 gen 里有多少个 yield,以便在外部执行相应次数 next,并且,如果是 Thunk 实现的话,那么就变成了回调地狱了。
这里可以根据 next 返回的值的 done 来明确是否需要再次执行 next,并且观察上面代码的执行部分,g.next().value.then(data => {....}) 部分是通用的,所以可以将这部分另写函数。
1 let g = gen() 2 g.next().value.then(data => { 3 g.next(data).value.then(data => { 4 g.next(data).value.then(data => { 5 console.log(g.next(data).value) 6 }) 7 }) 8 })
上面这种调用方式就比较清晰的看出 g.next().value.then(data => {....}) 部分是重复的。
1 let g = gen() 2 function _next(value) { 3 let res = g.next(value) 4 if (res.done) return 5 res.value.then(data => { 6 _next(data) 7 }) 8 } 9 _next()
改成这样就可以自动运行了。执行上面的代码。虽然最后报错了( TypeError: res.value.then is not a function ),但是除了最后一个 yield ,其余都是成功执行的,这里报错是因为最后一个 yield 返回的不是 Promise, _next 这里也只处理了是 Promise 的情况。
如果 gen 里的代码是 Thunk 实现的,那么用 _next 也会报错,因为不是 Promise ,所以下一个需要修复的问题就是这个。
-
问题3:只能处理是 Promise 的情况
这里最简单的解决方法就是在 res.value 上包装一层 Promise 即可,如下:
1 let g = gen() 2 function _next(value) { 3 let res = g.next(value) 4 if (res.done) return 5 Promise.resolve(res.value).then(data => { 6 _next(data) 7 }) 8 } 9 _next()
这样就不会报错了。
-
问题4:不能返回值
上面 _next 的实现,不能接收 gen 执行完后的返回值,如果直接 console.log(_next()) ,打印结果是 undefined。
因为 _next 里面有 Promise.then ,而 Promise.then 的回调是异步任务,所以直接返回 undefined。
要解决这一问题也很简单,重新将上面代码封装一个函数,再用 Promise 包裹即可,在 res.done 为 true 时 resolve:
1 function run(gen) { 2 return new Promise((resolve, reject) => { 3 let g = gen() 4 let val 5 function _next(value) { 6 let res = g.next(value) 7 if (res.done) { 8 return resolve(val) 9 } 10 val = res.value 11 Promise.resolve(res.value).then(data => { 12 _next(data) 13 }) 14 } 15 _next() 16 }) 17 } 18 19 run(gen).then((e) => { 20 console.log(e) 21 })
上面代码成功打印出 4 。但这里有个值得注意的问题,在 res.done 为 true 的时候 resolve,因为此时 res.value 的值必定为 undefined,所以需要 resolve 的值是 res.done 为 true 时的上一个 next 的值。
这里先定义一个 val,将 res.done 不为 true 时的值暂存一下,然后 res.done 为 true 时就 resolve(val) 即可。
-
问题5:没有异常处理
作为一个健全的程序,需要有一个异常处理,在这里,在 .next() 的是 try 一下即可。
1 function run(gen) { 2 return new Promise((resolve, reject) => { 3 let g = gen() 4 let val 5 function _next(value) { 6 7 let res 8 try{ 9 res = g.next(value) 10 }catch(err){ 11 return reject(err) 12 } 13 14 if (res.done) { 15 return resolve(val) 16 } 17 val = res.value 18 Promise.resolve(res.value).then(data => { 19 _next(data) 20 }, err => { 21 g.throw(err) 22 }) 23 } 24 _next() 25 }) 26 }
并且在包裹成 Promise 那也处理,避免 res.value 出错,出错了直接调用 g.throw 抛出异常。
最后在返回值那捕获异常即可。如下:
1 run(function*(){ 2 yield aaaa 3 }).then((e) => { 4 console.log(e) 5 }).catch(err => { 6 console.log(err) 7 })
上面代码中的 Generator 的 yield 一个 aaaa 变量,而 aaaa 变量明显未定义,所以肯定会报错,并且在 run 返回的 Promise 中捕获了该异常,打印出:ReferenceError: aaaa is not defined。
-
await/async 与 */yield
上面的一系列实现,就相当于实现了 await/async,都可以同步执行,结构也类似:
1 async function fn () { 2 3 } 4 5 // 等同于 6 7 function fn () { 8 return run(function*(){ 9 10 }) 11 }
对于 Generator 来说,多了一层函数的包裹。
1 async function fn () { 2 await Promise.resolve(1) 3 } 4 5 // 等同于 6 7 function fn () { 8 return run(function*(){ 9 yield Promise.resolve(1) 10 }) 11 }
async 相当于 * ,而 await 相当于 yield。
只不过在使用 Generator 时,需要一个自动运行函数,而这个自动运行函数需要用户自己提供,Async 则会自动运行。
两者皆返回 Promise。
参考
手写generator核心原理,再也不怕面试官问我generator原理_前端阳光的博客-CSDN博客_generator原理
【JS】548- Promise/async/Generator实现原理解析 - 王平安 - 博客园 (cnblogs.com)