Home  >  Article  >  Web Front-end  >  Summary of the differences between browser and Node event loops (Event Loop)

Summary of the differences between browser and Node event loops (Event Loop)

不言
不言forward
2019-01-15 09:35:122405browse

This article brings you a summary of the differences between the browser and Node's event loop (Event Loop). It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you. help.

In this article we will introduce the principle of asynchronous implementation in JS, and understand that the Event Loop is actually different in the browser and Node.

1. Threads and processes

1. Concept

We often say that JS is executed in a single thread, which means There is only one main thread in a process, so what exactly is a thread? What is a process?

The official statement is: The process is the smallest unit of CPU resource allocation; the thread is the smallest unit of CPU scheduling. These two sentences are not easy to understand. Let’s look at the picture first:

Summary of the differences between browser and Node event loops (Event Loop)

  • The process is like the factory in the picture. Have separate and exclusive factory resources.

  • Threads are like workers in the picture. Multiple workers work collaboratively in a factory. The relationship between the factory and the workers is 1:n. That is to sayA process consists of one or more threads, and threads are different execution routes of code in a process;

  • The factory space is shared by workers , which symbolizes that the memory space of a process is shared, and each thread can use these shared memories.

  • Multiple factories exist independently.
  • 2. Multi-process and multi-thread

    Multi-process: At the same time, if two or more processes are allowed in the same computer system, More than two processes are running. The benefits of multiple processes are obvious. For example, you can open an editor and type code while listening to music, and the processes of the editor and the music-listening software will not interfere with each other at all.
  • Multi-threading: The program contains multiple execution streams, that is, multiple different threads can be run simultaneously in one program to perform different tasks, which means that a single program is allowed to create multiple threads executing in parallel to complete their respective tasks.
  • Take the Chrome browser as an example. When you open a Tab page, you actually create a process. There can be multiple threads in a process (details will be introduced below). For example, rendering thread, JS engine thread, HTTP request thread, etc. When you initiate a request, you actually create a thread. When the request ends, the thread may be destroyed.

2. Browser kernel

Simply speaking, the browser kernel obtains page content, organizes information (applying CSS), calculates and combines the final output of visual image results, which is usually also called for the rendering engine.

The browser kernel is multi-threaded. Under the control of the kernel, each thread cooperates with each other to maintain synchronization. A browser usually consists of the following resident threads:

    GUI rendering Thread
  • JavaScript Engine Thread
  • Timed Trigger Thread
  • Event Trigger Thread
  • Asynchronous http request thread
  • 1. GUI rendering thread

    Mainly responsible for page rendering and parsing HTML , CSS, building DOM tree, layout and drawing, etc.
  • This thread will be executed when the interface needs to be redrawn or when a reflow is caused by some operation.
  • This thread is mutually exclusive with the JS engine thread. When the JS engine thread is executed, GUI rendering will be suspended. When the task queue is idle, the JS engine will execute GUI rendering.
  • 2. JS engine thread

    This thread is of course mainly responsible for processing JavaScript scripts and executing code.
  • is also mainly responsible for executing events that are ready to be executed, that is, when the timer count ends, or when the asynchronous request succeeds and returns correctly, it will enter the task queue in turn and wait for the execution of the JS engine thread. .
  • Of course, this thread is mutually exclusive with the GUI rendering thread. When the JS engine thread executes JavaScript scripts for too long, it will cause page rendering to be blocked.
  • 3. Timer trigger thread

    #The thread responsible for executing functions such as asynchronous timers, such as: setTimeout, setInterval.
  • When the main thread executes the code in sequence, when it encounters a timer, it will hand over the timer to the thread for processing. When the counting is completed, the event triggering thread will add the events after the counting is completed. Go to the end of the task queue and wait for the JS engine thread to execute.
  • 4. The event triggering thread

    is mainly responsible for handing prepared events to the JS engine thread for execution.
  • For example, when the setTimeout timer count ends, ajax waits for the asynchronous request to succeed and triggers the callback function, or when the user triggers a click event, the thread will add the events ready to be sent to the task queue in sequence. At the end, wait for the execution of the JS engine thread.

5. Asynchronous http request thread

    A thread responsible for executing functions such as asynchronous requests, such as: Promise, axios, ajax, etc.
  • When the main thread executes the code in sequence and encounters an asynchronous request, the function will be handed over to the thread for processing. When the status code changes are monitored, if there is a callback function, the event triggering thread will add the callback function. Go to the end of the task queue and wait for the JS engine thread to execute.

3. Event Loop in the browser

1. Micro-Task and Macro-Task

There are two types of asynchronous queues in the event loop : macro (macro task) queue and micro (micro task) queue. There can be multiple macro task queues, but only one micro task queue .

  • Common macro-tasks such as: setTimeout, setInterval, setImmediate, script (overall code), I/O operations, UI rendering, etc.

  • Common micro-task such as: process.nextTick, new Promise().then(callback), MutationObserver(html5 new feature), etc.

2. Event Loop process analysis

A complete Event Loop process can be summarized into the following stages:

