背景

一天惬意的下午。敌人给我分享了一道头条面试题,如下:

async function async1(){    console.log('async1 start')    await async2()    console.log('async1 end')}async function async2(){    console.log('async2')}console.log('script start')setTimeout(function(){    console.log('setTimeout') },0)  async1();new Promise(function(resolve){    console.log('promise1')    resolve();}).then(function(){    console.log('promise2')})console.log('script end')

这个题目次要是考查对同步工作、异步工作:setTimeout、promise、async/await的执行程序的了解水平。(倡议大家也本人先做一下o)

过后因为我对async、await理解的不是很分明,答案错的千奇百怪 :(),就不记录了,而后我就去看文章理了理思路。当初写在上面以供日后参考。

js事件轮询的一些概念

这里首先须要明确几个概念:同步工作、异步工作、工作队列、microtask、macrotask

同步工作
指的是,在主线程上排队执行的工作,只有前一个工作执行结束,能力执行后一个工作;

异步工作
指的是,不进入主线程、而进入"工作队列"(task queue)的工作,期待同步工作执行结束之后,轮询执行异步工作队列中的工作

macrotask 即宏工作,宏工作队列等同于咱们常说的工作队列,macrotask是由宿主环境散发的异步工作,事件轮询的时候总是一个一个工作队列去查看执行的,"工作队列"是一个先进先出的数据结构,排在后面的事件,优先被主线程读取。

microtask 即微工作,是由js引擎散发的工作,总是增加到当前任务队列开端执行。另外在解决microtask期间,如果有新增加的microtasks,也会被增加到队列的开端并执行。留神与setTimeout(fn,0)的区别:

setTimeOut(fn(),0)
指定某个工作在主线程最早可得的闲暇工夫执行,也就是说,尽可能早得执行。它在"工作队列"的尾部增加一个事件,因而要等到同步工作和"工作队列"现有的事件都解决完,才会失去执行。

总结一下:

task queue、microtask、macrotask

  • An event loop has one or more task queues.(task queue is macrotask queue)
  • Each event loop has a microtask queue.
  • task queue = macrotask queue != microtask queue
  • a task may be pushed into macrotask queue,or microtask queue
  • when a task is pushed into a queue(micro/macro),we mean preparing work is finished,so the task can be executed now.

所以咱们能够失去js执行程序是:

开始 -> 取第一个task queue里的工作执行(能够认为同步工作队列是第一个task queue) -> 取microtask全副工作顺次执行 -> 取下一个task queue里的工作执行 -> 再次取出microtask全副工作执行 -> … 这样周而复始

参考:前端进阶面试题具体解答

常见的一些宏工作和微工作:

macrotask:

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI rendering

microtask:

  • process.nextTick
  • Promises
  • Object.observe
  • MutationObserver

Promise、Async、Await都是一种异步解决方案

Promise是一个构造函数,调用的时候会生成Promise实例。当Promise的状态扭转时会调用then函数中定义的回调函数。咱们都晓得这个回调函数不会立即执行,他是一个微工作会被增加到当前任务队列中的开端,在下一轮工作开始执行之前执行。

async/await成对呈现,async标记的函数会返回一个Promise对象,能够应用then办法增加回调函数。await前面的语句会同步执行。但 await 上面的语句会被当成微工作增加到当前任务队列的开端异步执行。

咱们来看一下答案

不记得题的!持续往下看,舒适的筹备了题目,不必往上翻 :)

async function async1(){    console.log('async1 start')    await async2()    console.log('async1 end')}async function async2(){    console.log('async2')}console.log('script start')setTimeout(function(){    console.log('setTimeout') },0)  async1();new Promise(function(resolve){    console.log('promise1')    resolve();}).then(function(){    console.log('promise2')})console.log('script end')
=node10版本是这个后果: script start -> async1 start -> async2 -> promise1 -> script end -> promise2 -> async1 end -> setTimeout

<node10版本是这个后果: script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout

依照下面写的js执行程序就能够失去正确后果,但最初却又存在两个答案,为什么会呈现两种后果呢?咱们能够看到两种后果中就是async1 end 和 Promise2之间的程序呈现差异,我猜测是因为不同版本的node对await的执行办法不同,导致await上面的代码进入工作队列的工夫点不同。具体参见 如何在V8中优化JavaScript异步编程? 外面的《深刻理解await》

