[关闭]
@gyyin 2020-04-03T23:23:15.000000Z 字数 7019 阅读 356

深入理解 generator 和 async

慕课专栏


1. 前言

上篇文章,我们介绍过了最常见的两种解决异步的方式 —— 回调函数和 Promise,这篇文章我们进一步介绍两种终极解决异步的方法 —— generator 和 async/await。

2. generator

generator 是一个状态机,内部封装了状态。generator 返回了一个遍历器对象,可以遍历函数内部的每一个状态。
generator 函数声明的时候,在 function 和函数名之间会有一个星号*用来说明当前是一个 generator 函数,同时在函数体内会有 yield 关键字,用于定义状态。我们通过 next 方法不断地调用。

  1. function* test() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }

image_1dj4mt3si1bjb15r58ef1486h2fm.png-32.3kB

从图上可以看到,每次执行完 next 方法后,会返回一个对象,里面有 value 和 done 两个值。
value 就是 yield 表达式后面的返回值。done 则表示函数是否终止。当执行完所有的 yield 后,最后一次返回的 done 就是 true。
同时,next 也可以接受一个参数,作为上一个 yield 的返回值。

  1. function* test() {
  2. const a = yield 1;
  3. const b = yield a * 2;
  4. return b;
  5. }
  6. const gen = test()
  7. gen.next() // {value: 1, done: false}
  8. gen.next(10) // {value: 20, done: false}
  9. gen.next() // {value: undefined, done: true}

给第二个 next 方法传了参数 10,这个 10 就是第一个 yield 1 执行后返回的结果,赋值给了a,因此第二个 yield 得到的结果是20。

2.1 generator 的异步用法

由于我们可以在外部获得 generator 的执行控制权,也能通过 value 拿到执行后的结果,所以 generator 也可以被用来处理异步。
我们以前面的红绿灯为例:

  1. function* lightGenerator() {
  2. yield green(60);
  3. yield red(60);
  4. yield green(60);
  5. yield red(60);
  6. yield green(60);
  7. yield red(60);
  8. }

在这种场景下看,generator 比 Promise 和回调函数都要更加简洁。但是在调用的时候又会比较繁琐,我们每次都需要手动调用 next 方法,而不是自动执行。

  1. const gen = lightGenerator();
  2. gen.next();
  3. gen.next();
  4. gen.next();
  5. gen.next();
  6. gen.next();
  7. gen.next();

如果是在网络请求中,generator 调用会更加复杂。

  1. function* fetchGenerator(){
  2. const url = 'https://baidu.com'; // 假设请求的是百度
  3. const res = yield fetch(url);
  4. return res;
  5. }

我们在调用 gen 函数获得返回结果的时候,就需要这么做。

  1. const gen = fetchGenerator(),
  2. result = gen.next();
  3. result.value
  4. .then(data => data.json())
  5. .then(data => gen.next(data))

因为 fetch 函数执行后返回的是一个 Promise,所以 result.value 是一个 Promise,需要通过 then 来获取到请求到的数据,再将 data 传给 gen.next,让 yield 后面的代码可以继续正常执行。

2.2 自动执行

这是只有一个请求的情况,如果有多个请求呢?那岂不是要多次调用 then?这样代码的可读性非常差了。

  1. function* fetchGenerator(){
  2. const res1 = yield fetch('https://baidu.com');
  3. const res2 = yield fetch('https://google.com');
  4. const res3 = yield fetch('https://bing.com');
  5. return [res1, res2, res3];
  6. }
  1. const gen = fetchGenerator(),
  2. result = gen.next();
  3. result.value
  4. .then(data => data.json())
  5. .then(data => gen.next(data).value)
  6. .then(data => data.json())
  7. .then(data => gen.next(data).value)
  8. .then(data => data.json())
  9. .then(data => gen.next(data).value)

那么有没有一种方法,不需要我们手动调用 next,可以让 generator 自动执行呢?
你可能会想到,既然可以通过 done 来判断是否执行结束,那么用 while 循环不就行了?

  1. let g = gen(),
  2. res = g.next();
  3. while(!res.done){
  4. console.log(res.value);
  5. res = g.next();
  6. }

这样看起来是可以一下子全部执行了,但对于需要上个请求结束后再发送下个请求的场景,这里是无法保证顺序的。
那么我们是否可以利用 Promise 来保证前一步执行完,才能执行后一步呢?参考上述代码,我们可以用递归来实现。
在每次请求拿到 data 后,将这个 data 通过 next 传给下一次的 yield,这样就实现了自动执行。

  1. function run(gen) {
  2. const g = gen();
  3. function next(data) {
  4. const result = g.next(data);
  5. if (result.done) return;
  6. result.value
  7. .then(data => data.json())
  8. .then(data => next(data))
  9. }
  10. next();
  11. }
  12. run(fetchGenerator);

