一文让你搞懂async/await存在时的执行顺序

2,017 阅读10分钟

最近在写项目,发现代码中总出现这样的内容,一直百思不得其解,后来同事说这段代码的意义是把后边的任务变成宏任务执行,这我更迷惑了,我发现自己的Promise基础还是很差,因此在摸索了几天后,感觉好像摸索到了一些原理,特此记录,请大佬们批评指正。

await new Promise(resolve => {
        setTimeout(() => {
          resolve();
        }, 0);
      });

前置条件

首先你要弄明白JS事件循环(Event Loop)的基本原理,要明白任务队列(即宏任务)、微任务队列、执行栈(同步任务)的执行顺序,在此不做过多阐述。

案例

案例一

const fn = async () => {
  setTimeout(() => {
    console.log('after 0s');
  }, 0);
  new Promise((resolve) => {
    resolve('我是微任务啦');
  }).then((res) => {
    console.log(res);
  });
  console.log('我是同步任务哎');
  
  await new Promise((resolve) => {
     resolve("我成功喽")
     });
  new Promise((resolve) => {
    resolve('我是await之后的微任务啦');
  }).then((res) => {
    console.log(res);
  });
  console.log('我是await之后的同步任务啦');
};
fn();

这段代码的执行顺序是:

image.png

原因如下,我们用事件循环机制来一步步分析

1、首先,代码从上运行,console.log('after 0s')是宏任务,而且执行时间是0s,因此把回调函数放入到宏任务队列

2、遇到resolve('我是微任务啦')之后,发现是一个Promise.then,放入到微任务队列

3、紧接着遇到console.log('我是同步任务哎'),这是同步任务,直接放入执行栈即可

到目前为止,事件循环队列中的任务如下:

image.png

4、最关键的来喽,这里遇到了await,我们知道await会阻塞后边的代码,要等获取到await后边函数的结果之后才会放行,因此我们可以理解为这段代码目前是绑定在一起

await new Promise((resolve) => {
    resolve('我成功喽');
  });
  new Promise((resolve) => {
    resolve('我是await之后的微任务啦');
  }).then((res) => {
    console.log(res);
  });
  console.log('我是await之后的同步任务啦');

而await是then的语法糖,因此 await new Promise((resolve) => { resolve('我成功喽'); })这段代码其实是一个微任务!!!只不过由于await会暂时阻塞后边的代码而已!!!

而且一旦遇到await阻塞,JS就会把控制权交给我们的全局作用域,因此会从执行栈中的同步任务开始逐个进行执行!!!

因此目前队列中如下:

image.png

大家不要着急,我们开始一步步分析代码的执行:

1)首先肯定执行同步任务,于是打印 我是同步任务哎

2)同步任务其实也属于宏任务,因此会马上执行微任务,并需要把微任务队列清空,因此会打印我是微任务啦,清空以上队列后,此时事件循环中的任务如下:

image.png

3)此时会继续第二个微任务,也就是await new Promise(()=>{resolve('我成功喽')})

请注意,由于new Promise本身是一个同步事件,里面只有一行代码resolve('我成功喽'),因此该Promise的状态会立马变成 resolved,那么await会开始执行,执行完后就不会阻塞后边的代码了,此时我们再继续把代码放入任务队列中:

首先遇到了微任务 ,因此放入微任务队列

 new Promise((resolve) => {
    resolve('我是await之后的微任务啦');
  }).then((res) => {
    console.log(res);
  });

然后遇到了console.log('我是await之后的同步任务啦'),放入同步任务中 因此现在的事件循环内容如下:

image.png

那么到这里,是不是你已经知道如何运行下面的代码啦。 当执行完await后,由于await之后的代码不再阻塞,所以会执行同步任务 我是await之后的同步任务啦,然后执行微任务 我是await之后的微任务啦,最后会执行宏任务打印出 after0s

至此,所有代码执行完毕。

注意:这里在以前的讨论中,不同的浏览器会有不同的结果,有的打印顺序是:我是await之后的微任务啦==> console.log('我是await之后的同步任务啦');但是有的是console.log('我是await之后的同步任务啦')==>我是await之后的微任务啦。 这个讨论大家可以参考我文末的参考文献,这里不多赘述。

案例二

当我们讨论完案例一,大家对await的原理我想已经了解的差不多了,那么言归正传,回到文章开头提到的问题,为什么要用这段代码。

image.png

这里我举一个比较简单的例子,来快速说清楚,因为案例一已经足够繁琐了。 注:这里有个案例一没有提到的地方,就是await确实会堵塞代码,但是如果await右边的Promise中有同步任务,那么是不会被堵塞的,可以看下边这个案例

//注:在vue中,页面是在执行所有的同步代码执行完之后才能得到渲染。
const fn = async () => {
  setTimeout(() => {
    console.log('这是一个异步操作,模拟页面渲染');
  }, 0);
  
  console.log('我是同步任务哎');
  await new Promise((resolve) => {
   console.log('我是await右边Promise中的同步任务');
    setTimeout(() => {
      resolve('我成功喽');
    }, 0);
  });
  console.log('我是await之后的同步任务啦');
};
fn();

首先公布答案:

image.png

我们看到 我是await之后的同步任务啦 是最后才执行,这样可以让页面得到渲染之后再执行await之后的代码,那么这个场景会应用到哪里呢?

注:在vue中,页面是在执行完所有的同步代码才去渲染页面,假设该案例中没有await new Promise这段代码,那么页面肯定是从上到下执行完 console.log('我是await之后的同步任务啦') 才进行渲染页面,但是如果我想让页面渲染之后,再执行 console.log('我是await之后的同步任务啦') 代码,该怎么办呢?

