┌───────────────────────────┐
┌─>│ timers │ (this phase executes callbacks scheduled by setTimeout() and setInterval())
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ (executes I/O callbacks deferred to the next loop iteration.)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ (only used internally)
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │ (retrieve new I/O events; execute I/O related callbacks)
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │ (setImmediate() callbacks are invoked here)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ (some close callbacks, e.g. socket.on('close', ...))
└───────────────────────────┘
for more information, please refer to https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
One go-around of the event loop will have exactly one task being processed from th macrotask queue (this queue is simply called the task queue in the WHATWG specification). After this marcotask has finished, all available microtasks will be processed, namely within the same go-around cycle. While these microtasks are processed, they can queue even more microtasks, which will all be run one by one, until the microtask queue is exhausted.
As noted above, if one microtask creates a new microtask constantly, which will be pushed into the tail of the microtask queue, it can starve the other marcotask because the microtask queue will never be cleared.
Examples: macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering microtasks: process.nextTick, Promises, Object.observe, MutationObserver
JS code
console.log('main thread start ...');
setTimeout(() => console.log('Timeout1'), 0); // Macro task queue
let promiseF = () => new Promise(resolve => setTimeout(() => resolve('Timeout3'), 0));
let asyncF = async () => console.log(await promiseF());
asyncF(); // For async will wrap the result with promise, "console.log(await promiseF())"" enters Micro task
let p1 = Promise.resolve('p1');
let p2 = Promise.resolve('p2');
p1.then(r => {
console.log(r); // p1
setTimeout(() => console.log('Timeout2'), 0); // Macro task queue
const p3 = Promise.resolve('p3');
p3.then(console.log); // p3
}); // Micro task queue
p2.then(console.log); // p2
setTimeout(() => console.log('Timeout4'), 0); // Macro task
console.log('main thread end.');
The result of above code
main thread start ...
main thread end.
p1
p2
p3
Timeout1
Timeout4
Timeout2
Timeout3
The order of execution
-
Synchronous code
- Executing the
console.log('main thread start ...');
, then printing the logmain thread start ...
. - Pushing the
() => console.log('Timeout1')
setTimeout callback task into the macrotask queue, then go on. - Calling the
asyncF()
function, Because this function is async function, it will wrap theconsole.log(await promiseF())
, it can be regarede aspromise.then(...)
, so theconsole.log(await promiseF())
will be pushed into the microtask queue, go on. p1.then(...)
andp2.then(...)
will also be pushed into the microtask queue.- Pushing the
() => console.log('Timeout4')
setTimeout callback task into the macrotask queue. - Executing the
console.log('main thread end.');
, then printing the logmain thread end.
.
After the above actions, the Macrotask queue and Microtask queue will like this:
Macrotask queue Microtask queue ┌─────────────────────────┐ ┌───────────────────────────────────┐ | console.log('Timeout1') | | console.log(await promiseF()) | └─────────────────────────┘ └───────────────────────────────────┘ | console.log('Timeout4') | | p1.then(...) | └─────────────────────────┘ └───────────────────────────────────┘ | p2.then(...) | └───────────────────────────────────┘
- Executing the
-
Handling the microtask queue
- According to the "First in First out" principle, Firstly handling
console.log(await promiseF())
, we can regard theawait
as microtask, so the microtask queue will look like this:
Microtask queue ┌────────────────────────────────────────────┐ | p1.then(...) | └────────────────────────────────────────────┘ | p2.then(...) | └────────────────────────────────────────────┘ | setTimeout(() => resolve('Timeout3'), 0) | └────────────────────────────────────────────┘
- Handling
p1.then(...)
, printing "p1", pushingconsole.log('Timeout2')
into macrotask queue, and pushingp3.then(...)
into microtask queue.
Macrotask queue Microtask queue ┌─────────────────────────┐ ┌────────────────────────────────────────────┐ | console.log('Timeout1') | | p2.then(...) | └─────────────────────────┘ └────────────────────────────────────────────┘ | console.log('Timeout4') | | setTimeout(() => resolve('Timeout3'), 0) | └─────────────────────────┘ └────────────────────────────────────────────┘ | console.log('Timeout2') | | p3.then(...) | └─────────────────────────┘ └────────────────────────────────────────────┘
- Handling
p2.then(...)
, printingp2
, then executingsetTimeout(() => resolve('Timeout3'), 0)
, pushing theresolve('Timeout3')
into macrotask queue, and printingp3
.
Macrotask queue ┌─────────────────────────┐ | console.log('Timeout1') | └─────────────────────────┘ | console.log('Timeout4') | └─────────────────────────┘ | console.log('Timeout2') | └─────────────────────────┘ | resolve('Timeout3') | └─────────────────────────┘
- Finally clearing the macrotask queue.
- According to the "First in First out" principle, Firstly handling
So, the result will be as shown above.
中文版请访问这儿