用转换的思想理解async/await

419 阅读9分钟

images.jpeg

前言

之前对async/await总是一知半解,导致有时候场景比较复杂的时候,自己就蒙了。所以这次积累总结了下。我觉得这次能耐心,安静的看下去是有收获的。如果对你们有帮助的话,还请点赞关注鼓励下,提前感谢了。在说async/await之前,我们首先看看generator。网上经常遇到这句话:async/await是generator的语法糖。这句话怎么理解了?我们一步步看下去就会知道,下面我们先解决与generator相关的几个名词。

迭代器,迭代器对象,生成器,可迭代的对象

  • 迭代器,迭代器对象 我理解的迭代器和迭代器对象其实是一样的,因为迭代器本身就是一个对象,下面就以迭代器进行说明。

    MDN上面解释是:迭代器是任何一个可以通过next方法生成迭代协议(或规则)的对象

    用自己的话概述下就是:

    1:迭代器是一个拥有next方法的对象。

    2:这个next返回一个达到迭代协议或者规则的对象,迭代协议或者规则对象其实就是包含value和done两个字段的对象。

  • 生成器

MDN上明确解释的是:生成器是一个function*函数,调用生成器就可以生成一个迭代器。

  • 可迭代的对象

MDN上解释的是:可迭代的对象是一个具有迭代行为的对象。

什么是可迭代行为,按照我的理解是:

1:这个对象具有[Symbol.iterator]属性,并且值是函数。我们要注意不是字符串,用代码展示下更清楚。

const obj = {
  [Symbol.iterator]: () => {}
}
// 不是下面这种
const obj = {
  "Symbol.iterator": () => {}
}

2:[Symbol.iterator]这个函数执行后会返回一个迭代器。

generator几种写法

为什么要说这个了,主要是因为generator用的比较少,所以有时候在比较慌的情况下,会短路,还是因为没总结,下面全部写一遍。

  • 具名函数
function* init() {}
  • 匿名函数
function* () {}
  • 函数表达式
const init = function* () {}
  • 箭头函数
// 记住 generator 现在还不能用箭头函数写2021/03/24
const a = *() => {};

简单的generator用法

再来说一下generator简单的用法,都是为了说明async/await做铺垫,可以去阮一峰老师的es6课程里面熟悉下。

const init = function*(x) {
    const y = yield (x + 1);
    const z = yield (y / 3);
    return z;
}
a = init(5);
a.next(); // {value: 6, done: false}
a.next(); // {value: NaN,done: false}
a.next(); // {value: NaN,done: true}
const init = function*(x) {
    const y = yield (x + 1);
    const z = yield (y / 3);
    return z;
}
a = init(6);
a.next(6); // {value: 7, done: false}
a.next(6); // {value: 2,done: false}
a.next(6); // {value: 6,done: true}

来一个promise版本的--很重要

const test = function* () {
  const a = yield new Promise((resolve, reject) => {
     setTimeout(() => {
     resolve(3);
}, 2000);
});
return a;
}

const a = test();
console.log(a.next()); // {value: Promise, done: false}
console.log(a.next());

构造一个自执行next函数

上面展示promise版本的generator,主要是为了说明async/await的原理,后面会再说。现在我们要实现一个自执行next函数,因为async/await就是自执行的。我们先实现一个简单的架子。

const exec = g => {
    const {value, done} = g.next();
    if (!done) {
      return  exec(g);
}
}
const value = exec(g);

稍微强一点的版本

上面的实现是有一点不好,主要是为了今后我能知道怎么一步步变化过来的,上面的函数并没有传递每一次的value值,现在我们在改版下。

const g = generator();

const next = value => {
    const {value, done} = g.next(value);
    
     if (done) {
        return value;
     }else {
       next(value)
    }
}

next();

这一次的版本是不是好一点,记住这个函数很重要。

promise版本

下面为了理解asyc/await我们在来一个promise版本

const g = generator();
const next = value => {
   const {value, done} = g.next(value);

    if (!done) {
        // value 是一个promise
        value.then(val => {
        next(val); 
      })
    }
}

开始async/await认知

在说明async/await的之前我要抛出自己的三个认知,如果能帮助你们更好的理解,可以采纳,反之略过。

1:async 相当于返回一个promise对象

2:await 相当于一个yield,并放入generator函数里面

3:await 后面相当于包一层promise对象,并把值放入resolve。下面我用代码简单描述下

const test = async function() {
   const a = await 123;
   const b = 5 + a;
   console.log(b);
}
// 将上面的代码换成我认知的那样
const test = function() {
   //  认知1 async 相当于返回的是一个promise
   return new Promise((resolve, reject) => {
          const generator = function*() {
             // 认知2 await 相当于一个yield,并放入generator函数里面
            // 认知3 await  后面 相当于包一层promise对象,并把值放入resolve
              const a = yield new Promise((resolve, reject) => {
                   resolve(123);
                });
               const b = 5 + a; 
               console.log(b);
             }
          // 自执行函数要加上哈
          const g = generator();
          const next = val => {
              const {value, done} = g.next(val);
              if (done) {
                    resolve(value);
              } else {
                value.then(val => next(val));
             }
          }
            next()
})
}

按照我上面转化之后,其实我理解的async/await就是generator与promise的结合。当然源码肯定不是我这样的。现在我们就可以知道,为什么说async/await是genarator的语法糖了吧。有不对的地方可以指出,一起交流,下面用一个实际点复杂点的例子来验证,巩固下。

工作中的版本

先封装一个请求函数

const getList = async url => {
  return await fetch.get(url);
}

fetch.get请求函数我们用promise模拟下,让其表现的更复杂

