Why we need to knowjsEvent loops in

Javascript is an event-based, single-threaded, asynchronous, non-blocking programming language. I read about this a lot when I read books or read other people’s blogs, but I never really understood it until NOW, except that javascript uses a lot of callbacks. For example, browser-side event callback (click event, scroll event, etc.), Ajax request callback, setTimeout callback, requestAnimationFrame, Promise callback used in React16 kernel Fiber, Node fs module asynchronous read file content callback, NextTick of the Process module and so on. I’ve had time to browse through a variety of materials recently (see links to various related materials below) and realize that all of these are actually dependent on javascript event loops, and the browser side of the event loop is quite different from the Node side of the event loop, which is described below.

Browser vs Node

Since the introduction of Node, javascript can be run on both the browser side and the server side. Take Chrome for example, the same is based on the V8 engine. The difference is that the browser side implements page rendering, while the Node side provides some features used by the server side, such as FS and Process modules. At the same time, in order to achieve cross-platform, Node uses Libuv to be compatible with Linux, Windows and macOS. So while both implement the asynchronous, non-blocking features of javascript, there are a number of differences.

The browser

No matter on the browser side or node side, thread entry is a javascript script file. The entire script file can be regarded as an entry task, namely initialization task, from the first line to the last line. The first item in the task shown in the following figure is the process. The initialization process is sure to register many asynchronous events, such as setTimeout, onClick, promise, etc., with the possibility of registering more asynchronous events during the execution of these asynchronous events. All of these asynchronous tasks are executed over and over again in the event loop, and these asynchronous tasks fall into two main categories, microTasks and Tasks (or macroTasks). So how many asynchronous tasks are executed in an event loop? What is the order in which microTasks and tasks are executed? See below.

Ignoring the red part in the figure (rendering process, to be introduced later), clockwise is the direction of the event cycle. It can be seen that each cycle will successively execute two types of tasks, task and microtask. Each type of task is composed of a queue, in which task mainly includes the following types of tasks:

  1. index.js(entry)
  2. setTimeout
  3. setInterval
  4. Network I/O

Microtask includes:

  1. promise
  2. MutationObserver

Therefore, the execution event node of microTask is between two task executions. As mentioned above, each type of task consists of a queue, which is actually a producer-consumer model. The registration process of events is the task production process, and the execution process of tasks is the event consumption process. So how many tasks are queued for each class I task? I have indicated in the figure that the task queue queues one task at a time for execution and then executes the microTask. Microtask completes all tasks (including new tasks added to the current MicroTask execution process) each time before continuing to execute the task. That is, even if microTasks are asynchronous, they should not be registered incontinently, otherwise they will block the execution of tasks and page renders. For example, the setTimeout callback in this code will never execute (note that the browser may freeze if you run this code carefully) :

setTimeout((a)= > {
    console.log('run setTimout callback');
}, 0);

function promiseLoop() {
    console.log('run promise callback');
    return Promise.resolve().then((a)= > {
        return promiseLoop();
    });
}

promiseLoop();
Copy the code

Back to the dotted pink line in the image above, this process represents browser rendering processes such as style, layout, and position of DOM elements. Why dotted lines? Because that part of the scheduling is controlled by the browser, and scheduling is 60 hz frequency, is 60 hz is in order to satisfy the human eye visual effects at the same time as far as possible low frequency scheduling, if the browser without stopping frequently rendering, so not only to the human eye to observe the effect is less than the change of the interface (like summer fan turn too fast to the human eye could not tell), And it consumes computing resources. Therefore, the rendering process shown in the figure above is represented by dotted lines and may not be performed in every event loop.

Looking closely at the dotted rendering process, you can see that a callback function, requestAnimationFrame, can be executed before rendering and a callback function, requestIdleCallback, can be executed after rendering. Callbacks registered with these two hook functions enter their own event queues like task and microTask callbacks, but unlike setTimeout, they are not executed after 4ms, 16ms, or 1s. Instead, they are executed during the next page rendering phase. Specifically, requestAnimationFrame is executed before style and Layout calculations, and requestIdleCallback is executed after the changes are actually rendered to the page.

