Take a look at the features of event loops in browsers and Nodes:

  • Browser: Different implementation, Browser Context
  • Node: multiple stages, process.nexttick ()

This is a problem that rarely needs to be explored in real life, but it’s worth knowing, not least to see that the following print order problems don’t get stuck:

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

new Promise((resolve) => {
  console.log('promise');
  resolve()
}).then(() => {
  console.log('promise - then');
}).then(() => {
  console.log('promise - then - then');
})

console.log('script end');
Copy the code

contrast

In comparing the browser and Node event loops, you can look at several aspects in the following table:

category The browser Node
Specification/Design HTML Spec libuv
implementation Different browsers have different implementations The use of libuvuv_default_loop()
Task queue macrotask queue, microtask queue Timers Queue/I/O Queue/Check Queue…
context browsing context Depends on the calling module

The browser

The browser event loop divides tasks into two categories: microTasks and MacroTasks (also known as tasks)

process

The event loop in the browser flows as follows

  1. The read function performs tasks on the stack, and executes
  2. Read all tasks in the MicroTask Queue and execute
  3. Read a task in a MacroTask Queue and execute
  4. Loop 2,3 steps until there are no tasks

Task queue

Types of tasks in different queues:

  • macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
  • microtasks: process.nextTick, Promises, Object.observe, MutationObserver

This task type is not specified in the HTML specification.

Tasks in queues have priorities, and they have different priorities in different environments.

Promise is special in that it is defined in ECMAScript rather than in the HTML specification. Jobs in ECMAScript is similar to a microTask in that some browsers refer to it as a microtask and some as a MacroTask. A general consensus is that promises belong to MicroTask.

other

  • You can view live event loops and task queues as the browser executes code on this site.
  • Web workers work on separate threads and have their own Event loops that do not share the Browser Context.

Node

In browsers and Nodes, JS runs in a single thread, and the current stack must be empty before the next event loop is entered.

Node uses the default event loop object uv_default_loop in Libuv.

process

Each event loop in Node(libuv) runs as follows:

  1. The timer: performsetTimeout()andsetInterval()Callback of a scheduled task
  2. Pending Callbacks: Execute callbacks that were not executed in the previous loop
  3. Idle, prepare: the command is executed internally
  4. Poll: Polls I/O tasks
  5. Check: to performsetImmediate()The callback
  6. The close callbacks: somecloseEvent callbacks, such assocket.on('close', ...)

It can be seen that the Event loop in Node is divided into different stages, and each stage has its own task.

aboutsetTimeout().setImmediate()andprocess.nextTick()

While setTimeout() and setImmediate() are called, their scheduled call-back functions are executed in the next event loop, nextTick() does not. It is called before the end of this event loop. It is conceivable that if nextTick() is called recursively, the delayed task will have no chance to execute. In addition, promises and nextTick are microtasks that are executed before an event loop ends.

If the following code exists:

setImmediate(() => {
    console.log('immediate');
});
process.nextTick(() => {
    console.log('nextTick');
});
// nextTick
// immediate
Copy the code

The results will always meet expectations, and nextTick will always print first. But if you change it to:

setImmediate(() => {
    console.log('immediate');
});
setTimeout(() => {
    console.log('timeout');
}, 0);
// ?
// ?
Copy the code

You’ll find that their output order changes and is unstable, whereas if called in an I/O loop:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
// immediate
// timeout
Copy the code

You see that the output always matches expectations, and immediate is always printed first.

This is because the order in which timers are executed in Node depends on the context in which they are executed.

While dispatching in the main module may be constrained by process performance (and by other applications running on the machine), setImmediate() always executes before callbacks from other timers if dispatching is in an I/O loop.

For performance constrained cases, here’s an example: Although the callback to setTimeout() is executed in the timer queue of the first phase, it needs to access the timer, calculate and wait for the timeout time and all functions in the wait queue to complete execution. Thus the callback may execute later than the setImmediate() callback in the fourth phase.

The instance

Execute the following code in the browser and Node respectively:

setTimeout(() => { console.log('setTimeout - 1') setTimeout(() => { console.log('setTimeout - 1 - 1') }) new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then') new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then - then') }) }) }) setTimeout(() => { console.log('setTimeout - 2') setTimeout(() => { console.log('setTimeout - 2 - 1') }) new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 2 - then') new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 2 - then -  then') }) }) })Copy the code

The results are as follows:

Browser (Chrome) Node
setTimeout – 1 setTimeout – 1
setTimeout – 1 – then setTimeout – 2
setTimeout – 1 – then – then setTimeout – 1 – then
setTimeout – 2 setTimeout – 2 – then
setTimeout – 2 – then setTimeout – 1 – then – then
setTimeout – 2 – then – then setTimeout – 2 – then – then
setTimeout – 1 – 1 setTimeout – 1 – 1
setTimeout – 2 – 1 setTimeout – 2 – 1

You can clearly see the difference between how browsers and Nodes handle delayed functions.

A brief description of the process in the browser:

  1. When two setTimeouts are encountered, register with the MacroTask Queue and execute the first task. (setTimeout - 1)
  2. When setTimeout is encountered, register with macroTask Queue. When you meet a Promise, register the MicroTask Queue and its nested Microtasks to execute all the tasks in the MicroTask Queue. (setTimeout - 1 - thenandsetTimeout - 1 - then - then)
  3. Execute the next task in the Macro Queue (setTimeout for the second outer layer), similar to the previous step, and print (setTimeout - 2.setTimeout - 1 - thenandsetTimeout - 1 - then - then)
  4. Execute the remaining two tasks in the Macro Queue. (setTimeout - 1 - 1andsetTimeout - 2 - 1)

Now look at the first question, you should see the order at a glance:

script start
promise
script end
promise - then
promise - then - then
setTimeout
Copy the code

And the order is the same in both environments.

Further reading

The browser

  • Event loops and task queues
  • Tasks, microtasks, queues and schedules
  • Difference between microtask and macrotask within an event loop context
  • Philip Roberts: What exactly is an Event Loop? | JSConf 2014 in Europe

Node

  • The Node.js Event Loop, Timers, and process.nextTick()
  • Node.js event loop and timer/setImmediat…
  • Event loop in JavaScript
  • Don’t confuse NodeJS with event loops in browsers

other

  • HTML 5.2:7. Web Application APIs Event-Loops
  • ES6 – Jobs and Job Queues
  • A libuv event loop log