Home  >  Article  >  Web Front-end  >  Let's talk about the principles and examples of JavaScript event loop

Let's talk about the principles and examples of JavaScript event loop

WBOY
WBOYforward
2022-11-10 17:27:492157browse

This article brings you relevant knowledge about JavaScript, which mainly introduces the relevant content of the event loop. Let’s take a look at it together. I hope it will be helpful to everyone.

Let's talk about the principles and examples of JavaScript event loop

[Related recommendations: JavaScript video tutorial, web front-end]

Understand JavaScript The event loop is often accompanied by related issues such as macrotasks and microtasks, JavaScript single-threaded execution process, and browser asynchronous mechanisms. The event loop implementations in browsers and NodeJS are also very different. Being familiar with the event loop and understanding the browser operating mechanism will be of great help to us in understanding the execution process of JavaScript and troubleshooting code running problems.

The principle of asynchronous execution of browser JS

JS is single-threaded, that is, it can only do one thing at the same time, so think about: why the browser can execute asynchronously at the same time What about the mission?

Because the browser is multi-threaded, when JS needs to perform an asynchronous task, the browser will start another thread to perform the task. In other words, "JS is single-threaded" means that there is only one thread that executes JS code, which is the JS engine thread (main thread) provided by the browser. There are also timer threads and HTTP request threads in the browser. These threads are not mainly used to run JS code.

For example, if you need to send an AJAX request in the main thread, this task is handed over to another browser thread (HTTP request thread) to actually send the request. When the request comes back, the JS that needs to be executed in the callback The callback is handed over to the JS engine thread for execution. **That is, the browser is the one who actually performs the task of sending the request, and JS is only responsible for performing the final callback processing. **So the asynchronous here is not implemented by JS itself, but is actually the capability provided by the browser.

Take Chrome as an example. The browser not only has multiple threads, but also multiple processes, such as rendering process, GPU process, plug-in process, etc. Each tab page is an independent rendering process, so if one tab crashes abnormally, other tabs will basically not be affected. As a front-end developer, we mainly focus on the rendering process. The rendering process includes JS engine threads, HTTP request threads and timer threads, etc. These threads provide the basis for JS to complete asynchronous tasks in the browser.

Event-driven analysis

The execution principle of browser asynchronous tasks is actually a set of event-driven mechanisms. Event triggering, task selection and task execution are all accomplished by event-driven mechanisms. The design of NodeJS and browsers are event-driven. In short, specific tasks are triggered by specific events. The events here can be triggered by user operations, such as click events; they can also be automatically triggered by programs. , for example, the timer thread in the browser will trigger the timer event after the timer ends. The theme of this articleThe event loop is actually a set of processes for managing and executing events in an event-driven model.

Take a simple scene as an example. Suppose there is a move button and a character model on the game interface. Each time you click to move right, the position of the character model needs to be re-rendered and moved to the right by 1 pixel. We can implement it in different ways depending on the rendering timing.

#Implementation method one: event driven. After clicking the button, when the coordinate positionX is modified, the interface rendering event is immediately triggered and re-rendering is triggered.

Implementation method two: state-driven or data-driven. After clicking the button, only the coordinate positionX is modified and interface rendering is not triggered. Before this, a timer setInterval will be started, or requestAnimationFrame will be used to continuously detect whether positionX changes. If there are changes, re-render immediately.

Click event processing in browsers is also typically event-driven. In an event-driven system, when an event is triggered, the triggered events will be temporarily stored in a queue in order. After the JS synchronization task is completed, the events to be processed will be taken out of this queue and processed. So when to fetch tasks and which tasks to fetch first, this is controlled by the event loop process.

Event loop in the browser

Execution stack and task queue

When JS parses a piece of code, the synchronization code will be arranged somewhere in order, that is, the execution stack, and then Execute the functions inside in sequence. When an asynchronous task is encountered, it is handed over to other threads for processing. After all the synchronization codes in the current execution stack are executed, the callbacks of the completed asynchronous tasks will be taken out from a queue and added to the execution stack to continue execution. When an asynchronous task is encountered, it will be processed again. Hand it over to other threads, and so on. After other asynchronous tasks are completed, the callback is placed in the task queue to be taken out of the execution stack for execution.

JS executes the methods in the execution stack in sequence. Each time a method is executed, a unique execution environment (context) will be generated for this method. After the execution of this method is completed, the current execution environment will be destroyed. , and pop this method from the stack (that is, consumption is completed), and then continue to the next method.