Summary of the differences between browser and Node event loops (Event Loop)

  • The execution stack is empty at the beginning. We can think of the execution stack as a stack structure that stores function calls, following the first-in, last-out principle. The micro queue is empty, and there is only one script (the overall code) in the macro queue.

  • The global context (script tag) is pushed into the execution stack and synchronizes code execution. During the execution process, it will be judged whether it is a synchronous task or an asynchronous task. By calling some interfaces, new macro-task and micro-task can be generated, and they will be pushed into their respective task queues. After the synchronization code is executed, the script will be removed from the macro queue. This process is essentially the execution and dequeuing process of the macro-task in the queue.

  • In the previous step we dequeued a macro-task, and in this step we are dealing with a micro-task. But it should be noted that when a macro-task is dequeued, the tasks are executed one by ; while when a micro-task is dequeued, the tasks are executed one team at a time. Therefore, when we process the micro queue, we will execute the tasks in the queue one by one and dequeue them until the queue is cleared.

  • Perform the rendering operation and update the interface

  • Check whether there is a Web worker task, and if so, perform it Processing

  • The above process repeats in a loop until both queues are cleared

Let us summarize, each cycle is a process like this:

Summary of the differences between browser and Node event loops (Event Loop)

When a macro task is executed, it will check whether there is a micro task queue. If there are, all tasks in the microtask queue will be executed first. If not, the top task in the macrotask queue will be read. During the execution of the macrotask, if microtasks are encountered, they will be added to the microtask queue in turn. After the stack is empty, read the tasks in the microtask queue again, and so on.

Next, let’s look at an example to introduce the above process:

Promise.resolve().then(()=>{
  console.log('Promise1')
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})
setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')
  })
},0)

The final output result is Promise1, setTimeout1, Promise2, setTimeout2

  • After the execution of the synchronization task of the stack (this is a macro task) is completed, it will check whether there is a microtask queue, which exists in the above question (there is only one), and then execute all tasks in the microtask queue to output Promise1, and at the same time, it will Generate a macro task setTimeout2

  • and then check the macro task queue. Before macro task setTimeout1, execute macro task setTimeout1 first and output setTimeout1

  • When executing macrotask setTimeout1, microtask Promise2 will be generated and placed in the microtask queue. Then first clear all tasks in the microtask queue and output Promise2

  • After clearing the microtask After all the tasks are in the queue, it will go to the macro task queue to get another one. This time, setTimeout2

will be executed. 4. Event Loop

1 in Node. Introduction to Node

Event Loop in Node is completely different from that in the browser. Node.js uses V8 as the parsing engine of js, and uses its own designed libuv for I/O processing. libuv is an event-driven cross-platform abstraction layer that encapsulates some underlying features of different operating systems and provides a unified API to the outside world. , the event loop mechanism is also implemented in it (will be introduced in detail below).

Summary of the differences between browser and Node event loops (Event Loop)

The operating mechanism of Node.js is as follows:

  • The V8 engine parses JavaScript scripts.

  • The parsed code calls the Node API.

  • The libuv library is responsible for the execution of the Node API. It allocates different tasks to different threads to form an Event Loop, and returns the execution results of the tasks to the V8 engine in an asynchronous manner.

  • V8 引擎再将结果返回给用户。

2. 六个阶段

其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

Summary of the differences between browser and Node event loops (Event Loop)

从上图中,大致看出 node 中的事件循环的顺序:

外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O 事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...

  • timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调

  • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调

  • idle, prepare 阶段:仅 node 内部使用

  • poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里

  • check 阶段:执行 setImmediate() 的回调

  • close callbacks 阶段:执行 socket 的 close 事件回调

注意:上面六个阶段都不包括 process.nextTick()(下文会介绍)

接下去我们详细介绍timerspollcheck这 3 个阶段,因为日常开发中的绝大部分异步任务都是在这 3 个阶段处理的。

(1) timer

timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。
同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行

(2) poll

poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情

  • 回到 timer 阶段执行回调

  • 执行 I/O 回调

并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情

  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

  • 如果 poll 队列为空时,会有两件事发生

    • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调

    • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去

当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

(3) check 阶段

setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后。

我们先来看个例子:

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
  • 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出 start end,并将 2 个 timer 依次放入 timer 队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出 promise3

  • 然后进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;这点跟浏览器端相差比较大,timers 阶段有几个 setTimeout/setInterval 都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于 Node 与浏览器的 Event Loop 差异,下文还会详细介绍)。

3. 注意点

(1) setTimeout 和 setImmediate

二者非常相似,区别主要在于调用时机不同。

  • setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;

  • setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行

setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});
  • 对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的
    进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调

  • 如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了

但当二者在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout

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

在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

(2) process.nextTick

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

五、Node 与浏览器的 Event Loop 差异

浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务

Summary of the differences between browser and Node event loops (Event Loop)

接下我们通过一个例子来说明两者区别:

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

浏览器端运行结果:timer1=>promise1=>timer2=>promise2

浏览器端的处理过程如下:

Summary of the differences between browser and Node event loops (Event Loop)

Node 端运行结果:timer1=>timer2=>promise1=>promise2

  • 全局脚本(main())执行,将 2 个 timer 依次放入 timer 队列,main()执行完毕,调用栈空闲,任务队列开始执行;

  • 首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;

  • 至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 microtask 队列的所有任务,依次打印 promise1、promise2

Node 端的处理过程如下:

Summary of the differences between browser and Node event loops (Event Loop)

六、总结

浏览器和 Node 环境下,microtask 任务队列的执行时机不同

  • Node 端,microtask 在事件循环的各个阶段之间执行

  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

The above is the detailed content of Summary of the differences between browser and Node event loops (Event Loop). For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:segmentfault.com. If there is any infringement, please contact admin@php.cn delete
Previous article:what is jqueryNext article:what is jquery