解决办法就是如下图片所述:我用await new Promise,里面包着定时器,定时器是宏任务,即异步任务,且会阻塞下面的代码,因此当代码执行到await后,就会认为同步代码已经执行结束了,因此会清除异步任务里的代码,而异步任务里也包含了new Promise中的定时器,当定时器执行完毕,Promise的状态就发生改变了,因此就不再阻塞下面的代码,就会继续执行了

image.png

废话少说,我们开始执行代码

1)首先,代码遇到定时器,会放入宏任务队列

2)然后遇到同步任务 console.log('我是同步任务哎') 以及new Promise中的 console.log('我是await右边Promise中的同步任务') ,会放入到执行栈中

3)然后遇到await,好!!!就此打住!!!因为案例一中我们说了,当遇到await,会放入到微任务队列,且会阻塞代码,因此下面的代码我们先不看,目前的事件循环事件如下:

image.png

那么肯定先打印 我是同步任务哎我是await右边Promise中的同步任务 ,其次进入到微任务,这里我们发现一个大问题,那就是,Promise中是一个定时器,而定时器是宏任务,因此会把定时器放入到宏任务中,即如下:

image.png

那此时微任务里的await被阻塞了,微任务就没有任务了,因此会去宏任务执行打印 这是一个异步操作,模拟页面渲染 ,执行完之后会继续执行 resolve('我成功喽'),一旦Promise状态发生改变,await就会执行,即此时会跳到微任务队列执行await,因此代码也不会阻塞,此时会把 console.log('我是await之后的同步任务啦') 拿到执行栈中,即:

image.png

最后会执行打印 我是await之后的同步任务啦

至此,代码结束。

案例三(验证案例二)

我们现在明白了,原来案例二中所讨论的代码其实和nextTick原理是一样的,下面我们做个验证

我们都知道nextTick的原理,比如有个输入框,最开始隐藏,当点击按钮的时候,让他显示并且聚焦,如果不用nextTick就会报错,因为页面渲染是在一个tick中完成的,因此我们常用nextTick来解决

<template>
 <el-button @click="showInput">点击我使得input框显示</el-button>
    <el-input v-if="isShow" ref="input"></el-input>
</template>
<script lang="ts" setup>
const input = ref();
const isShow = ref(false);
 const showInput=()=>{
   isShow.value=true
  nextTick(()=>{
    input.value.focus()
  })
}
</script>

上面的nextTick很经典,大家应该都看过,下面我们用另一种方法来写,也是OK的

<template>
 <el-button @click="showInput">点击我使得input框显示</el-button>
    <el-input v-if="isShow" ref="input"></el-input>
</template>
<script lang="ts" setup>
const input = ref();
const showInput = async () => {
  isShow.value = true;
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('成功了');
    }, 0);
  });
  input.value.focus();
};
</script>

所以,这下你应该彻底搞懂了async、await存在时的代码执行顺序了吧!!!

拓展

最近突然遇到一个问题,摸索了半天之后发现其实还是和async、await有关,特此记录下来。 我们知道在vue中父子组件的创建顺序为:父组件先创建,然后子组件创建;子组件先挂载,然后父组件挂载,即“父beforeCreate-> 父create -> 子beforeCreate-> 子created -> 子mounted -> 父mounted”。 那显而易见,如果在父组件中通过props传递一个值给子组件,且在父组件的mounted中改变这个值,在子组件中的mounted打印这个props值,那么子组件打印的值肯定是旧值,而不是新值。

例:

父组件
<template>
    <Son :name="name"></Son>
</template>

<script lang="ts" setup>
const name=ref('刘德华')
onMounted(()=>{
  name.value='张学友'
  })
</script>

子组件
<script lang="ts" setup>
const props = withDefaults(defineProps<{
name:string
}>(), {});
onMounted(()=>{
  console.log('这是父组件传递过来的props:',props.name); 
})
</script>

image.png

但是如果我们稍作改动,就可以让子组件收到父组件的最新值(当然一般情况肯定用watch就行,这里只是为了把async、await讲明白)

我们在子组件的mounted中加一个await

onMounted(async()=>{
  await Promise.resolve('成功')
  console.log('这是父组件传递过来的props:',props.name); 
})

image.png

神奇吗?其实原理如下:

  1. 正常情况下,由于子组件比父组件先挂载,因此子组件mounted中的回调函数会先于父组件进入执行栈;
  2. 但是子组件中遇到了await,因此是异步任务,会进入微任务执行队列并阻塞代码,此时会把控制权交给全局作用域,从而会先执行执行栈中的同步任务(父组件中mounted中的回调函数),执行完后才执行子组件的回调函数,
  3. 因此逻辑就是我用await强行阻塞了子组件的mounted,使得他们顺序颠倒了!

总结

1)当遇到await,会先暂停await右边非同步代码及后边代码的执行,此时会把控制权交给全局作用域,直到Promise的状态发生改变后,才会继续执行await以及后边的任务

2)await本质是then的语法糖,其实是个微任务

3)在await new Promise中如果包含一个定时器,定时器的回调函数中写resolve()或者reject(),那么这个定时器是个宏任务,会在宏任务队列排队完成后,再改变Promise的状态,然后await才能执行,再取消阻塞

注:此文参考文献来自于以下文章,非常好的帖子,大家可以去欣赏学习。

令人费解的 async/await 执行顺序 - 掘金 (juejin.cn) juejin.cn/post/684490…

「前端面试题系列1」今日头条 面试题和思路解析 - 掘金 (juejin.cn) juejin.cn/post/684490…

javascript - 8张图帮你一步步看清 async/await 和 promise 的执行顺序 - 前端进阶 - SegmentFault 思否 segmentfault.com/a/119000001… ————————————————

版权声明:本文为CSDN博主「想干到35岁的程序猿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/weixin_4897…