// 其实我觉得axios.post().then(), 最终应该还是promise的封装
const request = (url) => {
   return new Promise((resolve, reject) => {
      setTimeout(() => {
         resolve({result: [1, 2, 3], code: 1});
       }, 500); // 模拟请求
});
}
const fetch = {
     // url是虚的不要在意
     get: (url) => {
         // 为什么要用request在封一层,除了可以扩展下,还能让这个例子在复杂下
        // 因为用到了then
         return request(url).then(res => {
               if (res.code === 1) {
                  return {...res, success: true};
              }
         });
      }
}

实际业务代码

class Test extends React.Components {
    async componentDidMount() {
       const data = await getList(url);
       console.log('我是什么时候执行');
       console.log(data);
    }
}

上面的例子componentDidMount是一个async/await,而getList还是一个async/await,并且fetch.get的返回是request(url).then()。那么下面的打印“我是什么时候执行”会一直等到data有值了才会执行么?

答案是肯定的

截屏2021-03-26 下午5.41.33.png

分析

分析1

我们先用async/await去分析,其实前面几个内容很好分析componentDidMount是async/await,遇到了getList继续async/await下去。但是很多人可能在fetch.get这里迷糊了。这个函数不是已经return了么,data应该没有值哇。这么想的话首先是async/await没有深入理解然后就是没有对promise进行深入理解,下面按照我上面的三层认知来进行刨析。

分析2

2-1

按照我们上面的分析,将第一个函数改造下

 async componentDidMount() {
       const data = await getList(url);
       console.log('我是什么时候执行');
       console.log(data);
    }
// 改成如下
const componentDidMount = function() {
   return new Promise((resolve, reject) => {
          const generator = function*() {
              const data = yield new Promise((resolve, reject) => {
                   resolve(getList(url));
                });
               console.log('我是什么时候执行');
               console.log(data);
             }
          // 转化成自执行函数形式,参考上面
          const g = generator();
          const next = val => {
              const {value, done} = g.next(val);
              if (done) {
                    resolve(value);
              } else {
                value.then(val => next(val));
             }
          }
            next()
})
}
  • 我们可以看一下上面改造代码,如果想要console.log('我是什么时候执行')这句执行,是不是就要等到value.then运行,那么我们在看看value是什么?从上面可知value其实就是yield后面的promise对象。
         new Promise((resolve, reject) => {
                   resolve(getList(url));
                });

那么value.then的执行就取决于resolve(getList(url))

  • 再来分析getList(url),这个函数返回什么。getList也是一个async/await。按照我上面认知1,async返回的是一个promise对象,那么就相当resolve(new Promise())

  • 我强烈建议看一下我之前写的一篇关于promise源码这篇文章。看完之后你就会知道,如果一个promise(称作A)resolve里面还是一个promise(称作B),那么A的执行取决于B什么时候执行resolve。

  • 意思就是说console.log('我是什么时候执行'),这句什么时候执行取决于getList的resolve执行时机

2-2

我们来到getList发现其实和componentDidMount一样,我们还是改造下

const getList = async url => {
  return await fetch.get(url);
}
// 改造之后
getList = function() {
   return new Promise((resolve, reject) => {
          const generator = function*() {
             yield new Promise((resolve, reject) => {
                   resolve(fetch.get(url));
                });
         
             }
          // 转化成自执行函数形式,参考上面
          const g = generator();
          const next = val => {
              const {value, done} = g.next(val);
              if (done) {
                    resolve(value);
              } else {
                value.then(val => next(val));
             }
          }
            next()
})
}
  • 这里要说明一点的是getList什么时候resolve,是取决于这行代码if (done) {resolve(value)} ,想要他触发那么必须就要done是true,必然要把第一个yield执行完。

  • 我们看看第一个yield,还是一个promise对象。要想value.then执行必须先执行resolve(fetch.get(url))。有点熟悉了吧。

  • fetch.get 返回如下:request(url).then()。问题的最关键地方来了,这是一个promise么?如果不是没什么好说的,直接返回。如果是一个promise就需要继续分析下去,因为我们的500毫秒的停顿还没开始了。

  • 了解promise源码的人应该知道,then就是返回一个新promise对象。

  • resolve(fetch.get(url))返回又是一个新的promise,那么resolve的执行是不是需要看fetch.get(url)返回的这个promise对象什么执行resolve。换句话说就是看request(url).then()这个promise什么时候resolve。

2-3

再次对promise进行解释。then返回的promise什么执行,其实就是then什么时候执行。放一段我之前写的then的源码。

if (self.info.status === "pending") {
      self.onFulfilledArr.push((data) => {
        setTimeout(() => {
          try {
            let value = data;
            value = onFulfilled(value);

            resolve(value);
          } catch (e) {
            reject(e);
          }
        });
      });

我们会看到,只要then函数(第一个回调)被执行了,那么直接获取到值,然后resolve了。回到request(url).then()这句代码,then什么时候被执行,是不是看request(url)这个promise什么时候resolve,根据代码可以得出他是在500毫秒之后resolve的。可能你现在有点没串起来我下面大概捋一下。

2-4

async/await(compontDidMount)----->需要等到getList()这个promise对象resolve才能继续next(),才会去打印----->而getList()的resolve是要等到fetch.get()或者说request(url).then() resolve了才会执行----->而request(url).then()这个promise的resolve需要等到then函数执行----->然后then函数回调执行需要等到request(url)返回的promise对象resolve----->根据代码可知request(url)返回的promise对象在500毫秒之后resolve----->也就是整个代码由于generator函数会一直等待,直到500毫秒之后resolve了才去next。

总结

看了上面的总结,你可能对async/await有一个新的认识。网上很多文章是深入源码讲解。但是我这一篇文章是将async/await转化成promise + generator进行说明。当然如果你还想知道generator是怎么实现的,可以在网上搜一下。

参考文章

Async / Await / Generator 实现原理