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)

  Generator 函数的异步应用 - ECMAScript 6入门 (ruanyifeng.com)

posted @ 2021-06-13 21:10  blogCblog  阅读(60)  评论(0编辑  收藏  举报