欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

node学习—深入剖析node生命周期(事件循环)

程序员文章站 2022-05-20 11:13:10
...

深入剖析node生命周期(事件循环)


要想学好本篇博文,需要先了解js的事件循环: js事件循环原理.

一、node生命周期

提一嘴:node底层使用C语言libuv库操控操作系统
node学习—深入剖析node生命周期(事件循环)
由图,从运行模块入口main.js后,event loop会检测整个环境是否还有事件未结束,如文件读写等,定时器等。当所有事件全部执行完毕时,才会结束运行。

其中每一个阶段都会维护一个事件队列,这里主讲其中三个。

1.timers

timers:存放计时器的回调函数。
timers内部相当于一个循环在不停侦听定时器是否到达指定执行选时间。

setTimeout(()=>{
    console.log("1s后输出");
},1000);
console.log('abcd');

此时timers将()=>{console.log("1s后输出")}回调函数执行后即清空了它的队列。

2.poll

poll:轮询队列,绝大部分回调都会放入该队列,比如:文件的读取、监听用户请求。
node学习—深入剖析node生命周期(事件循环)

setTimeout(function f1(){
    console.log("1s后输出");
},1000);
console.log('abcd');

还是这段代码,f1要等1s的事件,所以timers里没有回调函数,然后到poll轮训队列,不停地查看是否有回调,1s后f1出现了,将f1放入timers队列,下一步check也没有。再来到timers,执行f1,清空了timers队列。然后又poll轮询,最终没有任何回调了,over。

setTimeout(function f1(){
    console.log("1s后输出");
},1000);
const http = require("http");

const server = http.creareServer((req,res)=>{
    console.log("request in");
});
server.listen(9527);

到达event loop事件循环,查看是否有值得等待的事件。此时timers还没有回调函数,然后到了poll轮询队列,不停地查看是否有回调,此时f1出现了,将f1放入timers队列,下一步check也没有。再来到timers,执行f1,清空了timers队列。又到了poll轮询,又一直等待用户请求触发createServer回调,等待很长时间仍未触发,则进入下一步check,然后又timers,poll…知道用户出发该回调函数后才能停止循环over。
接下来看这一段代码

const start = Date.now();
setTimeout(function f1(){
    console.log("setTimeout: ",Date.now() - start);
},200);
const fs = require("fs");
fs.readFile("./index.js","utf-8",(err,data)=>{
    console.log("readFile");
    const start = Date.now();
    while (Date.now() - start < 300){}
});

node学习—深入剖析node生命周期(事件循环)
此时问题来了,怎么setTimeout等待了509ms呢
node学习—深入剖析node生命周期(事件循环)
再看看这张图:
到达event loop事件循环,此时timers中没有回调。到poll轮询,此时发现需要读取文件,等待文件读取,并继续轮询,(如果文件较小)此时文件读取完成,触发了回调f2,将f2放入poll队列。执行f2,输出readFile,准备等待300ms后结束f2。等待期间此时f1好了,将f1放入timers队列,但是f2在执行中,所以继续执行f2,300ms后f2结束。发现此时没有任何需要等待的事。下一步check,没有事。又循环到timers,执行f1清空timers队列,接着poll发现没有任何事了,下一步,check也没有,最后over。所以这里显示setTimeout最终等待了509ms。

3.check

check:检查阶段,使用setImmediate的回调会直接进入这个队列
setImmediate相当于setTimeout(()=>{},0),一旦发现有setImmediate就会直接放入check队列。效率比timers高很多。

let i = 0;
console.time();
function test(){
    i++;
    if(i < 100){
        setTimeout(test,0);
    }else{
        console.timeEnd();
    }
}
test();

node学习—深入剖析node生命周期(事件循环)

let i = 0;
console.time();
function test(){
    i++;
    if(i < 100){
        setImmediate(test);
    }else{
        console.timeEnd();
    }
}
test();

node学习—深入剖析node生命周期(事件循环)
因为setTimeout每次需要判断时间放入timers队列,setImmediate没有计时直接放入check队列,所以setTimeout效率远远低于setImmediate。
但是setImmediate(()=>{})setTimeout(()=>{},0)的执行顺序就不一定了,得看计算机当时的执行环境和速度,且setTimeout一般取不到0,至少都是1

const fs = require("fs");
fs.readFile("./index.js",()=>{
    setTimeout(()=>console.log(1));
    setImmediate(()=>console.log(2));
});

从主线程到event loop事件循环,此时timers没有回调事件,接着到poll轮询,等待文件读取完成,执行回调函数,将setTimeout放入timers,setImmediate放入check。下一步check执行setImmediate。接着循环,timers执行setTimeout,poll轮询不到任何回调事件,over。所以这种情况下,setImmediate必先执行与setTimeout之前。
node学习—深入剖析node生命周期(事件循环)
此时回想js的事件循环,讲到了宏任务和微任务,那么在node中是怎么回事呢?
node学习—深入剖析node生命周期(事件循环)
所以我们前面讲到的事件循环相当于宏任务,而nextTick和Promise相当于微任务。其实它们不是事件循环的一部分,node希望优先用最快的速度去执行它们,其中nextTick优先度最高。
node学习—深入剖析node生命周期(事件循环)
看下面这一面试题,答案就显而易见了

setTimeout(() => {
    console.log(1);
});
process.nextTick(()=>{
    console.log(2);
    process.nextTick(()=>{
        console.log(6);
    });
});
console.log(3);
Promise.resolve().then(()=>{
    console.log(4);
    process.nextTick(()=>{
        console.log(5)
    });
});

node学习—深入剖析node生命周期(事件循环)
看下面这一道面试题

async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
}
async function async2() {
    console.log("async2");
}
console.log("script start");
setTimeout(function () {
    console.log("setTimeout0");
}, 0);
setTimeout(function () {
    console.log("setTimeout3");
}, 3);
setImmediate(() => console.log("setImmediate"));
process.nextTick(() => console.log("nextTick"));
async1();
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
    console.log("promise2");
}).then(function () {
    console.log("promise3");
});
console.log("script end");

我们根据代码顺序一步一步来:

  1. console.log("script start");
  2. timers:0ms_setTimeout0;3ms_setTimeout3
  3. check: setImmediate
  4. nextTick: console.log("nextTick"));
  5. console.log("async1 start");
  6. console.log("async2");
  7. Promise:console.log("async1 end");
  8. console.log("promise1");
  9. console.log("promise2");
  10. Promise:console.log("async1 end"); console.log("promise3");
  11. console.log("script end");

所以我们得出同步的结果:

  1. console.log("script start");
  2. console.log("async1 start"); 此时await async2();后为等待,即为Promise队列,所以console.log("async1 end");放入Promise队列
  3. console.log("async2");
  4. console.log("promise1");
  5. console.log("promise2");
  6. console.log("script end");

接下来我们查看队列:

  1. console.log("nextTick");
  2. console.log("async1 end")

但是后面就有问题了

setTimeout(function () {
    console.log("setTimeout0");
}, 0);
setTimeout(function () {
    console.log("setTimeout3");
}, 3);
setImmediate(() => console.log("setImmediate"));

我们不能确定得到同步的结果时是否消耗了3ms,所以不能timers队列里是否有setTimeout0或setTimeout3,最终不能确定setImmediate的实行顺序。
node学习—深入剖析node生命周期(事件循环)
在作者电脑里运行时,得到同步结果时显然已经超过了3ms,所以第一次事件循环时timers队列里已经有setTimeout0和setTimeout3了。