切换主题
事件循环
为什么会有EventLoop
?
由于
JavaScript
是单线程语言(即程序运行时,只有一个线程存在,同一时间只能做一件事),单线程会有线程运行阻塞问题,为了解决这一问题,JavaScript
便采用了事件循环EventLoop
机制。
既然单线程有运行阻塞问题,为什么JavaScript
不设计成多线程呢?
设计之初
JavaScript
作为一门浏览器脚本语言,通常用于操作DOM
,若设计成多线程,则会带来很复杂的同步问题,比如:一个线程要删除某DOM
元素 ,另一个要添加这个DOM
元素,此时的浏览器就不知道该怎么办了!
浏览器事件循环
何为浏览器事件循环?
在解释何为事件循环前,先了解一些基本概念,在JavaScript中,所有的任务都可以分为:
- 同步任务:在主线程上排队立即执行的任务(只有前一个任务执行完毕,才能执行后一个任务);
- 异步任务:不进入主线程,进入事件队列的任务(某个异步任务可以执行了,该任务才会进入主线程执行。
而其中的异步任务又分为宏任务和微任务:
- 常见的宏任务(
macro task
):script
、setTimeout
、setInterval
、setImmediate
(Node.js
)、postMessage
、MessageChannel
、UI事件
、I/O
(Node.js
); - 常见的微任务(
micro task
):await
、Promise.then
、MutaionObserver
、process.nextTick
(Node.js
)。
这里特别说明一下,new Promise
和async
函数(包括await
直接作用的内容,不包括之后的内容)自身都是同步任务,举个例子:
JavaScript
async function kon() {
console.log('同步任务-轻音')
await yui()
// await 之后的内容为微任务,不包括直接作用的 yui()
console.log('我是微任务')
}
async function yui() {
console.log('同步任务-平泽唯')
}
kon()
console.log('同步任务')
// 依次输出:'同步任务-轻音' --> '同步任务-平泽唯' --> '同步任务' --> '我是微任务'
有了以上前置知识,现在我们详细地讲讲何为事件循环机制?
当一个任务(可理解为函数)进入执行栈时:
- 若该任务是同步任务,直接进入主线程:
- 当主线程无其它待执行(排队的)同步任务时,立即执行该任务;
- 当主线程有其它待执行的同步任务时,则排队等待执行。
- 若该任务是异步任务,则将其加入
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]
- 在某个时刻,当主线程所有同步任务执行完毕后,开始读取事件队列
EventQueue
中的回调函数进入主线程(注意队列是FIFO
)执行; - 不断重复
步骤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
事件循环
Node
自11
版本后,事件循环机制和浏览器事件循环机制一样了。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'