其实著名的 co 模块也是为了解决这个问题而出现的,这个则是 co 模块的简化版实现。

3. async

在 ES2017 中引入了 async 函数,async 函数是处理异步的终极方案。相比 generator,async 不需要我们一步步调用 next 方法。同时,async 返回了一个 Promise,而不是一个 Iterator。

  1. async function foo(){
  2. const res1 = await fetch('https://baidu.com');
  3. const res2 = await fetch('https://google.com');
  4. const res3 = await fetch('https://bing.com');
  5. }
  6. foo();

我们可以清楚地看到,async 和 generator 写法很像,用 async 关键字代替星号,await 关键字代替 yield。
我们使用 async 来解决上面那个红绿灯的例子(当然了,green 和 red 方法都是用 Promise 来实现的)。

  1. const green = (time) => {
  2. return new Promise(resolve => {
  3. setTimeout(() => {
  4. console.log('green')
  5. resolve()
  6. }, time)
  7. })
  8. }
  9. const red = (time) => {
  10. return new Promise(resolve => {
  11. setTimeout(() => {
  12. console.log('red')
  13. resolve()
  14. }, time)
  15. })
  16. }
  17. async function light() {
  18. await green(60);
  19. await red(60);
  20. await green(60);
  21. await red(60);
  22. await green(60);
  23. await red(60);
  24. }
  25. light();

3.1 异常捕获

从这个例子看着,async 是不是比 generator 方便了很多?除此之外,相对于 Promise,async 在错误捕获方面更加优秀。
由于 Promise 异步错误无法通过 try...catch 来捕获,所以我们一般会用 try...catch 来捕获 Promise 构造函数中的错误,用.catch` 来捕获异步错误,这样实际上非常繁琐。

  1. function test() {
  2. try {
  3. new Promise((resolve) => {
  4. // ...
  5. }).then(() => {
  6. }).catch(() => {
  7. })
  8. } catch (err) {
  9. console.log(err)
  10. }
  11. }

而在 async 里面,捕获错误变得如此简单,只需要用 try...catch 就能够捕获到所有异常。

  1. async function test() {
  2. try {
  3. const res = await fetch('www.baidu.com');
  4. } catch (err) {
  5. console.log(err)
  6. }
  7. }

除此之外,我们还可以让 await 后面的 Promise 直接调用 catch,这样可以避免 `try...catch 处理多个不同错误时导致的问题。

  1. async function test() {
  2. const res = await fetch('www.baidu.com')
  3. .catch(err => {
  4. console.log(err)
  5. })
  6. }

3.2 断点调试

同时,由于 Promise then 的异步缘故,导致打断点的时候,经常会先走后面的代码,再回到 then 里面,这样对于断点调试来说非常不方便。

promise.gif-1025kB

但是在 async 里面,断点调试表现就像同步一样,让调试更加方便。

await.gif-1746.7kB

3.3 注意

在使用 async 的时候,经常会有人滥用 await,导致原本没有依赖关系的两个操作必须按顺序才能执行,比如:

  1. async function test() {
  2. const getUserInfo = await fetchUserInfo();
  3. const getHotelList = await fetchHotelList();
  4. }

可以看到原本没有关联的两个接口 fetchUserInfo 和 fetchHotelList,现在 fetchHotelList 必须要等 fetchUserInfo 接口获取到数据后才开始调用,大大提高了原本要花费的时间。
有两种方式可以解决这个问题。
我们可以改变一下 await 的写法,让两个接口同时调用,再用 await 去等待返回值,这样耗时是请求时间最长的那个。

  1. async function test() {
  2. const getUserInfo = fetchUserInfo();
  3. const getHotelList = fetchHotelList();
  4. await getUserInfo;
  5. await getHotelList;
  6. }

还可以用 Promise.all 来解决这个问题,原理和上面的例子差不多。

  1. function test() {
  2. Promise.all([fetchUserInfo(), fetchHotelList()])
  3. .then(dataArr => {
  4. })
  5. }

3.4 在循环中使用

如果在循环中使用 async/await,那么就要注意一下,在 for 循环和 each/map 中完全是不同的意思。
在 for 循环中表现的是继发,后面的一定会等待前面的返回后才会执行。

  1. async function execute(promises) {
  2. const len = promises.length;
  3. for(let i = 0; i < len; i++) {
  4. await promises[i]()
  5. }
  6. }

