Home  >  Article  >  Web Front-end  >  A combination of pictures and text will help you understand the event loop in Nodejs

A combination of pictures and text will help you understand the event loop in Nodejs

青灯夜游
青灯夜游forward
2021-09-30 10:22:382247browse

This article uses a combination of pictures and text to help you understand the event loop in Nodejs. I hope it will be helpful to everyone!

A combination of pictures and text will help you understand the event loop in Nodejs

The following full text is 7,000 words, please read it when you have clear thoughts and are full of energy. I guarantee you won’t forget it for a long time after you understand it. [Recommended learning: "nodejs Tutorial"]

A combination of pictures and text will help you understand the event loop in Nodejs

##Node event loop

The language used at the bottom of Node

libuv is a c language. It is used to operate the underlying operating system and encapsulates the interface of the operating system. Node's event loop is also written using libuv, so the Node life cycle is still different from that of the browser.

Because Node deals with the operating system, the event loop is relatively complex and has some unique APIs of its own.

The event loop has some subtle differences in different operating systems. This will involve knowledge of the operating system, which is not listed for the time being. This time we only introduce the operation process of Node in the JS main thread. Node's other threads will not be expanded for the time being.

Event Loop Diagram

A good diagram is not a secret. Once you understand the picture below, you will learn the event loop.

A combination of pictures and text will help you understand the event loop in Nodejs

Event Loop Diagram

A combination of pictures and text will help you understand the event loop in Nodejs

Event Loop Diagram-Structure

In order to give everyone a

Overall view, first post a directory structure diagram in front:

A combination of pictures and text will help you understand the event loop in Nodejs

Directory

Then let’s talk about it in detail

Main thread

A combination of pictures and text will help you understand the event loop in Nodejs

Main thread

In the picture above, the meanings of several color blocks :

  • main: Start the entry file and run the main function
  • event loop: Check whether to enter the event loop
      Check whether there are still pending items in other threads
    • Check whether other tasks are still in progress (such as whether timers, file reading operations and other tasks are completed)
    • In the above situation, enter Event loop, running other tasks
    • The process of event loop: follow the process from timers to close callbacks and walk in a circle. Go to the event loop to see if it is over. If not, go around again.
  • over: All things are completed, the end

Event loop circle

A combination of pictures and text will help you understand the event loop in Nodejs

Event Loop Circle

The gray circle in the picture is related to the operating system and is not the focus of this chapter. Focus on the yellow and orange circles and the orange box in the middle.

We call each cycle of event loop "a loop", also called "a poll", also called "a Tick".

A cycle goes through six stages:

  • timers: timers (callback functions such as setTimeout, setInterval, etc. are stored inside)

  • ##pending callback
  • idle prepare
  • poll: Polling queue (callbacks other than timers and check are stored here)

  • check: Checking phase (callbacks using setImmediate will enter this queue directly)

  • close callbacks

A combination of pictures and text will help you understand the event loop in NodejsThis time we only focus on the three key points marked in red above.

Working principle

  • Each stage maintains an event queue. You can think of each circle as an event queue.
  • This is different from the browser. The browser has up to two queues (macro queue and micro queue). But there are six queues in node
  • After arriving at a queue, check whether there is a task in the queue (that is, see if there is a callback function) that needs to be executed. If there are, execute them in sequence until all executions are completed and the queue is cleared.
  • If there is no task, enter the next queue to check. Until all queues are checked, it is counted as a poll.
  • Among them, timers, pending callback, idle prepare, etc. will arrive at the poll queue after completion of execution.

The working principle of timers queue

timers is not a real queue, it is stored internally It's a timer.
Every time this queue is reached, all timers in the timer thread will be checked. Multiple timers within the timer thread are sorted in chronological order.

Checking process: Calculate each timer in sequence, and calculate whether the time from when the timer starts counting to the current time meets the timer interval parameter setting (for example, 1000ms, calculate when the timer starts counting to Is there 1m now). When a timer check passes, its callback function is executed.