It can be seen that in the event-driven mode, at least one execution loop is included to detect whether there are new tasks in the task queue. By continuously looping to take out the asynchronous callback for execution, this process is an event loop, and each loop is an event cycle or a tick.

Macro tasks and micro tasks

There is more than one task queue. Depending on the type of task, it can be divided into micro task queue and macro task queue.

During the event loop, after the execution of the synchronization code is completed, the execution stack first checks whether there are tasks in the microtask queue that need to be executed. If not, then go to the macrotask queue to check whether there are tasks to be executed, and so on. Microtasks are generally executed first in the current cycle, while macrotasks will wait until the next cycle. Therefore, microtasks are generally executed before macrotasks, and there is only one microtask queue, and there may be multiple macrotask queues. In addition, our common click and keyboard events also belong to macro tasks.

Let’s take a look at common macro tasks and common micro tasks.

Common macro tasks:

  • setTimeout()
  • setInterval()
  • setImmediate()

Common microtasks:

  • promise.then(), promise.catch()
  • new MutaionObserver()
  • process.nextTick()
console.log('同步代码1');setTimeout(() => {    console.log('setTimeout')
}, 0)new Promise((resolve) => {  console.log('同步代码2')  resolve()
}).then(() => {    console.log('promise.then')
})console.log('同步代码3');// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"

The above code will be output in the following order: "Synchronization code 1", "Synchronization code 2", "Synchronization code 3", "promise.then", "setTimeout", detailed analysis is as follows.

(1) The setTimeout callback and promise.then are executed asynchronously and will be executed after all synchronous code;

By the way, setTimeout in the browser If the delay is set to 0, it will default to 4ms and NodeJS is 1ms. The exact value may vary, but it is not 0.

(2) Although promise.then is written later, the execution order takes precedence over setTimeout because it is a microtask;

(3) new Promise is synchronized When executed, the callback in promise.then is asynchronous.

Let’s take a look at the execution process demonstration of the above code:

Some people also understand it this way: microtasks are executed at the end of the current event loop; The macro task is executed at the beginning of the next event loop. Let’s take a look at the essential difference between microtasks and macrotasks.

We already know that when JS encounters an asynchronous task, it will hand over the task to other threads for processing, and its main thread will continue to perform synchronous tasks. For example, the timing of setTimeout will be handled by the browser's timer thread. When the timing ends, the timer callback task will be placed in the task queue and wait for the main thread to take it out for execution. As we mentioned earlier, because JS is executed in a single thread, to perform asynchronous tasks, other browser threads are required to assist. That is, multi-threading is an obvious feature of JS asynchronous tasks.

Let’s analyze the processing of promise.then (microtask). When promise.then is executed, the V8 engine will not hand over the asynchronous task to other browser threads. Instead, it will store the callback in its own queue. After the execution of the current execution stack is completed, it will immediately execute the queue where promise.then is stored. , promise.then microtasks do not involve multi-threading. Even from some perspectives, microtasks cannot be completely asynchronous. It just changes the execution order of the code when written.

setTimeout has the task of "timing waiting", which needs to be executed by the timer thread; the ajax request has the task of "sending a request", which needs to be executed by the HTTP thread, while promise.then does not have any asynchronous tasks that need to be executed by other threads. It only has callbacks, and even if there are any, they're just another macro task nested inside.

Briefly summarize the essential differences between microtasks and macrotasks.

  • Macro task features: There are clear asynchronous tasks that need to be executed and callbacks; support from other asynchronous threads is required.
  • Microtask features: There are no clear asynchronous tasks to be executed, only callbacks; no other asynchronous thread support is required.

Timer Error

In the event loop, the synchronous code is always executed first, and then the asynchronous callback is fetched from the task queue for execution. When setTimeout is executed, the browser starts a new thread for timing. After the timing is over, the timer event is triggered and the callback is stored in the macro task queue, waiting for the JS main thread to take out the execution. If the main thread is still executing the synchronization task at this time, then the macro task at this time will have to be suspended first, which will cause the problem of inaccurate timer. The longer the synchronization code takes, the greater the error in the timer. Not only the synchronization code, because the microtasks will be executed first, the microtasks will also affect the timing. If there is an infinite loop in the synchronization code or the recursion in the microtask is constantly starting other microtasks, then the code in the macrotask may never be obtained. implement. Therefore, it is very important to improve the execution efficiency of the main thread code.

