什么是事件循环#
JavaScript 是单线程非阻塞语言,这里单线程指的是执行程序的主线程是单线程的,而不是程序只有一个线程。
非阻塞是因为执行主程序虽然是单线程的,但是并不会被阻塞,这里得益于它的异步流程。而异步流程的是通过事件循环(Event Loop)机制来实现的。浏览器的事件循环和 Node.js 的事件循环实现过程是不一样的。
浏览器的事件循环#
在浏览器种有两种任务,分别为宏任务(marcoTask)和 微任务(microTask)。
宏任务(marcoTask):script,setTimeout,setInterval,setImmediate,I/O,UI rendering,MessageChannel,postMessage等
微任务(microTask):promise.then,Object.observe,MutationObserver,ajax等
浏览器事件循环是会先执行一个宏任务,接下来有异步代码的话就先执行微任务。
所以浏览器的一次Event loop顺序是这样的:
- 执行同步代码,这属于宏任务
- 执行栈为空,查询是否有微任务需要执行
- 执行所有微任务
- 必要的话渲染 UI
- 然后开始下一轮 Event loop,执行宏任务中的异步代码
代码示例 1:
async function async1() {
console.log("async1 start"); // 2 第二个同步 执行
await async2(); // => 3 执行await
console.log("async1 end"); // 6 异步 第一个微任务 => 进入微任务队列等待 => 执行同步 5 后回来执行
}
async function async2() {
console.log("async2"); // 3 第三个同步 执行
}
console.log("script start"); // 1 第一个同步 执行
setTimeout(function () {
console.log("setTimeout"); // 8 异步 第一个宏任务 => 进入宏任务队列等待 => 执行完所有的微任务后执行
}, 0);
async1(); // => 2 执行函数
new Promise(function (resolve) {
console.log("promise1"); // 4 第四个同步 执行
resolve();
}).then(function () {
console.log("promise2"); // 7 异步 第二个微任务 => 进入微任务队列等待 => 执行 第一个微任务后执行
});
console.log("script end"); // 5 第五个同步 执行代码示例 2:
console.log("start"); // 1 第一个同步 执行
// 4 第一个宏任务 => 执行完所有的微任务后执行
setTimeout(() => {
console.log("timer1"); // 4 宏任务的同步任务
Promise.resolve().then(function () {
console.log("promise1"); // 5 宏任务里面的微任务 (执行完所有同步后清空所有的微任务)
});
}, 0);
// 第二个宏任务
setTimeout(() => {
console.log("timer2"); // 6 宏任务的同步任务
Promise.resolve().then(function () {
console.log("promise2"); // 7 宏任务里面的微任务 (执行完所有同步后清空所有的微任务)
});
}, 0);
Promise.resolve().then(function () {
console.log("promise3"); // 3 第一个微任务
});
console.log("end"); // 2 第二个同步 执行Node.js 的事件循环(v10 版本)#

Node.js 的Event Loop并不分宏任务(macroTask)和微任务(microTask),它没有这个概念。
而是分为 6 个阶段,它们会按照顺序反复运行:
- timer 阶段。这个阶段执行
setTimeout和setInterval预定的回调。 - I/O callbacks 阶段。执行除了
close事件的回调、timers(setTimeout、setInterval等)设定的 回调、setImmediate设定的回调之外的回调。 - idle, prepare 阶段。仅 Node.js 内部使用。
- poll 阶段。取新的 I/O 事件, 适当的条件下 Node.js 将阻塞在这里。
- check 阶段。执行
setImmediate设定的回调。 - close callback 阶段 。执行
close事件的回调。比如socket.on('close', callback)的回调会在这个阶段执行。
重要阶段
- timer 阶段
timers 阶段会执行setTimeout和setInterval回调,并且是由 poll 阶段控制的。 同样,在 Node.js 中定时器指定的时间也不是准确时间,只能是尽快执行。
- poll 阶段
poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情
-
回到 timer 阶段执行回调
-
执行 I/O 回调
并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情
-
如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
-
如果 poll 队列为空时,会有两件事发生
-
如果有
setImmediate回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调 -
如果没有
setImmediate回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
-
当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。
-
check 阶段
process.nextTick:这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 micro task 执行。
代码示例 1:
async function async1() {
console.log("async1 start"); // 2 第二个同步 执行
await async2();
console.log("async1 end"); // 7 第二个 promise
}
async function async2() {
console.log("async2"); // 3 第三个同步 执行
}
console.log("script start"); // 1 第一个同步 执行
setTimeout(function () {
console.log("setTimeout"); // 8 第一个 setTimeout
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1"); // 4 第四个同步 执行
resolve();
}).then(function () {
console.log("promise2"); // 6 第一个 promise
});
console.log("script end"); // 5 第五个同步 执行代码示例 2:
console.log("start"); // 1 第一个同步 执行
// 4 第一个 setTimeout
setTimeout(() => {
console.log("timer1"); // 4 setTimeout 里面的同步任务
Promise.resolve().then(function () {
console.log("promise1"); // 6 第二个 promise (在 setTimeout 里面)
});
}, 0);
// 第二个 setTimeout
setTimeout(() => {
console.log("timer2"); // 5 setTimeout 里面的同步任务
Promise.resolve().then(function () {
console.log("promise2"); // 7 第三个 promise (在 setTimeout 里面)
});
}, 0);
Promise.resolve().then(function () {
console.log("promise3"); // 3 第一个 promise
});
console.log("end"); // 2 第二个同步 执行总结#
主要区别:
在执行 timers 任务setTimeout和setInterval时,Node.js 会一次性执行完全部这类任务。而浏览器只会先执行一次宏任务(或者所有的同步任务),然后立即去执行全部的微任务,如此循环。
备注:在 Node.js v11 以后的版本中,事件循环机制会被调整,输出的结果会和浏览器的保持一致,以后就不用再纠结和浏览器的差异了。