How the poll queue operates

  • If there are callback functions in poll that need to be executed, the callbacks will be executed in sequence , until the queue is cleared.
  • If there is no callback function in poll that needs to be executed, the queue is already empty. It will wait here for callbacks to appear in other queues.
    • If callbacks appear in other queues, it will go from poll downward to over, end this stage, and enter the next stage.
    • If there is no callback in other queues, continue to wait in the poll queue until a callback occurs in any queue before starting work. (It’s the way a lazy person does things)

Take an example to sort out the event process

setTimeout(() => {
  console.log('object');
}, 5000)
console.log('node');

The event flow of the above code is sorted out

  • Enter the main thread, execute setTimeout(), and the callback function is used as an asynchronous task Put it into the asynchronous queue timers queue and not execute it temporarily.
  • Continue down, execute the console behind the timer, and print "node".
  • Determine whether there is an event loop. Yes, go through a circle of polling: from timers - pending callback - idle prepare...
  • to the poll queue, stop the loop and wait.
    • Since it has not been 5 seconds at this time, the timers queue has no tasks, so it has been stuck in the poll queue, and at the same time it polls to check whether there are tasks in other queues.
  • Wait 5 seconds to arrive. The setTimeout callback is stuffed into timers. Routine polling checks that there are tasks in the timers queue, and then goes down, and reaches timers after checking and closing callbacks. Clear the timers queue.
  • Continue to poll and wait, and ask if the event loop is still needed. If not, it will reach the end of over.

To understand this problem, look at the code and process analysis below:

setTimeout(function t1() {
  console.log('setTimeout');
}, 5000)
console.log('node 生命周期');

const http = require('http')

const server = http.createServer(function h1() {
  console.log('请求回调');
});

server.listen(8080)

The code analysis is as follows :

  • As usual, execute the main thread first, print the "node life cycle", introduce http and then create the http service.
  • Then the event loop checks whether there are asynchronous tasks and finds that there are timer tasks and request tasks. So enter the event loop.
  • If there are no tasks in the six queues, wait in the poll queue. As shown below:

    A combination of pictures and text will help you understand the event loop in Nodejs

  • After five seconds, there is a task in the timers, and the process starts from poll to release downward, after passing through the check and close callbacks queues , reaching the event loop.
  • The event loop checks whether there are asynchronous tasks and finds that there are timer tasks and request tasks. So enter the event loop again.
  • Arrive at the timers queue and find that there is a callback function task, then execute the callbacks in sequence, clear the timers queue (of course there is only one callback that arrives after 5 seconds, so just execute it directly), print out " setTimeout". As shown in the figure below

    A combination of pictures and text will help you understand the event loop in Nodejs

  • After clearing the timers queue, polling continues down to the poll queue. Since the poll queue is now an empty queue, it waits here.
  • Later, assuming a user request is sent, the h1 callback function is placed in the poll queue. So there are callback functions in poll that need to be executed, and the callbacks are executed in sequence until the poll queue is cleared.
  • The poll queue is cleared. At this time, the poll queue is empty and continues to wait.

    A combination of pictures and text will help you understand the event loop in Nodejs

  • 由于node线程一直holding在poll队列,等很长一段时间还是没有任务来临时,会自动断开等待(不自信表现),向下执行轮询流程,经过check、close callbacks后到达event loop
  • 到了event loop后,检查是否有异步任务,检查发现有请求任务。(此时定时器任务已经执行完毕,所以没有了),则继续再次进入事件循环。
  • 到达poll队列,再次holding……
  • 再等很长时间没有任务来临,自动断开到even loop(再补充一点无任务的循环情况)
  • 再次回到poll队列挂起
  • 无限循环……

梳理事件循环流程图:

注意:下图中的“是否有任务”的说法表示“是否有本队列的任务”。

1A combination of pictures and text will help you understand the event loop in Nodejs

event loop流程梳理

再用一个典型的例子验证下流程:

const startTime = new Date();

setTimeout(function f1() {
  console.log('setTimeout', new Date(), new Date() - startTime);
}, 200)

console.log('node 生命周期', startTime);

const fs = require('fs')