#A very simple scenario is that there is a clock on our interface that is accurate to seconds and updates the time every second. You'll notice that sometimes the seconds just skip the 2 second interval, and that's why.

View update rendering

After the microtask queue is executed, that is, after an event loop ends, the browser will perform view rendering. Of course, there will be browser optimization here, and multiple processes may be merged. The result of this loop is a view redraw, so the view is updated after the event loop, so not every operation on the Dom will necessarily refresh the view immediately. The requestAnimationFrame callback will be executed before the view is redrawn, so it is controversial whether requestAnimationFrame is a microtask or a macrotask. From here, it should be neither a microtask nor a macrotask.

Event loop in NodeJS

The JS engine itself does not implement the event loop mechanism, which is implemented by its host. The event loop in the browser is mainly implemented by the browser, and NodeJS also has its own event loop implementation. In NodeJS, the process of cyclic task queue and micro-tasks are prioritized over macro-tasks, and the general performance is consistent with the browser. However, it also has some differences from the browser, and some new task types and task stages have been added. Next, we introduce the event loop process in NodeJS.

Asynchronous methods in NodeJS

Because they are all based on the V8 engine, the asynchronous methods included in the browser are also the same in NodeJS. There are also some other common forms of asynchronous in NodeJS.

  • File I/O: Load local files asynchronously.
  • setImmediate(): Similar to setTimeout setting 0ms, it will be executed immediately after certain synchronization tasks are completed.
  • process.nextTick(): Executed immediately after certain synchronization tasks are completed.
  • server.close, socket.on('close',...), etc.: close callback.

Imagine, if the above form exists at the same time as setTimeout, promise, etc., how to analyze the execution order of the code? As long as we understand the event loop mechanism of NodeJS, it will be clear.

Event Loop Model

NodeJS’s cross-platform capabilities and event loop mechanism are all implemented based on the Libuv library. You don’t need to care about the specific content of this library. We only need to know that the Libuv library is event-driven and encapsulates and unifies API implementations on different platforms.

In NodeJS, the V8 engine parses the JS code and calls the Node API. The Node API then hands over the task to Libuv for allocation, and finally returns the execution results to the V8 engine. A set of event loop processes are implemented in Libux to manage the execution of these tasks, so the event loop of NodeJS is mainly completed in Libuv.

Let’s take a look at what the loop in Libuv looks like.

Each stage of the event loop

In the execution of JS in NodeJS, the process we mainly need to care about is divided into the following stages. Each stage below has its own separate task queue. When executing When the corresponding stage is reached, it is judged whether there are tasks that need to be processed in the task queue of the current stage.

  • timers 阶段:执行所有 setTimeout() 和 setInterval() 的回调。
  • pending callbacks 阶段:某些系统操作的回调,如  TCP  链接错误。除了 timers、close、setImmediate 的其他大部分回调在此阶段执行。
  • poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等。V8 引擎将 JS 代码解析并传入 Libuv 引擎后首先进入此阶段。如果此阶段任务队列已经执行完了,则进入 check 阶段执行 setImmediate 回调(如果有 setImmediate),或等待新的任务进来(如果没有 setImmediate)。在等待新的任务时,如果有 timers 计时到期,则会直接进入 timers 阶段。此阶段可能会阻塞等待。
  • check 阶段:setImmediate 回调函数执行。
  • close callbacks 阶段:关闭回调执行,如 socket.on('close', ...)。

上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段。这里也是与浏览器中逻辑差异较大的地方,不过浏览器不用区分这些阶段,也少了很多异步操作类型,所以不用刻意去区分两者区别。代码如下所示:

const fs = require('fs');
fs.readFile(__filename, (data) => {    // poll(I/O 回调) 阶段
    console.log('readFile')    Promise.resolve().then(() => {        console.error('promise1')
    })    Promise.resolve().then(() => {        console.error('promise2')
    })
});setTimeout(() => {    // timers 阶段
    console.log('timeout');    Promise.resolve().then(() => {        console.error('promise3')
    })    Promise.resolve().then(() => {        console.error('promise4')
    })
}, 0);// 下面代码只是为了同步阻塞1秒钟,确保上面的异步任务已经准备好了var startTime = new Date().getTime();var endTime = startTime;while(endTime - startTime < 1000) {
    endTime = new Date().getTime();
}// 最终输出 timeout promise3 promise4 readFile promise1 promise2

