事件循环

首先说一下事件循环

浏览器中 JavaScript 的执行流程和 Node.js 中的流程都是基于事件循环

理解事件循环的工作方式对于代码优化很重要,有时对于正确的架构也很重要

事件循环的概念非常简单。它是一个在 JavaScript 引擎等待任务,执行任务和进入休眠状态等待更多任务这几个状态之间转换的无限循环

引擎的一般算法:

  • 当有任务时:从最先进入的任务开始执行
  • 休眠直到出现任务,然后转到第 1

一个任务到来时,引擎可能正处于繁忙状态,那么这个任务就会被排入队列。多个任务组成了一个队列,即所谓的宏任务队列

宏任务 && 微任务

除了上述所讲的宏任务外,还有微任务

微任务仅来自于我们的代码。它们通常是由 promise 创建的:对 .then/catch/finally 处理程序的执行会成为微任务。微任务也被用于 await 的幕后,因为它是 promise 处理的另一种形式

还有一个特殊的函数 queueMicrotask(func),它对 func 进行排队,以在微任务队列中执行

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作

看下面的例子

setTimeout(() => alert('timeout'));

Promise.resolve().then(() => alert('promise'));

alert('code');

上面代码的执行顺序为:

  • code 首先显示,因为它是常规的同步调用
  • promise 第二个出现,因为 then 会通过微任务队列,并在当前代码之后执行
  • timeout 最后显示,因为它是一个宏任务

微任务会在执行任何其他事件处理,或渲染,或执行任何其他宏任务之前完成

比较常见的面试题

下面这段代码的输出顺序是什么

setTimeout(function () {
  console.log('1')
  
  new Promise((resolve) => {
    console.log('2')
    resolve()
  }).then(() => {
    console.log('3')
  })
})

new Promise((resolve) => {
  console.log('6')
  resolve()
}).then(() => {
  setTimeout(function() {
    console.log('7')
  })
  console.log('8')
})

console.log('4')

setTimeout(function() {
  console.log('5')
})

正确的输出顺序为:6 4 8 1 2 3 5 7

再看一道题

下面这段代码的输出顺序是什么

setTimeout(function() {
  console.log(4)
}, 0)

new Promise((resolve) => {
  console.log(1)
  resolve()
}).then(_ => {
  console.log(3)
  Promise.resolve().then(() => {
    console.log(5)
  }).then(() => {
    Promise.resolve().then(() => {
      console.log(6)
    })
  })
})

console.log(2)

正确的输出顺序为:1 2 3 5 6 4