乐趣区

关于javascript:js事件循环与macromicro任务队列前端面试进阶

背景

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

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 会依照什么样的形式来执行这些新个性。

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

退出移动版