事件循环(Event Loop)是让单线程Javascript实现无阻塞I/O操作的关键。
一. 前言
事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。
- 常见的 macro-task 比如:script(整体代码)、setTimeout()、setInterval()、 setImmediate()、 I/O 操作、UI 渲染等。
- 常见的 micro-task 比如: process.nextTick()、new Promise().then()、new MutationObserver()等。
new Promise(callback),Promise构造器是代码同步执行的,所以callback属于‘script’。
浏览器和 Node 环境下,microtask 任务队列的执行时机不同
- 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
- Node 端,microtask 在事件循环的各个阶段之间执行
二. 浏览器中的 Event Loop
当一个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。
三. Node.js中的 Event Loop
- timers: 执行由
setTimeout()
、setInterval()
预定的回调。 - pending callbacks: 执行推迟到下一个循环迭代的I/O回调。
- idle, prepare: 仅供内部使用。但是可以利用空闲阶段做一些事情,比如用requestIdleCallback做react FiberTree的循环构建。
- poll: 检索新的I/O事件;执行与I/O相关的回调(除了close callbacks、timers、
setImmediate()
),node将在此处适当阻塞。当执行poll queue到空时,event loop将会检查timers中的计时器,如果存在到达时间的timers,event loop会回到timers阶段执行那些timers回调。 - check: 执行
setImmediate()
回调。 - close callbacks: 关闭的回调,例如
socket.on('close', ...)
在每次运行事件循环之间,Node.js会检查是否有等待的异步I/O及定时器,如果没有则完全关闭。
四. 理解process.nextTick()
process.nextTick不是Event Loop的一部分。不管当前的event loop处于哪个阶段,nextTickQueue将在当前operation完成后执行,operation被定义为从底层c/c++ 处理者的过渡,处理需要被执行的JavaScript。
那么process.nextTick有什么用?
let bar;
// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }
// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
// since someAsyncApiCall hasn't completed, bar hasn't been assigned any value
console.log('bar', bar); // undefined
});
bar = 1;
通过把callback
放在process.nextTick()
中,script仍然具有运行完成的能力,允许在调用回调之前初始化所有变量,函数等。它还具有不允许event loop继续的优点,可能对于在事件循环被允许前向用户发出错误提示很有帮助。
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
另一个例子:当端口号被传递时,端口立即被绑定。因此listening
回调应该被立即执行,问题是在那个时候on('listening')
回调还没被设置。
const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});
为了解决这个问题,listening
事件是在nextTick()
中排队的,以允许脚本运行完成。这允许用户设置他们想要的任何处理程序。
五. process.nextTick()
vs setImmediate()
process.nextTick()
在相同的‘阶段’立即触发setImmediate()
在事件循环的下一个迭代或’tick’触发
本质上,他俩的名称应该互换。 process.nextTick()
触发的更立即,但这是历史的产物,做交换会让很多已有的库出现问题。建议开发者在所有的情况下都使用setImmediate()
,因为这样更容易推理。
六. 为什么用process.nextTick()
?
允许用户处理错误、清除任何不需要的资源,或者在事件循环继续之前再次尝试请求。
- 允许用户处理错误、清除任何不需要的资源,或者在事件循环继续之前再次尝试请求。
- 有时,必须允许在调用堆栈解除后但在事件循环继续之前运行回调。
运行一个从EventEmitter继承而来的函数构造器,它想在构造器中调用一个事件:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
您不能立即从构造函数发出事件,因为script还没有处理到用户为该事件分配回调的程度。因此,在构造函数本身内,你可以使用process.nextTick()设置一个回调,在构造函数完成后触发事件,它提供了预期的结果:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});