我们都知道,Event Loop即事件循环,是指浏览器或Node的一种解决JavaScript单线程运行时不会阻塞的一种机制,即我们经常使用异步的原理。
而浏览器中的事件循环与Node.js事件循环机制各不相同。
1. 浏览器端Event Loop
1.1 我们为什么需要Event Loop
在介绍Event Loop原理前,我们必须得要搞清楚为什么需要Event Loop?JavaScript是一门单线程语言,当一个函数执行时,它不会被抢占,只有在它运行完毕后才会去运行任何其他的代码,才能去修改这个函数操作的数据。然而当一个消息需要太长时间才能处理完毕时,Web应用程序就无法处理与用户的交互,例如点击或滚动;又或者是浏览新闻时图片加载过慢,网页不可能一直卡着直到图片完全显示出来。为了解决这些问题,我们将任务分为了两类:同步任务与异步任务。
在异步任务中,同一时间按照代码顺序等待执行。通常我们在遇到异步代码时将其挂起并略过,等待同步代码执行完毕后按照特定顺序执行异步代码。接下来我们深入了解一下。
1.2 JavaScript的运行模型

如图所示,当遇到同步代码时会立即执行;而遇到异步代码时将其加入到工作线程中,等异步代码所需时间到达后将其加入到任务队列当中。当执行栈为空时,被处理的消息被移除队列,并作为输入参数来调用与之关联的函数。函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息。
说了这么多,还不如来一个实例更容易理解:
const s = new Date().getSeconds();setTimeout(function() {// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");}, 500);while(true) {if(new Date().getSeconds() - s >= 2) {console.log("looped for 2 seconds");break;}}
输出结果如下:
:::info
looped for 2 seconds
Ran after 2 seconds
:::
上述的代码执行流程如下:
- 先将异步代码挂起,放在工作线程中;
- 然后在运行同步代码
while语句,等待间隔2秒; - 在500ms时,将异步任务加入任务队列中;到2秒时执行
console.log("looped for 2 seconds"); - 此时执行栈为空,任务队列推送任务
setTimeout给执行栈,开始执行。
相信通过上述的代码,大家对Event Loop有了基本的了解。同时我们从代码中也可以发现,函数**setTimeout**中的时间值参数它代表的是消息被实际加入队列的最小延迟时间,而不是确切的等待时间。如果任务队列中没有其他消息且栈为空,在该延迟时间过后消息会被立马处理;但如果有其他消息,setTimeout消息必须等待其他消息处理完。
1.3 宏任务与微任务
在ECMA标准升级后,将异步任务分为了微任务和宏任务。
宏任务:是JS中原始的异步任务,包括setTimeout、setInterval、AJAX等,在代码执行环境中按照同步代码的顺序,逐个进入工作线程挂起,再按照异步任务到达的时间节点,逐个进入异步任务队列,最终按照队列中的顺序进入函数执行栈执行。
微任务:每一个宏任务执行前,程序先检测是否有当次事件循环未执行的微任务,优先清空本次的微任务后,在执行下一个宏任务。每个宏任务内部可注册当次任务的微任务队列,在下一个宏任务执行前运行,微任务也是按照进入队列的顺序执行。包括Promise、MutationObserve等。
让我们先来看看两者的执行顺序:
执行栈的执行完同步任务后,判断执行栈是否为空。若执行栈为空,则去检查微任务队列是否为空,如果为空则去执行宏任务,否则一次性执行完所有的微任务。
当宏任务执行完毕后,检查是否存在微任务队列是否为空。如果为空则继续执行下一个宏任务;否则去执行完所有的微任务,然后再去执行宏任务,如此循环。
举个例子:
console.log('script start')async function async1() {await async2()console.log('async1 end')}async function async2() {console.log('async2 end')}async1()setTimeout(function() {console.log('setTimeout')}, 0)new Promise(resolve => {console.log('Promise')resolve()}).then(function() {console.log('promise1')}).then(function() {console.log('promise2')})console.log('script end')
输出结果如下:
:::info
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
:::
在讲解之前,我们必须得要理解async/await在底层是转换成promise和then的回调函数。当我们使用await,解释器创建一个promise对象,然后把剩下的async函数操作放到then回调函数中。我们需要知道,在同一个上下文当中,总的执行顺序为同步代码 ——> 微任务 ——> 宏任务。
我相信,根据上面的讲述大家应该都能正确理解,这里就不过多讲解了。
2. Node.js环境Event Loop
2.1 执行流程
Node.js的Event Loop分为六个阶段,它们按照顺序反复执行,在每个阶段后面都会运行微任务队列。
- `timers`:执行`setTimeout()` 和 `setInterval()`中到期的callback。- `I/O callbacks`:上一轮循环中有少数的`I/Ocallback`会被延迟到这一轮的这一阶段执行- `idle, prepare`:队列的移动,仅内部使用- `poll`:最为重要的阶段,执行`I/O callback`,在适当的条件下会阻塞在这个阶段- `check`:执行`setImmediate`的callback- `close callbacks`:执行close事件的callback,例如s`ocket.on("close",func)`
2.2 setTimeout/setImmediate
在运行过程中,如果timers阶段执行时创建了setImmediate,则会在此轮循环的check阶段中执行;如果timers阶段创建了setTimeout,此时由于timers已取出完毕,则会进入到下一轮循环。check阶段创建timers任务同理。
来个代码演示一下:
const fs = require('fs');// 此时处在 I/O 周期fs.readFile(__filename, () => {setTimeout(() => {console.log('timeout');}, 0);setImmediate(() => {console.log('immediate');});});
运行结果一定为`setImmediate > setTimeout`, 因为在`I/O`阶段读取文件后进行到了`poll`阶段,然后到`check`阶段,此时会立刻执行`setImmediate`,等到进入`timers`阶段采取执行`setTimeout`。
2.3 Process.nextTick()
process.nextTick()将callback添加到next tick队列。
setTimeout(() => console.log(1));setImmediate(() => console.log(2));Promise.resolve().then(() => console.log(3));process.nextTick(() => console.log(4));// 输出结果:4 3 1 2或者4 3 2 1
从输出结果可以看到,微任务比宏任务先运行,而在Node中process.nextTick比Promise更为优先,所以输出结果4 —> 3;但在Node中没有绝对意义上的0ms,所以setTimeout和setImmediate顺序不固定。
2.4 浏览器与Node执行顺序比较
setTimeout(()=>{console.log('timeout1')Promise.resolve().then(function() {console.log('promise1')})}, 0)setTimeout(()=>{console.log('timeout2')Promise.resolve().then(function() {console.log('promise2')})}, 0)
输出结果如下:
:::info
在浏览器中:timeout1 —> promise1 —> timeout2 —> promise2
在Node环境中: timeout1 —> timeout2 —> promise1 —> promise2
:::
下面来解释一下Node环境中的逻辑:
开始时进入timers阶段,执行timeout1的回调,打印出timeout1后将promise。then()放入微任务队列中,相同步骤执行 timeout2。在timers阶段结束后进入下一个阶段前,执行微任务队列中的所有任务,依次打印出promise1、promise2.
浏览器的执行顺序就不过多讲述了。
3、总结
总体而言,浏览器端与Node.js的执行顺序大有不同。最大的差异在于浏览器端按照同步代码 —>微任务 —>宏任务的顺序执行;而Node.js环境中按照六个阶段顺序执行,且在每个阶段结束后都会执行微任务队列里的所有任务。
本文就写到这里,如有错误,敬请指正!