RequestAnimationFrame is better for animation than setTimeout. Here’s an example: jsfiddle.net/H7EEE/245/. As shown below, requestAnimationFrame is much smoother than setTimeout animation.

requestIdleCallback
Task scheduling
react16
reconcilation
requestIdleCallback
API
react
pollyfill
requestAnimationFrame

To summarize, there are four event queues on the browser side: Task queue, requestAnimationFrame queue, requestIdleCallback queue, and MicroTask queue. The first task in the task queue is initialized after the javascript script is loaded. All microTask queue tasks are then executed, followed by a second task queue task again, and so on, interspersed with 60HZ rendering. We now know who is executed first and who is executed later, but how many events are queued for each round of event queue execution? The answer is shown below (screenshot from Jake Archibald’s JSConf talk) :

ordinarytaskOne callback is executed each time the queue exits,requestAnimationFrameAll callbacks to the current queue are executed each time the queue exits (requestIdleCallback),microtaskAll the callback functions of the current queue and the callback functions added to the end of the queue during the execution of their own rounds

Practice a

Not:index.js,promise,setTimeoutOrder of execution
console.log('script start');

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

new Promise(function (resolve) {
    console.log('promise1.1');
    resolve();
}).then(function () {
    console.log('promise1.2');
}).then(function () {
    console.log('promise1.3');
}).then(function () {
    console.log('promise1.4');
});

new Promise(function (resolve) {
    console.log('promise2.1');
    resolve();
}).then(function () {
    console.log('promise2.2');
}).then(function () {
    console.log('promise2.3');
}).then(function () {
    console.log('promise2.4');
});

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

The input to this code is as follows:

Script Start Promise1.1 Promise2.1 Script End Promise1.2 Promise2.2 Promise1.3 Promise2.3 Promise1.4 2.4 setTimeoutCopy the code

Follow the previous example diagram of the event loop in the following order:

  1. performtask(index. Js); There are four outputs:script start,promise1.1,promise2.1,script end. One thing to be aware ofpromise1.1andpromise2.1Because thenew PromiseIn theresolve()The code is also synchronized before the call, so it is executed synchronously.
  2. performmicrotask; So you have to be careful heremicrotaskA new addition to is generated as it executesThe event queueEnd of the line, so execute allmicrotaskBefore re-entering the event loop to start the next item.
  3. performtaskSetTimeout (); Based on the previous example diagram, this is the turn againtaskOnly this time it issetTimout.

The node end

The browser-side event loop involves tasks and microtasks. In fact, asynchronous tasks on the Node side also include these tasks, but the tasks on the Node side are more detailed, as shown in the following figure. Tasks on the Node side can be divided into four types of task queues:

  1. Index. Js (entry),setTimeout,setInterval
  2. Network I/O,fs(disk),child_process
  3. setImmediate
  4. The close event

Microtask includes:

  1. process.nextTick
  2. promise

    All registered entries will be executed firstmicrotaskThe four classes are then executed in turntaskThe queue. And every time you execute onetaskThe queue will continue to executemicrotaskQueue, and then proceed to the next onetaskThe queue. somicrotaskQueue execution is interspersed between class shapestaskBetween, of course.nodeEnd withThe browserOne important difference in the end event loop is thatThe browserendtaskEach round of the queue event loop produces only one callback function to execute and then another to executemicrotaskAnd thenodeEnd just turn to executetask, all the current tasks in the queue will be executed, but the new tasks added to the end of the queue in the current round will be executed in the next round. This mechanism is similar to the browserrequestAnimationFrameThe scheduling mechanism is the same. To summarizenodeEnd event loop, including four classestaskEvent queues with 2 classesmicrotaskEvent queue,microtaskintaskExecute between.taskAll callbacks in the current queue are queued for execution on each turn, andmicrotaskThe scheduling mechanism of theThe browserAll callbacks in the current queue and those added to the end of the queue are executed on each turn. withThe browserThe end is differentnodethemicrotaskincludingprocess.nextTickandpromiseTwo categories.