而在 forEach 和 map 中则表现为并发。这是为什么呢?

  1. async function execute(promises) {
  2. promises.forEach(async p => {
  3. await p()
  4. })
  5. }

究其原因是 forEach 并不是一个 async 函数,执行 callback 的时候没有用await等待返回,这样自然是同时执行,而非后面的执行需要依赖前面的执行完成。

  1. // 实际上 forEach 并不是一个 async 函数
  2. const forEach = (arr, callback) => {
  3. let len = arr.length;
  4. for(let i = 0; i < len; i++) {
  5. callback(arr[i], i, arr);
  6. }
  7. }

3.5 实现一个 await

await 的原理就是返回了一个 Promise,使用一个函数让 generator 自动迭代,而执行的时机则是由 Promise 来控制,其实原理和上面的 run 方法很相似。
首先定义一下 myAsync 方法,让其返回一个 Promise 对象。

  1. function myAsync(fn) {
  2. return function(...args) {
  3. return new Promise((resolve, reject) => {
  4. const gen = fn.apply(self, args);
  5. gen.next();
  6. })
  7. }
  8. }

这样一个大体的结构就实现了,但现在这个 myAsync 函数只能够让 generator 执行一次,还需要做到让它自动迭代。参考前文我们实现的那个 run 方法,这里考虑使用递归实现。
可以设计一个 next 方法,来递归调用这个 next,在 next 中去执行 generator 的 next 方法。是否执行结束则通过 done 属性来判断。

  1. function next(value) {
  2. try {
  3. let result = gen.next(value);
  4. let value = result.value;
  5. if (result.done) {
  6. return resolve(value);
  7. }
  8. return next(value);
  9. } catch (err) {
  10. reject(err)
  11. return
  12. }
  13. }

next 函数是成功实现了,可是还有一些问题,比如 await 后面是可以跟一个原始类型的值的,会默认用 Promise.resolve 将其包起来。这里还需要将返回值用 Promise.resolve 再进行一次封装。

  1. function next(value) {
  2. try {
  3. let result = gen.next(value);
  4. let value = result.value;
  5. if (result.done) {
  6. return resolve(value);
  7. }
  8. return Promise.resolve(value).then(next);
  9. } catch (err) {
  10. reject(err);
  11. return
  12. }
  13. }

如果你比较细心,还会发现虽然这个 generator 可以自执行了,但还缺少了错误处理,如果在 await 函数后面跟着一个失败的 Promise,该怎么处理呢?
所以我们来修改一下 next 函数,兼容错误处理,加上一个 error 函数来捕获。

  1. function error(err) {
  2. try {
  3. let result = gen.throw(err);
  4. let value = result.value;
  5. } catch (err) {
  6. reject(err);
  7. return
  8. }
  9. }
  10. function next(value) {
  11. try {
  12. let result = gen.next(value);
  13. let value = result.value;
  14. if (result.done) {
  15. return resolve(value);
  16. }
  17. return Promise.resolve(value).then(next, error);
  18. } catch (err) {
  19. reject(err);
  20. return
  21. }
  22. }

最终的完成版如下:

  1. function myAsync(fn) {
  2. return function(...args) {
  3. return new Promise((resolve, reject) => {
  4. const gen = fn.apply(self, args);
  5. function run(gen, resolve, reject, next, error, key, arg) {
  6. try {
  7. let result = gen[key](arg);
  8. let value = result.value;
  9. if (result.done) {
  10. return resolve(value);
  11. }
  12. return Promise.resolve(value).then(next, error);
  13. } catch (err) {
  14. reject(err);
  15. return
  16. }
  17. }
  18. function next(value) {
  19. run(gen, resolve, reject, next, error, 'next', value);
  20. }
  21. function error(err) {
  22. run(gen, resolve, reject, next, error, 'throw', err);
  23. }
  24. next();
  25. })
  26. }
  27. }

找几个例子来测试一下这个 myAsync 函数到底行不行。

  1. const test = myAsync(function *() {
  2. const a = yield 1;
  3. const b = yield Promise.resolve(2);
  4. const c = yield Promise.reject(3);
  5. return [a, b, c]
  6. });
  7. test()
  8. .then(res => console.log('res', res))
  9. .catch(err => console.log('err', err))
  10. // err 3

4. 总结

在平时使用中,Promise 和 async 使用最多,generator 比较不常用。async 在一定程度上可以简化代码,提高可读性和方便调试。但 Promise.allPromise.race 在一些场景下会更加适用。
在未来的一些新特性和提案上,Promise 家族还增加了 Promise.allSettledPromise.any 两个新成员。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注