什么是 Node.js 事件循环滴答?

我一直在深入研究 Node.js 体系结构的内部结构,我经常看到的一个术语是“剔”,就像“事件循环的下一个剔”或函数 NextTick ()一样。

我没有看到的是什么是“蜱虫”的确切定义。基于各种文章(比如这个) ,我已经能够在我的头脑中拼凑出一个概念,但我不确定它有多准确。

我能得到一个 Node.js 事件循环滴答的精确和详细的描述吗?

27006 次浏览

Remember that while JavaScript is single-threaded, all of node's I/O and calls to native APIs are either asynchronous (using platform-specific mechanisms), or run on a separate thread. (This is all handled through libuv.)

So when there's data available on a socket or a native API function has returned, we need a synchronized way to invoke the JavaScript function that is interested in the particular event that just happened.

It's not safe to just call the JS function from the thread where the native event happened for the same reasons that you'd encounter in a regular multi-threaded application – race conditions, non-atomic memory access, and so forth.

So what we do is place the event on a queue in a thread-safe manner. In oversimplified psuedocode, something like:

lock (queue) {
queue.push(event);
}

Then, back on the main JavaScript thread (but on the C side of things), we do something like:

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
}

The while (true) (which doesn't actually exist in node's source code; this is purely illustrative) represents the event loop. The inner for invokes the JS function for each event that was on the queue.

This is a tick: the synchronous invocation of zero or more callback functions associated with any external events. Once the queue is emptied out and the last function returns, the tick is over. We go back to the beginning (the next tick) and check for events that were added to the queue from other threads while our JavaScript was running.

What can add things to the queue?

  • process.nextTick
  • setTimeout/setInterval
  • I/O (stuff from fs, net, and so forth)
  • crypto's processor-intensive functions like crypto streams, pbkdf2, and the PRNG (which are actually an example of...)
  • any native modules that use the libuv work queue to make synchronous C/C++ library calls look asynchronous

A simpler answer for those new to JavaScript:

The first thing to understand is that JavaScript is a "single-threaded environment". This refers to JavaScript's behavior of executing your blocks of code one at a time from "the event loop" on a single thread. Below there's a rudimentary implemenation of the event loop taken from Kyle Simpson's book ydkJS and afterwards, an explanation:

// `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);
}
}
}

The first while loop simulates the event loop. A tick is the dequeuing of an event from the "event loop queue" and the execution of said event.

Please see the response of 'Josh3796' for a more detailed explanation of what happens in the dequeuing and execution of an event.

Also, I recommend reading Kyle Simpson's book for those who are interested in getting a deep understanding of JavaScript. It's completely free and open-source and can be found at this link: https://github.com/getify/You-Dont-Know-JS

The specific section I referenced can be found here: https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/sync-async/ch1.md

Very simple and short way of Event Loop tick is:

It is used by node internal mechanism where when set of requests on a queue is processed then tick is initiated which represents the completion of a task

A "tick" refers to a complete pass through the event loop. Confusingly, setImmediate() takes a tick to run, whereas process.nextTick() is more immediate, so the two functions deserve a name swap.


┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
THE EVENT LOOP
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
                                                              

┌───────────────────────────────┐
│             poll              │
┌─▶│                               │──┐
│  └───────────────┬───────────────┘  │
│                  │                 tick
│  ┌───────────────▼───────────────┐  │
│  │             check             │  │
│  │                               │◀─┘
│  └───────────────┬───────────────┘
│                  │
│  ┌───────────────▼───────────────┐
│  │        close callbacks        │
│  │                               │
loop └───────────────┬───────────────┘
│                  │
│  ┌───────────────▼───────────────┐
│  │            timers             │
│  │                               │
│  └───────────────┬───────────────┘
│                  │
│  ┌───────────────▼───────────────┐
│  │       pending callbacks       │
│  │                               │
│  └───────────────┬───────────────┘
│                  │
│  ┌───────────────▼───────────────┐
│  │         idle, prepare         │
└──│                               │
└───────────────────────────────┘

The event loop (in Node.js) is an execution model where aspects of a script are executed in a cyclical manner according to a defined schedule.

It [event loop] is made up of a number of phases (as illustrated above). Each phase contains (1) a call stack, and (2) a callback queue. The call stack is where code is executed (on a LIFO basis), while the callback queue is where code is scheduled (on a FIFO basis) for later placement in the call stack for execution.