另一个与浏览器的差异还体现在同一个阶段里的不同任务执行,在 timers 阶段里面的宏任务、微任务测试代码如下所示:

setTimeout(() => {  console.log('timeout1')    Promise.resolve().then(function() {    console.log('promise1')
  })
}, 0);setTimeout(() => {  console.log('timeout2')    Promise.resolve().then(function() {    console.log('promise2')
  })
}, 0);
  • 浏览器中运行

    每次宏任务完成后都会优先处理微任务,输出“timeout1”、“promise1”、“timeout2”、“promise2”。

  • NodeJS 中运行

    因为输出 timeout1 时,当前正处于  timers 阶段,所以会先将所有 timer 回调执行完之后再执行微任务队列,即输出“timeout1”、“timeout2”、“promise1”、“promise2”。

上面的差异可以用浏览器和 NodeJS 10 对比验证。是不是感觉有点反程序员?因此 NodeJS 在版本 11 之后,就修改了此处逻辑使其与浏览器尽量一致,也就是每个 timer 执行后都先去检查一下微任务队列,所以 NodeJS 11 之后的输出已经和浏览器一致了。

nextTick、setImmediate 和 setTimeout

实际项目中我们常用 Promise 或者 setTimeout 来做一些需要延时的任务,比如一些耗时计算或者日志上传等,目的是不希望它的执行占用主线程的时间或者需要依赖整个同步代码执行完成后的结果。

NodeJS 中的 process.nextTick() 和 setImmediate() 也有类似效果。其中 setImmediate() 我们前面已经讲了是在 check 阶段执行的,而 process.nextTick() 的执行时机不太一样,它比 promise.then() 的执行还早,在同步任务之后,其他所有异步任务之前,会优先执行 nextTick。可以想象是把 nextTick 的任务放到了当前循环的后面,与 promise.then() 类似,但比 promise.then() 更前面。意思就是在当前同步代码执行完成后,不管其他异步任务,先尽快执行 nextTick。如下面的代码,因此这里的 nextTick 其实应该更符合“setImmediate”这个命名才对。

setTimeout(() => {    console.log('timeout');
}, 0);Promise.resolve().then(() => {    console.error('promise')
})
process.nextTick(() => {    console.error('nextTick')
})// 输出:nextTick、promise、timeout

接下来我们再来看看 setImmediate 和 setTimeout,它们是属于不同的执行阶段了,分别是 timers 阶段和 check 阶段。

setTimeout(() => {  console.log('timeout');
}, 0);setImmediate(() => {  console.log('setImmediate');
});// 输出:timeout、 setImmediate

分析上面代码,第一轮循环后,分别将 setTimeout   和 setImmediate 加入了各自阶段的任务队列。第二轮循环首先进入  timers 阶段,执行定时器队列回调,然后  pending callbacks 和 poll 阶段没有任务,因此进入check 阶段执行 setImmediate 回调。所以最后输出为“timeout”、“setImmediate”。当然这里还有种理论上的极端情况,就是第一轮循环结束后耗时很短,导致 setTimeout 的计时还没结束,此时第二轮循环则会先执行 setImmediate 回调。

再看这下面一段代码,它只是把上一段代码放在了一个 I/O 任务回调中,它的输出将与上一段代码相反。

const fs = require('fs');
fs.readFile(__filename, (data) => {    console.log('readFile');    setTimeout(() => {        console.log('timeout');
    }, 0);    setImmediate(() => {        console.log('setImmediate');
    });
});// 输出:readFile、setImmediate、timeout

如上面代码所示:

  • There is no asynchronous task queue that needs to be executed in the first round of the cycle;
  • There are no tasks in the timers and other stages of the second round of the cycle. Only the poll stage has I/O callback tasks, that is, "readFile" is output. ;
  • Refer to the previous description of the event stage. Next, the poll stage will detect if there is a setImmediate task queue and enter the check stage. Otherwise, it will be judged. If there is a timer task callback, it will return to the timers stage. Therefore, you should enter the check phase to execute setImmediate and output "setImmediate";
  • Then enter the final close callbacks phase, and this cycle ends;
  • Finally proceed to the third round of loop, enter the timers phase, and output "timeout".

So the final output of "setImmediate" is before "timeout". It can be seen that the execution order of the two is related to the current execution stage.

【Related recommendations: JavaScript video tutorial, web front-end

The above is the detailed content of Let's talk about the principles and examples of JavaScript event loop. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.im. If there is any infringement, please contact admin@php.cn delete