-
Notifications
You must be signed in to change notification settings - Fork 30.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
doc: add topic - event loop, timers, nextTick()
#4936
Changes from 1 commit
46f0702
5a28415
dc1b8a5
bb5b682
ba98380
936bf17
35cf726
f80d7cc
254694b
45fb2fe
d6d76f5
c133caf
f425164
1bd3e6c
7574d4b
8dc6ecb
d82a7f1
1dc26f6
82d0fb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
# Overview of the Event Loop, Timers, and `process.nextTick()` | ||
|
||
The Following diagram shows a simplified overview of the event loop's order of operations. | ||
|
||
┌───────────────────────┐ | ||
┌─>│ timers │ | ||
│ └──────────┬────────────┘ | ||
│ ┌──────────┴────────────┐ | ||
│ │ pending callbacks │ | ||
│ └──────────┬────────────┘ ┌───────────────┐ | ||
│ ┌──────────┴────────────┐ │ incoming: │ | ||
│ │ poll │<─────│ connections, │ | ||
│ └──────────┬────────────┘ │ data, etc. │ | ||
│ ┌──────────┴────────────┐ └───────────────┘ | ||
└──│ setImmediate │ | ||
└───────────────────────┘ | ||
|
||
note: each box will be referred to as a "phase" of the event loop. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe make this italic? |
||
|
||
*There is a slight discrepancy between the Windows and the Unix/Linux | ||
implementation, but that's not important for this demonstration. The most | ||
important parts are here. There are actually seven or eight steps, but the | ||
ones we care about--ones that Node actually uses are these four.* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this "what Node uses vs. what libuv provides?" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @trevnorris will you clarify this, please? I'm not familiar with the relationship between Node.js and libuv enough to comment, myself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. There are other minor steps inbetween these four provided, but as I understand, they are not particularly notable to an end user. (i.e. how this interacts with the microtask queue.) It is possible this should be expanded to encompass some of those, so long as it is still concise. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. libuv's event loop does have more steps than what is provided here. For example, Or that if the event loop is run with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also there's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'd lean towards documenting microtask queue behavior if it's feasible to do so here — I've definitely found that information useful in the past when dealing with builtin promises. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (But otherwise I'm totally on board with the simplification. We should note in the doc that libuv is providing the seven or eight steps, but that Node only exposes the steps described in this document to end users, primarily.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think the most notable thing is that it's processed after the nextTick queue, but then after the microtask queue is processed the nextTick queue is processed again. Then back to the microtask queue. etc. This is to handle the case that one queues the other. But might be sufficient to put that it's "processed at the same time as the nextTick queue". |
||
|
||
## timers | ||
|
||
This phase executes callbacks scheduled by `setTimeout()` and `setInterval()`. | ||
When you create a timer, you make a call to setTimeout(). The event loop will | ||
eventually enter the 'poll' phase which determines how many milliseconds remain | ||
until the next timer. If there is a timer, it will wait for connections for that | ||
many milliseconds. After that many milliseconds, it will break the 'poll' phase | ||
and wrap back around to the timers phase where those callbacks can be processed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the poll phase of the event loop is entered the number of ms before the soonest timer is to be called is set as the poll's timeout. Meaning the poll phase will return after "timeout" ms. Take note that the poll phase can only return while idle. Meaning execution of a callback is allowed to run to completion, and can cause unexpected delay running the timer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Updated to include these notes. Thanks for the further detail! |
||
|
||
*Note: The 'poll' phase technically controls when timers are called due to its | ||
ability to cause a thread to sit idly without burning CPU in order to stall the | ||
event loop so the timer can execute.* | ||
|
||
## pending callbacks: | ||
|
||
This phase executes callbacks for specific types of TCP errors, for example. | ||
|
||
## poll: | ||
|
||
This is the phase in which the event loop sits and waits for incoming | ||
connections to be received. Ideally, most scripts spend most of their time here. | ||
|
||
## setImmediate: | ||
|
||
`process.setImmediate()` is actually a special timer that runs in a separate | ||
phase of the event loop. It uses a libuv API that schedules callbacks to execute | ||
after the poll phase has completed. | ||
|
||
Generally, as the code is executed, the event loop will eventually hit the | ||
'poll' phase where it will wait for an incoming connection, request, etc. | ||
However, after a callback has been scheduled with `setImmediate()`, at the start | ||
of the poll phase, a check will be run to see if there are any callbacks | ||
waiting. If there are none waiting, the poll phase will end and continue to the | ||
`setImmediate` callback phase. | ||
|
||
### setImmediate vs setTimeout | ||
|
||
How quickly a `setImmediate()` callback is executed is only limited by how | ||
quickly the event loop can be processed whereas a timer won't fire until the | ||
number of milliseconds passed have elapsed. | ||
|
||
The advantage to using `setImmediate()` over `setTimeout()` is that the lowest | ||
value you may set a timer's delay to is 1 ms (0 is coerced to 1), which doesn't | ||
seem like much time to us humans, but it's actually pretty slow compared to how | ||
quickly `setImmediate()` can execute--the event loop operates on the microsecond | ||
scale (1 ms = 1000 µs). | ||
|
||
## nextTick: | ||
|
||
### Understanding nextTick() | ||
|
||
You may have noticed that `nextTick()` was not displayed in the diagram, even | ||
though its a part of the asynchronous API. This is because nextTick is not | ||
technically part of the event loop. Instead, it is executed at the end of each | ||
phase of the event loop. | ||
|
||
Looking back at our diagram, any time you call nextTick in a given phase, all | ||
callbacks passed to nextTick will be resolved before the event loop continues. | ||
This can create some bad situations because **it allows you to asynchronously | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might suggest nixing |
||
"starve" your I/O by making recursive nextTick calls.** which prevents the | ||
event loop from reaching the poll phase. | ||
|
||
### Why would that be allowed? | ||
|
||
Why would something like this be included in Node? Part of it is a design | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Node.js |
||
philosophy where an API should always be asynchronous even where it | ||
doesn't have to be. Take this code snippet for example: | ||
|
||
```javascript | ||
function apiCall (arg, callback) { | ||
if (typeof arg !== 'string') | ||
return process.nextTick( | ||
callback, | ||
new TypeError('argument should be a string')); | ||
} | ||
``` | ||
|
||
The snippet does an argument check and if its not correct, it will pass the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
error to the callback. The API updated fairly recently to allow passing | ||
arguments to nextTick allowing it to take any arguments passed after the callback | ||
to be propagated as the arguments to the callback so you don't have to nest functions. | ||
|
||
What we're doing is passing an error back to the user. As far as the event loop | ||
is concerned, its happening **synchronously**, but as far as the user is | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might consider splitting this sentence in two:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Thank you! |
||
concerned, it is happening **asynchronously** because the API of apiCall() was | ||
written to always be asynchronous. | ||
|
||
This philosophy can lead to some potentially problematic situations. Take this | ||
snippet for example: | ||
|
||
```javascript | ||
// this has an asynchronous signature, but calls callback synchronously | ||
function someAsyncApiCall (callback) { | ||
callback(); | ||
}; | ||
|
||
// the callback is called before `someAsyncApiCall` completes. | ||
someAsyncApiCall(function () { | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should use ES2015 for our examples? IE fat arrow functions? |
||
// since someAsyncApiCall has completed, bar hasn't been assigned any value | ||
console.log('bar', bar); // undefined | ||
|
||
}); | ||
|
||
var bar = 1; | ||
``` | ||
|
||
The user defines `someAsyncApiCall()` to have an asynchronous signature, | ||
actually operates synchronously. When it is called, the callback provided to | ||
`someAsyncApiCall ()` is called in the same phase of the event loop | ||
because `someAsyncApiCall()` doesn't actually do anything asynchronously. As a | ||
result, the callback tries to reference `bar` but it may not have that variable | ||
in scope yet because the script has not been able to run to completion. | ||
|
||
By placing it in a nextTick, the script | ||
still has the ability to run to completion, allowing all the variables, | ||
functions, etc., to be initialized prior to the callback being called. It also | ||
has the advantage of not allowing the event loop to continue. It may be useful | ||
that the user be alerted to an error before the event loop is allowed to | ||
continue. | ||
|
||
## process.nextTick() vs setImmediate() | ||
|
||
We have two calls that are similar as far as users are concerned, but their | ||
names are confusing. | ||
|
||
* nextTick fires immediately on the same phase | ||
* setImmediate fires on the following iteration or 'tick' of the event loop | ||
|
||
In essence, the names should be swapped. nextTick fires more immediately than | ||
setImmediate but this is an artifact of the past which is unlikely to change. | ||
Making this switch would break a large percentage of the packages on npm. | ||
Every day more new modules are being added, which mean every day we wait, more | ||
potential breakages occur. While they are confusing, the names themselves won't change. | ||
|
||
*We recommend developers use setImmediate in all cases because its easier to | ||
reason about.* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (And it leads to code that's compatible with a wider variety of environments, like browser JS.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. technically this can be achieved using |
||
|
||
## Two reasons to use nextTick: | ||
|
||
1. Allow users to handle errors, cleanup any then unneeded resources, or | ||
perhaps try the request again before the event loop continues. | ||
|
||
2. If you were to run a function constructor that was to, say, inherit from | ||
`EventEmitter` and it wanted to call an event within the constructor. You can't | ||
emit an event from the constructor immediately because the script will not have | ||
processed to the point where the user assigns a callback to that event. So, | ||
within the constructor itself, you can set a callback to emit the event after | ||
the constructor has finished, which provides the expected results. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't one want to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe also an example as well would be awesome :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It sounds like Are there good reasons to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wrote up something about this: nodejs/docs#34 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chrisdickinson As for the constructor case, say the construction is happening in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This begs the questions:
If it is the former, then it seems to me that If it is the latter and it IS in the From what I've gathered, I believe the former provides the most accurate analogy for this example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @techjeffharris The event loop doesn't start until after bootstrap. Which includes the initial script passed to node. Here's a partial native stack of running
If it had been in the event loop then you would have seen Even if you're in the poll phase then execution order of anything queued while in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent, thank you for clarifying that. |
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file should end with a
.md
extension.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ooops >_<