我越来越深入地了解Node.js架构内部,一个经常出现的术语是“tick”,比如“事件循环的下一个tick”或函数nextTick()。
但我没有看到对“tick”具体含义的确切定义。根据各种文章(例如这篇文章),我已经能够在脑海中构建一个概念,但我不确定它有多准确。
我可以得到一个关于Node.js事件循环tick的精确和详细描述吗?
我越来越深入地了解Node.js架构内部,一个经常出现的术语是“tick”,比如“事件循环的下一个tick”或函数nextTick()。
但我没有看到对“tick”具体含义的确切定义。根据各种文章(例如这篇文章),我已经能够在脑海中构建一个概念,但我不确定它有多准确。
我可以得到一个关于Node.js事件循环tick的精确和详细描述吗?
lock (queue) {
queue.push(event);
}
然后,在JavaScript的主线程上(但在C端),我们做如下操作:
while (true) {
// this is the beginning of a tick
lock (queue) {
var tickEvents = copy(queue); // copy the current queue items into thread-local memory
queue.empty(); // ..and empty out the shared queue
}
for (var i = 0; i < tickEvents.length; i++) {
InvokeJSFunction(tickEvents[i]);
}
// this the end of the tick
}
while (true)
(实际上不存在于Node的源代码中;这仅是为了说明)代表了事件循环。内部的for
调用JS函数处理队列中的每个事件。
这就是一个tick:同步调用与任何外部事件相关联的零个或多个回调函数。一旦队列清空并且最后一个函数返回,tick就结束了。我们回到开头(下一个tick)并检查从其他线程添加到队列中的事件,而我们的JavaScript正在运行。
什么可以将事物添加到队列中呢?
process.nextTick
:在当前执行栈的末尾添加一个任务,以便在下一个事件循环迭代中运行。它比setTimeout
和setInterval
更快。setTimeout
/setInterval
:用于在一定时间后执行函数或在一定时间间隔内重复执行函数。fs
,net
等):与文件系统、网络等相关的输入输出操作。crypto
的处理器密集型函数,如加密流、pbkdf2和PRNG(实际上是一个示例...)setImmediate
也会将一个函数加入到队列中。 - DanielKhan对于刚接触JavaScript的人来说,这是一个更简单的答案:
首先要理解的是JavaScript是“单线程环境”。这意味着JavaScript会在单个线程上从“事件循环”一次执行你的代码块。下面是Kyle Simpson的书《ydkJS》中提取出的一个基本的事件循环实现,以及后面的解释:
// `eventLoop` is an array that acts as a queue (first-in, first-out)
var eventLoop = [ ];
var event;
// keep going "forever"
while (true) {
// perform a "tick"
if (eventLoop.length > 0) {
// get the next event in the queue
event = eventLoop.shift();
// now, execute the next event
try {
event();
}
catch (err) {
reportError(err);
}
}
}
第一个 while 循环模拟事件循环。 一个 tick 是从“事件循环队列”中出队一个事件并执行该事件。
请参见 “Josh3796” 的回复,以了解关于事件出队和执行的更详细说明。
此外,我建议那些有兴趣深入了解 JavaScript 的人阅读 Kyle Simpson 的书籍。这本书完全免费且开源,可以在以下链接找到:https://github.com/getify/You-Dont-Know-JS
我引用的具体章节可以在此处找到:https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/sync-async/ch1.md
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
THE EVENT LOOP
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
┌───────────────────────────────┐
│ poll │
┌─▶│ │──┐
│ └───────────────┬───────────────┘ │
│ │ tick
│ ┌───────────────▼───────────────┐ │
│ │ check │ │
│ │ │◀─┘
│ └───────────────┬───────────────┘
│ │
│ ┌───────────────▼───────────────┐
│ │ close callbacks │
│ │ │
loop └───────────────┬───────────────┘
│ │
│ ┌───────────────▼───────────────┐
│ │ timers │
│ │ │
│ └───────────────┬───────────────┘
│ │
│ ┌───────────────▼───────────────┐
│ │ pending callbacks │
│ │ │
│ └───────────────┬───────────────┘
│ │
│ ┌───────────────▼───────────────┐
│ │ idle, prepare │
└──│ │
└───────────────────────────────┘
在Node.js中,事件循环是一种执行模型,其中脚本的各个方面按照定义的时间表循环执行。
事件循环由许多阶段(如上图所示)组成。每个阶段都包含(1)调用栈 和 (2) 回调队列。调用栈是代码执行的地方(按照后进先出LIFO原则),而回调队列是代码预定执行的地方(按照先进先出FIFO原则),稍后会安排进入调用栈以供执行。
此回调队列可以分成两个队列:一个微任务队列和一个宏任务队列。一旦预定了微任务,它将立即在当前阶段中正在运行的脚本之后执行,而一旦预定了宏任务,则它将在该阶段的下一个循环中执行(在该阶段中的任何微任务之后执行)。
事件循环通过所有阶段的循环运行,直到没有更多工作需要完成为止。每个循环(通过所有阶段)可以称为一个“循环(loop)”,而在给定队列中完全调用脚本可以称为一个“tick(滴答)”。
这个“tick”通常会在一个阶段之间发生,但是当微任务和宏任务队列都不为空时,也可能在阶段内发生,例如当Promise在运行脚本中得到解决时,它的“then(然后)”方法会向微任务队列添加项目。
当您编写代码(比如一个名为“mycode.js”的文件),并通过node mycode.js
调用它时,此代码将按照其编写方式使用事件循环执行。
下面是一个示例脚本:
process.nextTick(function() {
console.log('next tick - 1 [scheduled from poll]');
});
console.log('poll phase - 1');
setImmediate(function() {
console.log('check phase - 1');
process.nextTick(function() {
console.log('next tick - 2 [scheduled from check]');
});
Promise.resolve()
.then(function() {
console.log(`check phase - 1.1 [microTask]`);
})
.then(function() {
console.log(`check phase - 1.2 [microTask]`);
})
.then(function() {
setTimeout(function() {
console.log('timers phase [scheduled from Promise in check]');
});
process.nextTick(function() {
console.log('next tick - 3 [scheduled from Promise in check]');
});
});
console.log('check phase - 2');
});
setTimeout(function() {
console.log('timers phase - 1');
setImmediate(function() {
console.log('check phase [scheduled from timers]');
});
Promise.resolve()
.then(function() {
console.log('timers phase - 1.1 [microTask]');
})
.then(function() {
console.log('timers phase - 1.2 [microTask]');
})
.then(function() {
setTimeout(function() {
console.log('timers phase [scheduled from Promise in timers]');
});
});
});
process.nextTick(function() {
console.log('next tick - 4 [scheduled from poll]');
});
console.log('poll phase - 2');
node
调用它。poll phase - 1
poll phase - 2
next tick - 1 [scheduled from poll]
next tick - 4 [scheduled from poll]
check phase - 1
check phase - 2
next tick - 2 [scheduled from check]
check phase - 1.1 [microTask]
check phase - 1.2 [microTask]
next tick - 3 [scheduled from Promise in check]
timers phase - 1
timers phase - 1.1 [microTask]
timers phase - 1.2 [microTask]
timers phase [scheduled from Promise in check]
check phase [scheduled from timers]
timers phase [scheduled from Promise in timers]
注意:使用Node.js版本16.15.0
在解释之前,请记住以下几点规则:
setImmediate
将脚本安排在事件循环的下一个check 阶段(宏任务队列中)中运行setTimeout
将脚本安排在事件循环的下一个timers阶段(宏任务队列中)中运行process.nextTick
将脚本安排在下一次tick之前运行,即(1)在当前脚本运行后但在微任务队列运行之前运行[如果该队列不为空],或者(2)在事件循环从一个阶段遍历到下一个阶段之前运行[如果微任务队列为空]Promise.prototype.then
将脚本安排在当前的微任务队列中运行,即在当前脚本之后,但在下一阶段计划的脚本之前运行这是一个事件时间轴的解释:
A. 来自POLL PHASE (LOOP 1)
console.log('poll phase - 1')
和console.log('poll phase - 2')
是同步代码,将立即在当前阶段运行console.log('next tick - 1 [scheduled from poll]')
和console.log('next tick - 4 [scheduled from poll]')
由process.nextTick
调度,以在下一次tick之前运行,即在check 阶段之前(因为微任务队列中没有任何内容)。setImmediate
的回调函数(第7行)被安排在check 阶段运行setTimeout
的回调函数(第33行)被安排在timers阶段运行B. 在CHECK PHASE之前 (LOOP 1)
5. 执行console.log('next tick - 1 [scheduled from poll]')
和console.log('next tick - 4 [scheduled from poll]')
console.log('check phase - 1')
和 console.log('check phase - 2')
[来自之前由 setImmediate
(第7行) 调度的回调] 立即执行,因为它们是同步的。
7. console.log('next tick - 2 [scheduled from check]')
被 process.nextTick
调度。
8. 第15、18和21行的回调被安排在 microTask 队列中运行。
9. console.log('next tick - 2 [scheduled from check]')
被执行 (因为这是在下一个 tick 之前,即在当前脚本之后但在 microTask 队列之前)。
10. 第15和18行的回调被执行 (因为 microTask 在运行脚本后立即执行)。
11. 第21行的回调被执行并调度 (1) console.log('timers phase [scheduled from Promise in check]')
在下一个 timers 阶段运行,以及 (2) console.log('next tick - 3 [scheduled from Promise in check]')
在下一个 tick 之前运行,即在从当前阶段 (check) 到下一个活动阶段 (timers) 的遍历之前运行。console.log('next tick - 3 [scheduled from Promise in check]')
被执行。console.log('timers phase - 1')
被执行。
14. setImmediate
(第36行) 调度其回调在下一个 check 阶段运行。
15. Promise
(第40行) 调度三个回调在 microTask 队列中运行。
16. console.log('timers phase - 1.1 [microTask]')
和 console.log('timers phase - 1.2 [microTask]')
如第15点所安排的那样被执行。
17. console.log('timers phase [scheduled from Promise in check]')
被执行。它先前由 setTimeout
(第22行) 调度。它现在运行 (在上面的代码16之后) 因为它是一个 macroTask (所以它在 microTask 队列运行后运行)。console.log('check phase [scheduled from timers]')
被执行。它先前是由 setImmediate
(第36行) 在第一次循环中的 timers 阶段中调度的。console.log('timers phase [scheduled from Promise in timers]')
,它先前是由setTimeout
(第1次循环中的timers阶段)在timers阶段中安排的(第48行)。
参考资料
事件循环的简单且短暂的表述是:
这是Node内部机制使用的方法,在处理队列上的一组请求后,将启动Tick,表示任务已完成。