Skip to content
本页目录

事件循环

为什么会有EventLoop

由于JavaScript是单线程语言(即程序运行时,只有一个线程存在,同一时间只能做一件事),单线程会有线程运行阻塞问题,为了解决这一问题,JavaScript便采用了事件循环EventLoop机制。

既然单线程有运行阻塞问题,为什么JavaScript不设计成多线程呢?

设计之初JavaScript作为一门浏览器脚本语言,通常用于操作DOM,若设计成多线程,则会带来很复杂的同步问题,比如:一个线程要删除某DOM元素 ,另一个要添加这个DOM元素,此时的浏览器就不知道该怎么办了!

浏览器事件循环

何为浏览器事件循环?

在解释何为事件循环前,先了解一些基本概念,在JavaScript中,所有的任务都可以分为:

  • 同步任务:在主线程上排队立即执行的任务(只有前一个任务执行完毕,才能执行后一个任务);
  • 异步任务:不进入主线程,进入事件队列的任务(某个异步任务可以执行了,该任务才会进入主线程执行。

而其中的异步任务又分为宏任务微任务

  • 常见的宏任务(macro task):scriptsetTimeoutsetIntervalsetImmediateNode.js)、postMessageMessageChannelUI事件I/ONode.js);
  • 常见的微任务(micro task):awaitPromise.thenMutaionObserverprocess.nextTickNode.js)。

这里特别说明一下new Promiseasync函数(包括await直接作用的内容,不包括之后的内容)自身都是同步任务,举个例子:

JavaScript
async function kon() {
  console.log('同步任务-轻音')
  await yui()
  // await 之后的内容为微任务,不包括直接作用的 yui()
  console.log('我是微任务')
}
async function yui() {
  console.log('同步任务-平泽唯')
}
kon()
console.log('同步任务')
// 依次输出:'同步任务-轻音' --> '同步任务-平泽唯' --> '同步任务' --> '我是微任务'

有了以上前置知识,现在我们详细地讲讲何为事件循环机制

当一个任务(可理解为函数)进入执行栈时:

  1. 若该任务是同步任务,直接进入主线程:
    • 当主线程其它待执行(排队的)同步任务时,立即执行该任务;
    • 当主线程其它待执行的同步任务时,则排队等待执行。
  2. 若该任务是异步任务,则将其加入EventTable,当任务被触发时,将对应的回调函数加入事件队列EventQueue
    • 若该任务是任务,则将其回调函数加入任务事件队列末尾;
    • 若该任务是任务,则将其回调函数加入任务事件队列末尾。
    • 特别注意,这里的宏任务事件队列和微任务事件队列其实是一个事件队列,你可以理解为同一级任务的所有微任务排在所有宏任务之前 (即,新的微任务在微任务队列的队尾添加,宏任务同理),举个例子:
    JavaScript
    // String 类型表示 微任务 
    const MicroEventQueue = ['1', '2']
    // Number 类型表示 宏任务
    const MacroEventQueue = [1, 2]
    // 当前 EventQueue 为(注意这里的顺序,非常重要)
    // [...MicroEventQueue, ...MacroEventQueue]
    const EventQueue = ['1', '2', 1, 2]
    // 现在有一新的宏任务 3 要加入,MacroEventQueue.push(3)
    const EventQueue = ['1', '2', 1, 2, 3]
    // 现在又有一新的微任务 '3' 要加入,MicroEventQueue.push('3')
    const EventQueue = ['1', '2', '3', 1, 2, 3]
  3. 在某个时刻,当主线程所有同步任务执行完毕后,开始读取事件队列EventQueue中的回调函数进入主线程(注意队列是FIFO)执行;
  4. 不断重复步骤1步骤3,即所谓的事件循环EventLoop

将上述过程用流程图表示:

null

由以上不难发现,在事件循环机制中,任务处理的优先级顺序依次为:同步任务 => 微任务 => 宏任务

掌握与否

让我们来看一个面试真题,试着写出答案来:

Details
JavaScript
setTimeout(function () {
  console.log('setTimeout')
}, 0)
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
async1()
new Promise(function (resolve) {
  console.log('promise')
  resolve()
}).then(function () {
  console.log('promise then')
})
console.log('script end')

// 'script start'
// 'async1 start'
// 'async2'
// 'promise'
// 'script end'
// 'async1 end'
// 'promise then'
// 'setTimeout'

Node事件循环

Node11版本后,事件循环机制和浏览器事件循环机制一样了。Node多了一些异步任务API,比如:宏任务中的setImmediate、微任务中的process.nextTick等等,需要特别注意的就是这个process.nextTick,它在事件循环中的作用就是插队,插谁的队?微任务队列。

同样用浏览器事件循环机制中的例子来解释:

JavaScript
// Bigint 类型表示 nextTick微任务
const TickMicroEventQueue = [1n]
// String 类型表示 微任务 
const MicroEventQueue = ['1', '2']
// Number 类型表示 宏任务
const MacroEventQueue = [1, 2]
// 当前 EventQueue 为 (注意这里的顺序,非常重要)
// [...TickMicroEventQueue, ...MicroEventQueue, ...MacroEventQueue]
const EventQueue = [1n, '1', '2', 1, 2]
// 现在有一新的宏任务 3 要加入,MacroEventQueue.push(3)
const EventQueue = [1n, '1', '2', 1, 2, 3]
// 现在又有一新的微任务 '3' 要加入,MicroEventQueue.push('3')
const EventQueue = [1n, '1', '2', '3', 1, 2, 3]
// 现在有一新的nextTick微任务 2n 要加入,TickMicroEventQueue.push(2n)
const EventQueue = [1n, 2n, '1', '2', '3', 1, 2, 3]

Node事件循环机制任务处理的优先级顺序依次为:同步任务 => Tick微任务=> 微任务 => 宏任务

掌握与否

让我们来看一个面试真题,试着写出答案来:

Details
JavaScript
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)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick1'));
async1()
process.nextTick(() => console.log('nextTick2'));
new Promise(function (resolve) {
  console.log('promise1')
  resolve();
  console.log('promise2')
}).then(function () {
  console.log('promise3')
})
console.log('script end')

// 'script start'
// 'async1 start'
// 'async2'
// 'promise1'
// 'promise2'
// 'script end'
// 'nextTick1'
// 'nextTick2'
// 'async1 end'
// 'promise3'
// 'setTimeout'
// 'setImmediate'