fs.readFile('./poll.js', 'utf-8', function fsFunc(err, data) {
  const fsTime = new Date()
  console.log('fs', fsTime);
  while (new Date() - fsTime < 300) {
  }
  console.log(&#39;结束死循环&#39;, new Date());
});

连续运行三遍,打印结果如下:

1A combination of pictures and text will help you understand the event loop in Nodejs

执行流程解析:

  • 执行全局上下文,打印「node 生命周期 + 时间」

  • 询问是否有event loop

  • 有,进入timers队列,检查没有计时器(cpu处理速度可以,这时还没到200ms)

  • 轮询进入到poll,读文件还没读完(比如此时才用了20ms),因此poll队列是空的,也没有任务回调

  • 在poll队列等待……不断轮询看有没有回调

  • 文件读完,poll队列有了fsFunc回调函数,并且被执行,输出「fs + 时间」

  • 在while死循环那里卡300毫秒,

  • 死循环卡到200ms的时候,f1回调进入timers队列。但此时poll队列很忙,占用了线程,不会向下执行。

  • 直到300ms后poll队列清空,输出「结束死循环 + 时间」

  • event loop赶紧向下走

  • 再来一轮到timers,执行timers队列里的f1回调。于是看到「setTimeout + 时间」

  • timers队列清空,回到poll队列,没有任务,等待一会。

  • 等待时间够长后,向下回到event loop。

  • event loop检查没有其他异步任务了,结束线程,整个程序over退出。

check 阶段

检查阶段(使用 setImmediate 的回调会直接进入这个队列)

check队列的实际工作原理

真正的队列,里边扔的就是待执行的回调函数的集合。类似[fn,fn]这种形式的。
每次到达check这个队列后,立即按顺序执行回调函数即可【类似于[fn1,fn2].forEach((fn)=>fn())的感觉】

所以说,setImmediate不是一个计时器的概念。

如果你去面试,涉及到Node环节,可能会遇到下边这个问题:setImmediate和setTimeout(0)谁更快。

setImmediate() 与 setTimeout(0) 的对比

  • setImmediate的回调是异步的,和setTimeout回调性质一致。
  • setImmediate回调在check队列,setTimeout回调在timers队列(概念意义,实际在计时器线程,只是setTimeout在timers队列做检查调用而已。详细看timers的工作原理)。
  • setImmediate函数调用后,回调函数会立即push到check队列,并在下次eventloop时被执行。setTimeout函数调用后,计时器线程增加一个定时器任务,下次eventloop时会在timers阶段里检查判断定时器任务是否到达时间,到了则执行回调函数。
  • 综上,setImmediate的运算速度比setTimeout(0)的要快,因为setTimeout还需要开计时器线程,并增加计算的开销。

二者的效果差不多。但是执行顺序不定

观察以下代码:

setTimeout(() => {
  console.log(&#39;setTimeout&#39;);
}, 0);

setImmediate(() => {
  console.log(&#39;setImmediate&#39;);
});

多次反复运行,执行效果如下:

1A combination of pictures and text will help you understand the event loop in Nodejs

顺序不定

可以看到多次运行,两句console.log打印的顺序不定。
这是因为setTimeout的间隔数最小填1,虽然下边代码填了0。但实际计算机执行当1ms算。(这里注意和浏览器的计时器区分。在浏览器中,setInterval的最小间隔数为10ms,小于10ms则会被设置为10;设备供电状态下,间隔最小为16.6ms。)

以上代码,主线程运行的时候,setTimeout函数调用,计时器线程增加一个定时器任务。setImmediate函数调用后,其回调函数立即push到check队列。主线程执行完毕。

eventloop判断时,发现timers和check队列有内容,进入异步轮询:

第一种情况:等到了timers里这段时间,可能还没有1ms的时间,定时器任务间隔时间的条件不成立所以timers里还没有回调函数。继续向下到了check队列里,这时候setImmediate的回调函数早已等候多时,直接执行。而再下次eventloop到达timers队列,定时器也早已成熟,才会执行setTimeout的回调任务。于是顺序就是「setImmediate -> setTimeout」。

第二种情况:但也有可能到了timers阶段时,超过了1ms。于是计算定时器条件成立,setTimeout的回调函数被直接执行。eventloop再向下到达check队列执行setImmediate的回调。最终顺序就是「setTimeout -> setImmediate」了。

所以,只比较这两个函数的情况下,二者的执行顺序最终结果取决于当下计算机的运行环境以及运行速度。

二者时间差距的对比代码

------------------setTimeout测试:-------------------
let i = 0;
console.time(&#39;setTimeout&#39;);
function test() {
  if (i < 1000) {
    setTimeout(test, 0)
    i++
  } else {
    console.timeEnd(&#39;setTimeout&#39;);
  }
}
test();

------------------setImmediate测试:-------------------
let i = 0;
console.time(&#39;setImmediate&#39;);
function test() {
  if (i < 1000) {
    setImmediate(test)
    i++
  } else {
    console.timeEnd(&#39;setImmediate&#39;);
  }
}
test();

运行观察时间差距:

1A combination of pictures and text will help you understand the event loop in Nodejs

setTimeout与setImmediate时间差距

可见setTimeout远比setImmediate耗时多得多
这是因为setTimeout不仅有主代码执行的时间消耗。还有在timers队列里,对于计时器线程中各个定时任务的计算时间。

结合poll队列的面试题(考察timers、poll和check的执行顺序)

如果你看懂了上边的事件循环图,下边这道题难不倒你!

// 说说下边代码的执行顺序,先打印哪个?
const fs = require(&#39;fs&#39;)
fs.readFile(&#39;./poll.js&#39;, () => {
  setTimeout(() => console.log(&#39;setTimeout&#39;), 0)
  setImmediate(() => console.log(&#39;setImmediate&#39;))
})

上边这种代码逻辑,不管执行多少次,肯定都是先执行setImmediate。

1A combination of pictures and text will help you understand the event loop in Nodejs

先执行setImmediate

因为fs各个函数的回调是放在poll队列的。当程序holding在poll队列后,出现回调立即执行。
回调内执行setTimeout和setImmediate的函数后,check队列立即增加了回调。
回调执行完毕,轮询检查其他队列有内容,程序结束poll队列的holding向下执行。
check是poll阶段的紧接着的下一个。所以在向下的过程中,先执行check阶段内的回调,也就是先打印setImmediate。
到下一轮循环,到达timers队列,检查setTimeout计时器符合条件,则定时器回调被执行。

nextTick 与 Promise

说完宏任务,接下来说下微任务

  • 二者都是「微队列」,执行异步微任务。
  • 二者不是事件循环的一部分,程序也不会开启额外的线程去处理相关任务。(理解:promise里发网络请求,那是网络请求开的网络线程,跟Promise这个微任务没关系)
  • 微队列设立的目的就是让一些任务「马上」、「立即」优先执行。
  • nextTick与Promise比较,nextTick的级别更高。

nextTick表现形式

process.nextTick(() => {})

Promise表现形式

Promise.resolve().then(() => {})

如何参与事件循环?

事件循环中,每执行一个回调前,先按序清空一次nextTick和promise。

// 先思考下列代码的执行顺序
setImmediate(() => {
  console.log(&#39;setImmediate&#39;);
});

process.nextTick(() => {
  console.log(&#39;nextTick 1&#39;);
  process.nextTick(() => {
    console.log(&#39;nextTick 2&#39;);
  })
})

console.log(&#39;global&#39;);


Promise.resolve().then(() => {
  console.log(&#39;promise 1&#39;);
  process.nextTick(() => {
    console.log(&#39;nextTick in promise&#39;);
  })
})

最终顺序:

  • global

  • nextTick 1

  • nextTick 2

  • promise 1

  • nextTick in promise

  • setImmediate

两个问题:

基于上边的说法,有两个问题待思考和解决:

  • 每走一个异步宏任务队列就查一遍nextTick和promise?还是每执行完 宏任务队列里的一个回调函数就查一遍呢?

  • 如果在poll的holding阶段,插入一个nextTick或者Promise的回调,会立即停止poll队列的holding去执行回调吗?

A combination of pictures and text will help you understand the event loop in Nodejs

上边两个问题,看下边代码的说法

setTimeout(() => {
  console.log(&#39;setTimeout 100&#39;);
  setTimeout(() => {
    console.log(&#39;setTimeout 100 - 0&#39;);
    process.nextTick(() => {
      console.log(&#39;nextTick in setTimeout 100 - 0&#39;);
    })
  }, 0)
  setImmediate(() => {
    console.log(&#39;setImmediate in setTimeout 100&#39;);
    process.nextTick(() => {
      console.log(&#39;nextTick in setImmediate in setTimeout 100&#39;);
    })
  });
  process.nextTick(() => {
    console.log(&#39;nextTick in setTimeout100&#39;);
  })
  Promise.resolve().then(() => {
    console.log(&#39;promise in setTimeout100&#39;);
  })
}, 100)

const fs = require(&#39;fs&#39;)
fs.readFile(&#39;./1.poll.js&#39;, () => {
  console.log(&#39;poll 1&#39;);
  process.nextTick(() => {
    console.log(&#39;nextTick in poll ======&#39;);
  })
})

setTimeout(() => {
  console.log(&#39;setTimeout 0&#39;);
  process.nextTick(() => {
    console.log(&#39;nextTick in setTimeout&#39;);
  })
}, 0)

setTimeout(() => {
  console.log(&#39;setTimeout 1&#39;);
  Promise.resolve().then(() => {
    console.log(&#39;promise in setTimeout1&#39;);
  })
  process.nextTick(() => {
    console.log(&#39;nextTick in setTimeout1&#39;);
  })
}, 1)

setImmediate(() => {
  console.log(&#39;setImmediate&#39;);
  process.nextTick(() => {
    console.log(&#39;nextTick in setImmediate&#39;);
  })
});

process.nextTick(() => {
  console.log(&#39;nextTick 1&#39;);
  process.nextTick(() => {
    console.log(&#39;nextTick 2&#39;);
  })
})

console.log(&#39;global ------&#39;);

Promise.resolve().then(() => {
  console.log(&#39;promise 1&#39;);
  process.nextTick(() => {
    console.log(&#39;nextTick in promise&#39;);
  })
})

/** 执行顺序如下
global ------
nextTick 1
nextTick 2
promise 1
nextTick in promise
setTimeout 0 // 解释问题1. 没有上边的nextTick和promise,setTimeout和setImmediate的顺序不一定,有了以后肯定是0先开始。
// 可见,执行一个队列之前,就先检查并执行了nextTick和promise微队列
nextTick in setTimeout
setTimeout 1
nextTick in setTimeout1
promise in setTimeout1
setImmediate
nextTick in setImmediate
poll 1
nextTick in poll ======
setTimeout 100
nextTick in setTimeout100
promise in setTimeout100
setImmediate in setTimeout 100
nextTick in setImmediate in setTimeout 100
setTimeout 100 - 0
nextTick in setTimeout 100 - 0
 */

以上代码执行多次,顺序不变,setTimeout和setImmediate的顺序都没变。

执行顺序及具体原因说明如下:

  • global :主线程同步任务,率先执行没毛病

  • nextTick 1:执行异步宏任务之前,清空异步微任务,nextTick优先级高,先行一步

  • nextTick 2:执行完上边这句代码,又一个nextTick微任务,立即率先执行

  • promise 1:执行异步宏任务之前,清空异步微任务,Promise的优先级低,所以在nextTick完了以后立即执行

  • nextTick in promise:清空Promise队列的过程中,遇到nextTick微任务,立即执行、清空

  • setTimeout 0: 解释第一个问题. 没有上边的nextTick和promise,只有setTimeout和setImmediate时他俩的执行顺序不一定。有了以后肯定是0先开始。可见,执行一个宏队列之前,就先按顺序检查并执行了nextTick和promise微队列。等微队列全部执行完毕,setTimeout(0)的时机也成熟了,就被执行。

  • nextTick in setTimeout:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【这种回调函数里的微任务,我不能确定是紧随同步任务执行的;还是放到微任务队列,等下一个宏任务执行前再清空的他们。但是顺序看上去和立即执行他们一样。不过我比较倾向于是后者:先放到微任务队列等待,下一个宏任务执行前清空他们。】

  • setTimeout 1:因为执行微任务耗费时间,导致此时timers里判断两个0和1的setTimeout计时器已经结束,所以两个setTimeout回调都已加入队列并被执行

  • nextTick in setTimeout1:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】

  • promise in setTimeout1:执行完上边这句代码,又一个Promise微任务,立即紧随执行 【可能是下一个宏任务前清空微任务】

  • setImmediate:poll队列回调时机未到,先行向下到check队列,清空队列,立即执行setImmediate回调

  • nextTick in setImmediate:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】

  • poll 1:poll队列实际成熟,回调触发,同步任务执行。

  • nextTick in poll :执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】

  • setTimeout 100:定时器任务到达时间,执行回调。并在回调里往微任务推入了nextTick、Promise,往宏任务的check里推入了setImmediate的回调。并且也开启了计时器线程,往timers里增加了下一轮回调的可能。

  • nextTick in setTimeout100:宏任务向下前,率先执行定时器回调内新增的微任务-nextTick 【这里就能确定了,是下一个宏任务前清空微任务的流程】

  • promise in setTimeout100:紧接着执行定时器回调内新增的微任务-Promise 【清空完nextTick清空Promise的顺序】

  • setImmediate in setTimeout 100:这次setImmediate比setTimeout(0)先执行的原因是:流程从timers向后走到check队列,已经有了setImmediate的回调,立即执行。

  • nextTick in setImmediate in setTimeout 100:执行完上边这句代码,又一个nextTick微任务,下一个宏任务前率先清空微任务

  • setTimeout 100 - 0:轮询又一次回到timers,执行100-0的回调。

  • nextTick in setTimeout 100 - 0:执行完上边这句代码,又一个nextTick微任务,下一个宏任务前率先清空微任务。

扩展:为什么有了setImmediate还要有nextTick和Promise?

一开始设计的时候,setImmediate充当了微队列的作用(虽然他不是)。设计者希望执行完poll后立即执行setImmediate(当然现在也确实是这么表现的)。所以起的名字叫Immediate,表示立即的意思。 但是后来问题是,poll里可能有N个任务连续执行,在执行期间想要执行setImmediate是不可能的。因为poll队列不停,流程不向下执行。

于是出现nextTick,真正的微队列概念。但此时,immediate的名字被占用了,所以名字叫nextTick(下一瞬间)。事件循环期间,执行任何一个队列之前,都要检查他是否被清空。其次是Promise。

面试题

最后,检验学习成果的面试题来了

async function async1() {
  console.log(&#39;async start&#39;);
  await async2();
  console.log(&#39;async end&#39;);
}

async function async2(){
  console.log(&#39;async2&#39;);
}
console.log(&#39;script start&#39;);

setTimeout(() => {
  console.log(&#39;setTimeout 0&#39;);
}, 0)

setTimeout(() => {
  console.log(&#39;setTimeout 3&#39;);
}, 3)

setImmediate(() => {
  console.log(&#39;setImmediate&#39;);
})

process.nextTick(() => {
  console.log(&#39;nextTick&#39;);
})

async1();

new Promise((res) => {
  console.log(&#39;promise1&#39;);
  res();
  console.log(&#39;promise2&#39;);
}).then(() => {
  console.log(&#39;promise 3&#39;);
});

console.log(&#39;script end&#39;);

// 答案如下
// -
// -
// -
// -
// -
// -
// -
// -
// -
// -
// -
// -






/**
script start
async start
async2
promise1
promise2
script end

nextTick
async end
promise 3

// 后边这仨的运行顺序就是验证你电脑运算速度的时候了。
速度最好(执行上边的同步代码 + 微任务 + 计时器运算用了不到0ms):
setImmediate
setTimeout 0
setTimeout 3

速度中等(执行上边的同步代码 + 微任务 + 计时器运算用了0~3ms以上):
setTimeout 0
setImmediate
setTimeout 3

速度较差(执行上边的同步代码 + 微任务 + 计时器运算用了3ms以上):
setTimeout 0
setTimeout 3
setImmediate
*/

思维脑图 - Node生命周期核心阶段

1A combination of pictures and text will help you understand the event loop in Nodejs

A combination of pictures and text will help you understand the event loop in Nodejs

更多编程相关知识,请访问:编程视频!!

The above is the detailed content of A combination of pictures and text will help you understand the event loop in Nodejs. For more information, please follow other related articles on the PHP Chinese website!

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