1. event loop
1.1 什么是event-loop
js的标准文档定义如下
https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#event_loop
https://javascript.info/event-loop
html的标准定义
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
nodejs自己实现了一套event-loop,与html/js的都不一样
- https://github.com/nodejs/nodejs.dev/blob/main/src/documentation/0029-node-event-loop/index.md
- https://nodejs.org/uk/docs/guides/event-loop-timers-and-nexttick/
- https://stackoverflow.com/questions/31582672/what-is-the-different-between-javascript-event-loop-and-node-js-event-loop
1.2 nodejs的event-loop
官方文档的event-loop模型如下:
其中的关键节点说明
- timers: this phase executes callbacks scheduled by setTimeout() and setInterval(). 此阶段执行setTimeout()和setInterval()里面的回调逻辑
- pending callbacks: executes I/O callbacks deferred to the next loop iteration.
idle, prepare: only used internally. 内部使用的一个状态 - poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.
- check: setImmediate() callbacks are invoked here. 此阶段执行setImmediate()的回调
- close callbacks: some close callbacks, e.g. socket.on(‘close’, …).
在一个I/O循环中,immediate() 总是比 timeout() 先执行 within an I/O cycle, the
immediate callback is always executed first than timeout callback:
参考:
- https://hackernoon.com/arthurunderstanding-the-nodejs-event-loop-hin34zp
- https://nodejs.org/uk/docs/guides/event-loop-timers-and-nexttick/
2. macrotask和microtask
在ES6中macro-task队列又称为ScriptJobs,而micro-task又称PromiseJobs
https://tc39.es/ecma262/#sec-jobs-and-job-queues
每个event loop都有一个macrotask和microtask
Each event loop has a microtask queue and a macrotask queue.
一个microtask是用来放入microstask queue里面的,而不是task queue(对应的是macrotask),参考如下文档
A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. Refer to https://www.w3.org/TR/html51/webappapis.html#microtask-queue.
有2种类型的microtask(微任务)
There are two kinds of microtasks:
- solitary callback microtasks, such as Promise, 单独的回调,如Promise
- and compound microtasks, such as Object.observe, MutationObserver and process.nextTick in Node.js. 组合的microtask,如Object.observe, MutationObserver,和nodejs里面的process.nextTick
有如下类型的macrotask(宏任务)
And the macrotask queue mainly contains
- setTimeout,
- setInterval,
- setImmediate,
- requestAnimationFrame,
- I/O in Nodejs.
在一个event-loop里面,2种task运行规则如下:
In a event Loop, these two task queues will run in two steps:
- First, check whether there is a macrotask (call it X) in old macrotask queue ;
- If X exists and it is running, wait for it to go to the next step until it was complete; otherwise, goto the next step immediately;
- Second, run all microtasks of the microtask queue;
- and when run the microtasks, we can still add some more microtaks into the queue, these tasks will also run.
The Call Stack is a fundamental part of the JavaScript language. It is a record-keeping structure that allows us to perform function calls. Each function call is represented as a frame on the Call Stack. This is how the JavaScript engine keeps track of which functions have been called and in what order. The JS engine uses this information to ensure execution picks back up in the right spot after a function returns.
When a JavaScript program first starts executing, the Call Stack is empty. When the first function call is made, a new frame is pushed onto the top of the Call Stack. When that function returns, its frame is popped off of the Call Stack.
The Event Loop is a looping algorithm that processes the Tasks/Microtasks in the Task Queue and Microtask Queue. It handles selecting the next Task/Microtask to be run and placing it in the Call Stack for execution.
The Event Loop algorithm consists of four key steps:
- Evaluate Script: Synchronously execute the script as though it were a function body. Run until the Call Stack is empty.
- Run a Task: Select the oldest Task from the Task Queue. Run it until the Call Stack is empty.
- Run all Microtasks: Select the oldest Microtask from the Microtask Queue. Run it until the Call Stack is empty. Repeat until the Microtask Queue is empty.
- Rerender the UI: Rerender the UI. Then, return to step 2. (This step only applies to browsers, not NodeJS).
Let’s model the Event Loop with some JavaScript psuedocode:
while (EventLoop.waitForTask()) {
const taskQueue = EventLoop.selectTaskQueue();
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask();
}
const microtaskQueue = EventLoop.microTaskQueue;
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask();
}
rerender();
}
参考文档:
- https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
- https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context?rq=1
- https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth
- https://medium.com/dkatalis/eventloop-in-nodejs-macrotasks-and-microtasks-164417e619b9
- https://stackoverflow.com/questions/46375711/what-is-the-relationship-between-event-loop-and-promise
3. 执行时序
3.1 整体的执行时序
script(主程序代码)—>process.nextTick—>Promises…——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering
setTimeout(function(){console.log(1)},0);
// 异步创建
Promise.resolve().then(function () {
console.log(22);
});
// 同步创建
new Promise(function(resolve,reject){
console.log(2);
resolve(true);
}).then(function(){console.log(3)
}).then(function(){console.log(4)});
process.nextTick(function(){console.log(5)});
new Promise(function(resolve,reject){
console.log(7);
resolve(true);
});
console.log(6);
class Studuent {
private mAge:number = 0;
constructor(age:number) {
this.mAge = age
console.log("age");
}
}
new Studuent(10);
执行结果如下:
2
7
6
age
5
22
3
4
1
说明:
- 首先执行的是new Promise构造方法里面的代码,按js代码的执行顺序,输出2
- 然后输出的是7,主要是用来测试new Promise构造方和process.nextTick的顺序
- 第3个输出的是6,按代码的顺序
- 第4个输出的是age,主要是用来验证new Promise构造方法,原理都类似,代码按顺序执行过来,执行构造方法里面的逻辑
- 第5个输出的是5,执行process.nextTick,是在本次代码执行完,准备执行后续event-loop之前执行
- 第6个输出的是22,走第1个promise
- 第7个和第8个,依次走后续的promise
- 第9个输出是1,走的是setTimeout,在下一次macrotask里面执行
再看一个特殊一点的例子
process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
Promise.resolve().then(() => {
console.log(3);
process.nextTick(() => console.log(4));
Promise.resolve().then(() => console.log(5));
}).then(() => {
console.log(6);
})
执行结果
1
2
3
5
6
4
特殊的地方在4,当执行以上代码的时候
When we run the code:
- callback in process.nextTick is added to process.nextTick queue.
- callback in promises are added to promises microtask queue.
Event loop executes tasks in following order: process.nextTick queue, promises microtask queue, setTimeout queue, setImmediate queue
- Event loop will execute callback from process.nextTick queue and prints 1.
- Event loop will look for promises microtask queue and then processes them. So it prints 2 and 3. On the next line it again adds a callback to process.nextTick queue which will be executed once promises microtask queue is finished.
- So the next 5 and 6 will be printed. Now promises microtask queue is empty
- Event loop will again check if there is any handler in process.nextTick queue and will execute them. So it now prints 4.
参考:
- https://medium.com/dkatalis/eventloop-in-nodejs-macrotasks-and-microtasks-164417e619b9
- https://medium.com/dkatalis/eventloop-in-nodejs-settimeout-setimmediate-vs-process-nexttick-37c852c67acb
- https://developer.aliyun.com/article/613411
- https://github.com/nodejs/help/issues/1789
3.2 process.nextTick和setImmediate
nextTick是在setImmediate之前执行的
在上面的nodejs的event-loop图里面,process.nextTick()里面的回调会在每次event-loop继续执行之前执行
Looking back at our diagram, any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues.
使用process.nextTick()方法,可以保证我们的代码可以在用户代码执行完之后且在event-loop事件开始前执行
By using process.nextTick() we guarantee that apiCall() always runs its callback after the rest of the user’s code and before the event loop is allowed to proceed.
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(
callback,
new TypeError('argument should be string')
);
}
关于这2个方法的命名,其实有一些歧义,nodejs的开发也有如下解释:
Note that nextTick is opposite of “in the next tick”. It’s “in the
current tick but after all synchronous code and previously queued
nextTicks finished”.setImmediate is the opposite of immediately - it runs in the next tick
😅 .Their naming is pretty confusing. setImmediate is already promisified
and people can use that to await for I/O to run.
参考:
- https://github.com/nodejs/node/issues/19617
- https://github.com/nodejs/node/pull/38393
- https://medium.com/dkatalis/eventloop-in-nodejs-settimeout-setimmediate-vs-process-nexttick-37c852c67acb
- https://github.com/nodejs/nodejs.dev/blob/main/src/documentation/0030-node-process-nexttick/index.md
- https://nodejs.org/uk/docs/guides/event-loop-timers-and-nexttick/
3.3 setImmediate()和setTimeout()
2个方法都类似,差异的地方在于他们调用的时机
- 在非I/O环境下,setTimeout在setImmediate之前执行
- 在I/O环境下,setImmediate在setTimeout之前执行
在非I/O环境下
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
执行结果是(多次执行的结果是一样的,和里面说的有点不一样,可能是执行环境的原因吧https://nodejs.org/uk/docs/guides/event-loop-timers-and-nexttick/)
$ node timeout_vs_immediate.js
timeout
immediate
在I/O环境下
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
执行结果是:
$ node timeout_vs_immediate.js
immediate
timeout