简略了解如下:

async function f(){  await p  console.log(1);}//node.js8及行将推广的规范应该会解析成上面这样function f(){  Promise.resolve(p).then(()=>{    console.log(1)  })}//其余的版本应该会解析成上面的这样function f(){  new Promise(resolve=>{    resolve(p)  }).then(()=>{    console.log(1)  })}

正对下面的这两种差别次要是:

  1. 当Promise.resolve 的参数为 promise 对象时间接返回这个 Promise 对象,then 函数在这个 Promise 对象产生扭转后立即执行。
  2. 旧版的解析 await 时会从新生成一个Promise对象。只管该 promise 确定会 resolve 为 p,但这个过程自身是异步的,也就是当初进入队列的是新 promise 的 resolve 过程,所以该 promise 的 then 不会被立刻调用,而要等到以后队列执行到前述 resolve 过程才会被调用,而后再执行then函数。(上面的练习一下的例子会解说当resolve()参数为promise时会怎么执行)

不必放心这个题没解,假相只有一个。依据 TC39 最近决定,await将间接应用 Promise.resolve() 雷同语义。

最初咱们以最新决定来剖析这个题目的可能的执行过程(在Chrome环境下):

  • 定义函数async1、async2。输入'script start'
  • 将 setTimeout 外面的回调函数(宏工作)增加到下一轮工作队列。因为这段代码后面没有执行任何的异步操作且等待时间为0s。所以回调函数会被立即放到下一轮工作队列的结尾。
  • 执行async1。咱们晓得async函数外面await标记之前的语句和 await 前面的语句是同步执行的。所以这里先后输入"async1 start",’async2 start‘.
  • 这时暂停执行上面的语句,上面的语句被放到以后队列的最初。
  • 继续执行同步工作。
  • 输入 ‘Promise1’。将then外面的函数放在以后队列的最初。
  • 而后输入‘script end’,留神这时只是同步工作执行完了,当前任务队列的工作还没有执行结束,还有两个微工作被增加进来了!队列是先进先出的构造,所以这里先输入 ‘async1 end’ 再输入 ‘Promise2’,这时第一轮工作队列才真算执行完了。
  • 而后执行下一个工作列表的工作。执行setTimeout外面的异步函数。输入‘setTimeout’。

练习一下

stackoverflow上的一道题目

let resolvePromise = new Promise(resolve => {  let resolvedPromise = Promise.resolve()  resolve(resolvedPromise)})resolvePromise.then(() => {  console.log('resolvePromise resolved')})let resolvedPromiseThen = Promise.resolve().then(res => {  console.log('promise1')})resolvedPromiseThen  .then(() => {    console.log('promise2')  })  .then(() => {    console.log('promise3')  })
后果:promise1 -> promise2 -> resolvePromise resolved -> promise3

这道题真的是十分费解了。为什么'resolvePromise resolved'会在第三行才显示呢?和舍友探讨了一早晨无果。

其实这个题目的难点就在于resolve一个Promise对象,js引擎会怎么解决。咱们晓得Promise.resolve()的参数为Promise对象时,会间接返回这个Promise对象。但当resolve()的参数为Promise对象时,状况会有所不同:

resolve(resolvedPromise)//等同于:Promise.resolve().then(() => resolvedPromise.then(resolve));

所以这里第一次执行到这儿的时候:

  • 第一个then函数外面的() => resolvedPromise.then(resolve, reject)为microtask。会被放入当前任务列表的最初
  • 而后是Promise1被放入工作列表的最初。
  • 没有同步操作了开始执行工作列表,这时因为resolvedPromise是一个曾经resolved的Promise间接执行then函数,将then函数中的resole()函数放入以后队列的最初,而后输入Promise1。
  • 将Promise2放入队列的最初。执行resole()
  • 这时的resolvePromise终于变成了一个resolved状态的Promise对象了,将‘resolvePromise resolved’放入当前任务列表的最初。输入Promise2。
  • 将Promise3放到当前任务队列的最初。输入resolvePromise resolved。最初输入Promise3.

完结!这外面的几段代码是比拟重要的,解释了js会依照什么样的形式来执行这些新个性。

最初如果有误,欢送斧正。