IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    「过程详解」async await综合题

    wuwhs发表于 2023-03-29 13:55:19
    love 0

    前言

    如果你之前跟我一样一直对async await熟悉又陌生的话(熟悉是可能每天都在用,陌生是针对一些组合题又丈二和尚摸不着头脑),不妨可以边看边练,总结规律,相信会逐渐清晰并有所得。本文对每个案例都详细描述了代码的执行流程,如有不妥欢迎指正。

    async 函数return值

    async函数默认会返回一个Promise对象,不管最后函数有没有return值。但是针对具体的返回值情况,实际上表现会有所不同,下面分别看看。

    return值为普通值

    这里的普通值是指基础类型值(Number、String和Boolean等)和非thenable和非Promise的值

    async function foo() {
      return 'foo'
    }
    foo().then(() => console.log('a'))
    Promise.resolve()
      .then(() => console.log('b'))
      .then(() => console.log('c'))
    // 输出结果:a b c

    很简单,不出意外输出a b c,也就是async函数在执行完成后是没有等待的。

    1. foo()执行完成没有等待,遇到then将console.log('a')放入微任务队列;
    2. 继续往下执行Promise.resolve(),遇到then将console.log('b')入队,当前同步任务全部执行完成;
    3. 开始执行微任务队列,首先取出并执行console.log('a')输出a;
    4. 然后取出并执行console.log('b')输出b,此时遇到then将console.log('c')入队;
    5. 最后取出并执行console.log('c')输出c,至此微任务队列清空,代码执行结束;

      return值为thenable

      所谓值为thenable是指定义了then方法的对象,可以是一个字面对象,也可以是一个Class实例。

      class Bar {
       then(resolve) {
       resolve()
       console.log('then')
        }
      }
      
      async function foo() {
        // return new Bar()
        return {
       then(resolve, reject) {
         resolve()
         console.log('then')
       }
        }
      }
      foo().then(() => console.log('a'))
      Promise.resolve()
        .then(() => console.log('b'))
        .then(() => console.log('c'))
      // 输出结果:then b a c

      怎么顺序不一样了呢?

      如果async函数的返回值是一个thenable,等同于生成一个Promise,在foo函数执行完成,并且Promise状态变更(resolve或者reject)后,还要等1个then的时长
    6. foo()返回thenable值,执行then方法,Promise状态变更,执行console.log('then')输出then,等待1个then时长;
    7. 继续往下执行Promise.resolve(),遇到then将console.log('b')放入微任务队列,当前同步任务执行完成;
    8. 开始执行微任务队列,首先取出并执行console.log('b')输出b,当前微任务队列清空;
    9. 此时步骤1等待时长到期,遇到then将console.log('a')放入队列,取出执行输出a;
    10. 继续步骤3遇到then将console.log('c')放入队列,取出执行输出c,至此微任务队列清空,代码执行结束;

    这里如果foo函数返回的thenable方法的状态没有变更,则后面的foo().then将永远不会执行。

    async function foo() {
      return {
        then(resolve, reject) {
          console.log('then')
        }
      }
    }
    foo().then(() => console.log('a'))
    Promise.resolve()
      .then(() => console.log('b'))
      .then(() => console.log('c'))
    // 输出结果:then b c

    return 值为Promise

    return后面的值是Promise,比如 new Promise(resolve=>resolve())和Promise.resolve。

    async function foo() {
      return Promise.resolve('foo')
    }
    foo().then(() => console.log('a'))
    Promise.resolve()
      .then(() => console.log('b'))
      .then(() => console.log('c'))
      .then(() => console.log('d'))
    
    // 输出结果:b c a d

    明显可以看出async函数执行完后延迟了2个then时长。

    1. foo()返回Promise值,Promise状态变更,等待2个then时长;
    2. 继续往下执行Promise.resolve(),遇到then将console.log('b')放入微任务队列,当前同步任务执行完成;
    3. 开始执行微任务队列,首先取出并执行console.log('b')输出b,当前微任务队列清空;
    4. 遇到then将console.log('c')放入队列,取出执行输出c;
    5. 此时步骤1等待时长到期,遇到then将console.log('a')放入队列,取出执行输出a;
    6. 继续步骤4遇到then将console.log('d')放入队列,取出执行输出d,至此微任务队列清空,代码执行结束;

    综合上述表现可以总结出如下规律

    await 表达式值

    既然async函数返回值对代码执行顺序有影响,那么await后面的表达式值是否也有影响呢?下面同样分为上述三种场景进行实验分析

    await值为普通值

    async function foo() {
      await 'foo'
      console.log('a')
    }
    foo().then(() => console.log('b'))
    Promise.resolve()
      .then(() => console.log('c'))
      .then(() => console.log('d'))
    // 输出结果:a c b d

    可以判断,await后面的表达式值如果是普通值,无须等待then时长。那么,为什么b会在c后面输出呢?

    在await表达式有执行结果后,await下一行到函数结束部分代码codex可以看做放置到微任务队列中,等同于Promise.resolve(await xxx).then(()=>codex),这里是伪代码,await在时间顺序上等效于Promise.prototype.then。
    1. await 'foo'执行完成后,console.log('a')被添加到微任务队列;
    2. 继续往下执行同步任务Promise.resolve(),遇到then将console.log(c)添加到微任务队列,当前同步任务执行完成;
    3. 然后执行微任务队列中任务,取出并执行console.log('a')输出a;
    4. 此时foo函数执行完成,遇到then将console.log('b')入队;
    5. 继续执行微任务队列中console.log('c')输出c,此时遇到then将console.log('d')入队;
    6. 最后依次执行取出剩余微任务,执行并输出b和d,至此微任务队列清空,代码执行结束;

      await值为thenable

      async function foo() {
        await {
       then(resolve) {
         resolve()
         console.log('then')
       }
        }
        console.log('a')
      }
      foo().then(() => console.log('b'))
      Promise.resolve()
        .then(() => console.log('c'))
        .then(() => console.log('d'))
        .then(() => console.log('e'))
      // 输出结果 then c a d b e

      await后面表达式值如果是thenable,需要等待1个then时长,才会去执行后续代码。

    7. foo()执行await是一个thenable,Promise状态变更,执行同步代码console.log('then'),输出then,此时等待1个then时长;
    8. 继续往下执行同步任务Promise.resolve(),遇到then将console.log('c')加入到微任务队列,当前同步任务执行完成;
    9. 开始执行微任务队列,取出并执行console.log('c'),输出c,微任务队列清空;
    10. 此时步骤1等待时长到期,将await后续代码console.log('a')入队;
    11. 继续步骤3,遇到then将console.log('d')入队,然后依次取出console.log('a')和console.log('d')并执行,输出a和d;
    12. 执行完console.log('d')遇到then将console.log('e')放入队列,取出执行,输出e;

    确实有点绕,我们将1个then等待时长看做是下一个微任务从入队到执行完成出队的时间就好。比如这里c任务执行完成,下一个任务d正准备进入被a插了队。

    await值为Promise

    async function foo() {
      await Promise.resolve('foo')
      console.log('a')
    }
    foo().then(() => console.log('b'))
    Promise.resolve()
      .then(() => console.log('c'))
      .then(() => console.log('d'))
      .then(() => console.log('e'))
    // 输出结果 a c b d e

    await后面表达式如果是Promise,和普通值的结果是一样,无须等待then时长。
    image.png
    为什么不和return为Promise的情景一样是2次呢?原来这是nodejs在后期版本优化后的结果:移除了2个微任务,1个throwaway promise,具体原因可以查看「译」更快的 async 函数和 promises。
    。
    image.png
    对于早期版本(node 11及以前),输出的结果是c d a e b,需要等待2个then等待时长。
    image.png

    1. foo()执行await是一个Promise,Promise状态变更,此时等待2个then时长;
    2. 继续往下执行同步任务Promise.resolve(),遇到then将console.log('c')加入到微任务队列,当前同步任务执行完成;
    3. 开始执行微任务队列,取出并执行console.log('c'),输出c,微任务队列清空;
    4. 遇到then将console.log('d')入队,去除并执行,输出d,微任务队列清空;
    5. 此时步骤1等待时长到期,将await后续代码console.log('a')入队;
    6. 继续步骤4,遇到then将console.log('e')入队,然后依次取出console.log('a')和console.log('e')并执行,输出a和e;
    7. 执行完console.log('a')遇到then将console.log('b')放入队列,取出执行,输出b;

    综合await表达式值的结果,我们可以总结

    综合async await

    以上我们仅仅从async的return值和await表达式值单一视角来看,下面综合他们两个来分析(统一在node 12+环境)。

    await一个普通函数

    首先,await是一个普通函数(非async函数)

    function baz() {
      // console.log('baz')
      // return 'baz'
    
      // return {
      //   then(resolve) {
      //     console.log('baz')
      //     resolve()
      //   }
      // }
    
      return new Promise((resolve) => {
        console.log('baz')
        resolve()
      })
    }
    
    async function foo() {
      await baz()
      console.log('a')
    }
    foo().then(() => console.log('b'))
    Promise.resolve()
      .then(() => console.log('c'))
      .then(() => console.log('d'))
      .then(() => console.log('e'))
    // await baz函数return是普通值 输出结果是baz a c b d e
    // await baz函数return是thenable 输出结果是 baz c a d b e
    // await baz函数return是Promise 输出结果 baz a c b d e

    与直接await表达式值输出一致。

    • baz函数return是普通值,不等待then时长;
    • baz函数return是thenable,等待1个then时长;
    • baz函数return是Promise,不等待then时长;

      await一个async函数

      然后将baz函数改成async

      async function baz() {
      // console.log('baz')
      // return 'baz'
      
      // return {
      //   then(resolve) {
      //     console.log('baz')
      //     resolve()
      //   }
      // }
      
      return new Promise((resolve) => {
        console.log('baz')
        resolve()
      })
      }
      
      async function foo() {
      await baz()
      console.log('a')
      }
      foo().then(() => console.log('b'))
      Promise.resolve()
      .then(() => console.log('c'))
      .then(() => console.log('d'))
      .then(() => console.log('e'))
      // await baz函数return是普通值 输出结果是baz a c b d e
      // await baz函数return是thenable 输出结果是 baz c a d b e
      // await baz函数return是Promise 输出结果 baz c d a e b
      // node12 以下版本 await baz函数return是Promise 输出结果 baz c d e a b

      从中我们可以发现:await async函数的等待时长与async baz函数的return值等待时长保持一致。

    • async baz函数return是普通值,不等待then时长;
    • async baz函数return是thenable,等待1个then时长;
    • async baz函数return是Promise,等待2个then时长,但是在node12以下版本会等待3个then时长;

    综合async、await、Promise、then和setTimeout

    下面我们综合async、await、Promise、then和setTimeout来看一道题目

    const async1 = async () => {
      console.log('async1')
      setTimeout(() => {
        console.log('timer1')
      }, 2000)
      await new Promise((resolve) => {
        console.log('promise1')
        resolve()
      })
      console.log('async1 end')
      return Promise.resolve('async1 success')
    }
    console.log('script start')
    async1().then((res) => console.log(res))
    console.log('script end')
    Promise.resolve(1)
      .then(Promise.resolve(2))
      .catch(3)
      .then((res) => console.log(res))
    setTimeout(() => {
      console.log('timer2')
    }, 1000)

    思考几分钟,输出结果

    // script start
    // async1
    // promise1
    // script end
    // async1 end
    // 1
    // async1 success
    // timer2
    // timer1
    1. 执行同步任务输出script start和async1,遇到setTimeout放入宏任务队列;
    2. 继续往下执行await表达式,执行new Promise输出promise1,Promise状态变更,不等待then时长,将后续代码添加到微任务队列;
    3. 继续往下执行输出script end,执行Promise.resolve(1)遇到then将Promise.resolve(2)放入微任务队列;
    4. 再往下执行遇到setTimeout放入宏任务队列,至此同步任务执行完毕;
    5. 开始执行微任务队列,取出并执行步骤2的后续代码输出async1 end,返回一个已变更的Promise对象,需要等待2个then时长;
    6. 继续取出微任务Promise.resolve(2)并执行,状态为resolved后面走then;
    7. 遇到then将(res) => console.log(res)放入微任务队列,然后取出并执行输出1,注意:then中是非函数表达式会执行,默认返回的是上一个Promise的值,then(Promise.resolve(2))会透传上一层的1;
    8. 此时步骤5等待时长到期,将(res) => console.log(res)放入微任务队列,然后取出并执行输出async1 success;
    9. 最后2个定时器分别到期,输出timer2和timer1;

    如果对这个案例再稍作改造

    const async1 = async () => {
      console.log('async1')
      setTimeout(() => {
        console.log('timer1')
      }, 2000)
      await new Promise((resolve) => {
        console.log('promise1')
      })
      console.log('async1 end')
      return 'async1 success'
    }
    console.log('script start')
    async1().then((res) => console.log(res))
    console.log('script end')
    Promise.resolve(1)
      .then(2)
      .then(Promise.resolve(3))
      .catch(4)
      .then((res) => console.log(res))
    setTimeout(() => {
      console.log('timer2')
    }, 1000)
    // 输出结果:
    // script start
    // async1
    // promise1
    // script end
    // 1
    // timer2
    // timer1

    具体过程就不一一列举了,从输出结果可以发现:如果await表达式的Promise的状态没有变更,以下代码以及后面的then永远都不会执行。then的执行时机是在前面函数执行完成并且Promise状态变更以后才会被添加到微任务队列中等待执行。

    总结

    通过以上就是基本async await的使用场景,以及综合then、Promise和setTimeout的混合使用,大致可以总结如下几条规律:

    • async函数的return值为thenable会等待1个then时长,值为Promise会等待2个时长;
    • await表达式值为thenable会等待1个then时长,值为Promise在node12+不等待then时长,低版本node等待2个then时长;
    • await一个async函数,async函数的return值为thenable会等待1个then时长,值为Promise在node12+会等待2个then时长,在低版本node等待3个then时长;
    • 如果then中是非函数,表达式本身会执行,默认返回的是上一个Promise的值,也就是透传上一个Promise结果;
    • 如果await表达式的Promise的状态没有变更,以下代码以及后面的then永远都不会执行;

    以上案例均通过实验运行得出,流程如有解释错误,欢迎指正,完~



沪ICP备19023445号-2号
友情链接