preface

JS is a single-threaded language. If there is no asynchronous operation, a time-consuming operation can block the entire process. However, after asynchronous operation, there will be problems between data communication, and event loop well solves this problem.

Event Loop

What is an Event loop? That’s the first thing we need to know. The official HTML standard says so.

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

In order to coordinate events, user interactions, script runs, page rendering, network requests, etc., the user agent must describe the Event loop using this section. There are two types of Event loop, one is Browsing Context and the other is Workers.

  • Browsing Context: Event loop based on browser context
  • Workers: Based on event loop in Web Worker

Two types of tasks in the Event Loop

You can see two types of tasks in standard documentation, one called task and one called Microtask.

A task,

An event loop has one or more task queues. A task queue is an ordered list of tasks

The specification states that an event loop has one or more tasks, which are ordered in a queue. Here are a few typical task sources:

  • DOM manipulation
  • User interaction (click events and other actions)
  • Web request (Ajax)
  • Script code (script tasks)
  • setTimeout/setInterval
  • I/O (in node)
  • SetImmediate (nodejs)

Second, the microtask

Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. There are two kinds of microtasks: solitary callback microtasks, and compound microtasks.

It is also stated in the specification that each event loop has only one microtask queue, and microtasks are usually only arranged on the microtask queue, not the task queue. There are two types of microtasks: callback microtasks and compound microtasks. To name a few typical microtasks:

  • promise
  • Promise callbacks (catch and then)
  • Process.nexttick (in node)
  • MutationObserver (new feature, not used myself)

Iii. Event Loop operation mechanism

Before I write that, let me write a few generalizations:

  • Execution order: Task > MicroTask
  • Only one task is executed at a time
  • The microTask queue is empty before the next task is executed

Expressed in pseudocode as:

One task, clear the microtask stack, one task, clear the microtask stack…

Refer to Chapter 8 of the specification for a complete run

Fourth, the example

/ / referred to as"set1
setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')})}, 0) // for shortset2
setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2'}) // For shortset3
    setTimeout(() => {
    	console.log('timer3')
    }, 0)
}, 0)

Promise.resolve().then(function() {
    console.log('promise3')
})

console.log('start')
Copy the code
  • Running process
    • Loop a

      1. Put the script task into the Task queue.

      2, Select a task from the task, run the result set1 and set2 into the task, put promise. Then into the microtask, output start.

      3. Check the Microtask checkpoint to check whether there are tasks in the Microtask queue.

      4. Run all microTask tasks and output promise3.

      5. After clearing the MicroTask queue, proceed to the next loop.

    • Loop 2

      1. Retrieve a set1 task from task, which outputs timer1 and places promise.then on the MicroTask queue.

      2. Check the Microtask checkpoint to check whether there are tasks in the Microtask queue.

      3. Run all microTask tasks and output promise1.

      4. After clearing the MicroTask queue, proceed to the next loop.

    • Cycle three

      1, Retrieve a set2 task from the task, output timer2, place promise. Then into the microtask queue, and place SET3 into the task queue.

      2. Check the Microtask checkpoint to check whether there are tasks in the Microtask queue.

      3. Run all microTask tasks and export promise2.

      4. After clearing the MicroTask queue, proceed to the next loop.

    • Loop four

      Timer3 = timer3; timer3 = timer3; timer3 = timer3

      2. Check the microtask checkpoint to check whether there are no tasks in the microtask queue and proceed to the next loop.

    • Loop five

      Check that both the Task and MicroTask queues are empty, the Closing flag bit in the WorkerGlobalScope object is true, and the Event Loop is destroyed.

  • Output result
start
promise3
timer1
promise1
timer2
promise2
timer3
Copy the code

Event Loop in node

Let’s take a look at the architecture of Node.

Node asynchrony is implemented through the underlying libuv.

What is Libuv

libuv enforces an asynchronous, event-driven style of programming. Its core job is to provide an event loop and callback based notifications of I/O and other activities. libuv offers core utilities like timers, non-blocking networking support, asynchronous file system access, child processes and more.