Practice a

Demo1: Compares the execution order of promise and setTimeout
console.log('main');
setTimeout(function () {
    console.log('execute in first timeout');
    Promise.resolve(3).then(res= > {
        console.log('execute in third promise');
    });
}, 0);
setTimeout(function () {
    console.log('execute in second timeout');
    Promise.resolve(4).then(res= > {
        console.log('execute in fourth promise');
    });
}, 0);
Promise.resolve(1).then(res= > {
    console.log('execute in first promise');
});
Promise.resolve(2).then(res= > {
    console.log('execute in second promise');
});
Copy the code

The output of the previous code is as follows:

main
execute in first promise
execute in second promise
execute in first timeout
execute in second timeout
execute in third promise
execute in fourth promise
Copy the code

The execution sequence is as follows:

  1. Index.js (main program code);
  2. Microtask (promise1 promise2);
  3. Task (setTimeout1 setTimeout2);
  4. Microtask (promise3 promise4);

This order of execution corresponds exactly to the previous diagram.

Demo2: contrastindex.js,promise,async await,setTimeoutOrder of execution
console.log('script start');

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
    console.log('entry async2');
    return Promise.resolve();
}

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

async1();

new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
}).then(function () {
    console.log('promise3');
}).then(function () {
    console.log('promise4');
}).then(function () {
    console.log('promise5');
}).then(function () {
    console.log('promise6');
});

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

This code executes on node10 as follows:

script start
async1 start
entry async2
promise1
script end
promise2
promise3
promise4
async1 end
promise5
promise6
setTimeout
Copy the code

The async await on node8 and node9 is bug-free. The async await on node10 is fixed. Here is the result of the previous code execution as shown in the previous event loop example graph:

  1. performtask(index. Js); There are five outputs:script start,async1 start,entry async2,promise1,script end. Notice hereasyncThe first one in the functionawaitThe code that was executed before is also synchronous code, so it is printed outscync1 startAs well asentry async2.
  2. performmicrotask; This is where you print all the restpromiseAnd aawaitAfter the statement ofasync1 end.Printing this collection is definitely fine, but the question is whyasync1 endthanpromiseThree delays?This question is the hardest part about this code, and the answer is in the article just mentioned: everyawaitYou need at least 3microtask queue ticksSo hereasync1 endPrint relative topromiseThree were printed latetick. In fact, through this example we should also come to a conclusion, is the most should notpromiseandasync awaitMix it up, otherwise it’s easy to mess up the timing.
  3. performtaskSetTimeout (). Based on the previous example diagram, it is the task execution turn again, only this time it is setTimout. fromdemo2You can see that althoughasync awaitIn essencemicrotaskBut everyawaitIt’s going to take at least threemicrotask queue ticks“, this needs to be noted.

reference

This summary mainly refers to the following resources, which are highly recommended for reading:

  1. Jake Archibald: In The Loop – JSConf.Asia 2018
  2. Philip Roberts: What the heck is the event loop anyway? – JSConf.EU 2014
  3. Everything You Need to Know About Node.js Event Loop – Bert Belder, IBM
  4. Event Loop and the Big Picture — NodeJS Event Loop Part 1
  5. Timers, Immediates and Process.nextTick – NodeJS Event Loop Part 2
  6. Promises, Next-Ticks and Immediates — NodeJS Event Loop Part 3
  7. Handling IO — NodeJS Event Loop Part 4
  8. Event Loop Best Practices — NodeJS Event Loop Part 5
  9. Using requestIdleCallback