This callback queue can be sub-divided into 2 queues: a microTask queue and a macroTask queue. a micro-task (once scheduled) is a task that will be executed immediately after the current running script in the current phase, while a macro-task (once scheduled) is a task that will be executed in the next loop of said phase (after any micro-tasks in that phase).

The event loop runs in a cycle through all phases repeatedly until there is no more work to be done. Each cycle (through all the phases) can be referred to as a loop, while each complete invocation of scripts in a given queue can be referred to as a tick.

This tick will usually happen from one phase to another, but a tick can happen within a phase when both the microTask and macroTask queues are not empty e.g. when a Promise is resolved in the running script, its then method adds items to the microTask queue.

When you write code (say in a mycode.js file) and then invoke it (with node mycode.js), this code will be executed using the event loop according to how it is written.

Here's an example script:

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');

Copy (or type) this into a .js file, and invoke it with node.

You should get the following output:

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]

Note: Using Node.js version 16.15.0

Before the explanation, here are a few rules to remember:

  • setImmediate schedules scripts to run in the next check phase of the event loop (in the macroTask queue)
  • setTimeout schedules scripts to run in the next timers phase of the event loop (in the macroTask queue)
  • Process.nextTick schedules scripts to run before the next tick i.e. either (1) after the current script has run but before the microTask queue has run [if said queue is not empty], or (2) before the event loop traverses from one phase to the next [if microTask queue is empty]
  • Promise.prototype.then schedules scripts to run in the current microTask queue i.e. after the current script, but before scripts scheduled for the next phase
  • The microTask queue is run before the macroTask queue

Here's the explanation in the form of a timeline of events:

A. FROM POLL PHASE (LOOP 1)

  1. console.log('poll phase - 1') and console.log('poll phase - 2') are synchronous code and will run immediately in the current phase
  2. console.log('next tick - 1 [scheduled from poll]') and console.log('next tick - 4 [scheduled from poll]') are scheduled by process.nextTick to run before the next tick i.e. before the check phase (since there is nothing in microTask queue).
  3. The callback on setImmediate (Line 7) is scheduled to run in the check phase
  4. The callback on setTimeout (Line 33) is scheduled to run in the timers phase

B. BEFORE CHECK PHASE (LOOP 1) 5. console.log('next tick - 1 [scheduled from poll]') and console.log('next tick - 4 [scheduled from poll]') are executed

C. FROM CHECK PHASE (LOOP 1) 6. console.log('check phase - 1') and console.log('check phase - 2') [from callback previously scheduled by setImmediate (Line 7)] are executed immediately as they are synchronous 7. console.log('next tick - 2 [scheduled from check]') is scheduled by process.nextTick 8. The callbacks on Line 15, 18, and 21 are scheduled to run in the microTask queue. 9. console.log('next tick - 2 [scheduled from check]') is executed (because this is before the next tick i.e. after the current script but before microTask queue) 10. The callbacks on Line 15 and 18 are executed (because the microTask is executed immediately after the running script) 11. The callback on Line 21 is executed and schedules (1) console.log('timers phase [scheduled from Promise in check]') to run in the next timers phase, and (2) console.log('next tick - 3 [scheduled from Promise in check]') to run before the next tick i.e. before traversal from current phase (check) to the next active phase (timers)

D. BEFORE TIMERS PHASE (LOOP 1) 12. console.log('next tick - 3 [scheduled from Promise in check]') is executed

E. FROM TIMERS PHASE (LOOP 1) 13. console.log('timers phase - 1') is executed 14. setImmediate (Line 36) schedules its callback to be run in the next check phase 15. The Promise (Line 40) schedules three callbacks to be run in the microTask queue 16. console.log('timers phase - 1.1 [microTask]') and console.log('timers phase - 1.2 [microTask]') are executed as scheduled in 15. 17. console.log('timers phase [scheduled from Promise in check]') is executed. It was previously scheduled by setTimeout (Line 22). It's running now (after the code in 16. above) because it is a macroTask (so it runs after the microTask queue has been run)

E. FROM NEXT CHECK PHASE (LOOP 2) 18. console.log('check phase [scheduled from timers]') is executed. It was previously scheduled in the timers phase (of Loop 1) by setImmediate (Line 36)

F. FROM NEXT TIMERS PHASE (LOOP 2) 19. console.log('timers phase [scheduled from Promise in timers]') is executed. It was previously scheduled in the timers phase (of Loop 1) by setTimeout (Line 48)

References