Libuv uses an asynchronous and event-driven programming style. Its core job is to provide an event-loop with callback functions based on I/O and other event notifications. Libuv also provides some core tools, such as timers, non-blocking network support, asynchronous file system access, child processes, etc.

2. Event loop in libuv

In the official Node doc, El is divided into six phases, as shown below:

When node starts running, it initializes an Event loop, and each event loop contains the following six stages:

  • Timers: This phase performs the callbacks of setTimeout and setInterval.
  • Pending callbacks: AN I/O callback whose execution is deferred until the next iteration.
  • Idle,prepare: Only for internal use.
  • Poll: This process is a bit more complicated and will be covered below.
  • Check: Executes the SetimMediation () callback function.
  • Close callback: Some close callback, such as socket.on(‘close’,…) .

Each phase has a FIFO queue of callbacks, which node will execute when EL reaches a specified phase. When all the callbacks in the queue have been executed or when the maximum number of callbacks executed is reached, EL jumps to the next phase. All of the above phases do not include process.nexttick ().

The entire EL operation process source annotated version:

//deps/uv/src/unix/core.c
int uv_run(uv_loop_t *loop, uv_run_mode mode) {
	int timeout;
	int r;
	int ran_pending;
	//uv__loop_alive returns a handle or request in the event loop that has yet to be processed
	// And whether closing_handles is NULL, if neither, return 0
	r = uv__loop_alive(loop);
	// Update the timestamp of the current Event loop in ms
	if(! r) uv__update_time(loop);while(r ! =0 && loop->stop_flag == 0) {
    	// Update loop->time with hrtime
    	uv__update_time(loop);
    	// Check whether the current loop->time Timer has the highest priority
    	uv__run_timers(loop);
    	Pending_queue (pending_queue); pending_queue (pending_queue); pending_queue (pending_queue)
    	ran_pending = uv__run_pending(loop);
    	// Execute idLE_cd in &loop-> IDLE_handles (if any) at once in loop-watcher.c
    	uv__run_idle(loop);
    	// Execute prepare_cb from &loop->prepare_handles at once (if any) in loop-watcher.
    	uv__run_prepare(loop);

    	timeout = 0;
    	// If the mode is UV_RUN_ONCE and the pending_queue is empty, or if UV_RUN_DEFAULT is used (all events are handled in one loop), set the timeout parameter to
    	// The last timer timeout to prevent blocking in the uv_io_poll from entering the timer timeout
    	if((mode == UV_RUN_ONCE && ! ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop);// Enter the function for I/O processing (the focus of analysis), where timeout is mounted to prevent timers from being blocked in uv_io_poll; And for mode is
    	//UV_RUN_NOWAIT uv_run execution,timeout 0 guarantees that it immediately jumps out of uv__io_poll, which is a non-blocking call
    	uv__io_poll(loop, timeout);
    	// Execute check_cb in &loop->check_handles at once (if any) in loop-watcher.
    	uv__run_check(loop);
    	Loop ->closing_handles pointer to NULL
    	uv__run_closing_handles(loop);

    	if (mode == UV_RUN_ONCE) {
        	// In UV_RUN_ONCE mode, continue to update the timestamp of the current event loop
        	uv__update_time(loop);
        	// Run the timers command to check whether an expired timer exists
        	uv__run_timers(loop);
    	}
    	r = uv__loop_alive(loop);
    	// In UV_RUN_ONCE and UV_RUN_NOWAIT modes, the current loop is broken
    	if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
        	break;
		}
		
	// Flag the current stop_flag as 0, indicating that the current loop is completed
	if(loop->stop_flag ! =0)
    	loop->stop_flag = 0;
	// return r
	return r;
}
Copy the code

You can look at it in conjunction with the six processes above.

Three, poll stage

Timeout is processed before entering the poll phase

      timeout = 0;
    	if((mode == UV_RUN_ONCE && ! ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); uv__io_poll(loop, timeout);Copy the code

Timeout is the second argument to uv__io_poll, and the poll phase is skipped when timeout equals 0.

We can take a look at the source of uv_backend_timeout.

int uv_backend_timeout(const uv_loop_t* loop) {
  if(loop->stop_flag ! = 0)return 0;

  if(! uv__has_active_handles(loop) && ! uv__has_active_reqs(loop))return 0;

  if(! QUEUE_EMPTY(&loop->idle_handles))return 0;

  if(! QUEUE_EMPTY(&loop->pending_queue))return 0;

  if (loop->closing_handles)
    return 0;

  return uv__next_timeout(loop);
}
Copy the code

The above five cases (exit the event loop, there are no asynchronous tasks, IDLE_HANDLES and Pending_queue are not empty, and the loop goes to Closing_handles) return a timeout of 0.

Uv__next_timeout source

int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;

  heap_node = heap_min((const struct heap*) &loop->timer_heap);
  if (heap_node == NULL)
    return- 1; /* block indefinitely */ handle = container_of(heap_node, uv_timer_t, heap_node);if (handle->timeout <= loop->time)
    return0; Diff = handle-> timeout-loop ->time; diff = handle-> timeout-loop ->time; // Cannot be greater than the maximum INT_MAXif (diff > INT_MAX)
    diff = INT_MAX;

  return diff;
}
Copy the code

Diff represents the time to the nearest asynchronous callback. The maximum is 32,767 microseconds. Diff is then passed to the poll phase as the timeout value.

The poll phase has two main functions: 1. Calculate how long the poll phase will be blocked and polling. 2. Handle events in the POLL phase.

When EL enters the poll phase, if timers are not set in the code, one of two things can happen:

  • If the poll queue is not empty, the NUMBER of CB’s in the poll phase is executed until the number of CB’s is empty or the number of CB’s executed reaches the upper limit.

  • If poll is empty, two situations will occur:

    • If the code already has a setImmediate() callback, then the Check phase is entered and the check phase callback is performed
    • If there is no setImmediate() callback in the code, it will block at this stage.

Once the poll phase is empty, EL checks for expired timers and if one or more have arrived, jumps directly to the Timers phase to perform timers callbacks.

With a graph:

4, setImmediate() vs setTimeout()

The usage is similar, with setImmediate entering the check phase and setTimeout entering the timer phase.

Here’s an example from Node docs:

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

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

In multiple executions, the firing order may not be the same:

$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout
Copy the code

When executed in I/O, the order is fixed:

const fs = require('fs');

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

Output results:

$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout
Copy the code
  • Specific reasons:

In Node, timers are accurate to the millisecond level, so setTimeout(cb, 0) === setTimeout(cb, 1). EL initialization takes time, but hrtime is accurate to the nanosecond level, so two things happen to the entire script run:

1. If the loop preparation time exceeds 1ms, uv_run_timers occur when loop->time >=1. 2. If the loop preparation time is less than 1ms, loop->time<1 and uv_run_timers do not take effect, the timers are directly displayed in the later check phase.

In the case of FS, uv__io_poll is directly carried out. After the callback is triggered, check is directly carried out in the timer phase.

Five, the process. NextTick ()

Process.nexttick () does not participate in any phases in Node, but the callbacks in the process.Nexttick () queue need to be cleared each time a phase is switched.

Look at an example:

var fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout((a)= > {
    console.log('setTimeout');
  }, 0);
  setImmediate((a)= > {
    console.log('setImmediate');
    process.nextTick((a)= >{
      console.log('nextTick3'); })}); process.nextTick((a)= >{
    console.log('nextTick1');
  })
  process.nextTick((a)= >{
    console.log('nextTick2'); })});Copy the code

Output result:

nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
Copy the code

The whole cycle: Cycle 1:

1. When it comes in, go directly to the poll phase and perform the callback.

2. SetTimeout: setImmediate 3

3. Execute the nextTick queue first and output nextTick1 and nextTick2.

4. Enter the Check phase, perform the setImmediate callback, and output setImmediate.

5. After executing nextTick queue, output nextTick3.

Loop 2:

1. Enter the timer phase. If the timer expires, output setTimeout.

Refer to the post

The Node.js Event Loop, Timers, and process.nextTick()

HTML Official Standard

Timers, process.nexttick ()