Node之生命周期

41 阅读4分钟

Node之生命周期

Node 生命周期和浏览器事件循环过程类似,只是比它要更复杂一些。不考虑执行栈和其他线程,因为 js 是单线程。
首先是进入 main 入口文件,会运行所有全局上下文的代码,然后就进入事件循环,会检查 6 个部分有没有任务进行,它会一直进行检查有没有任务可以执行,如果没有任务,就结束运行。
这 6 个部分重点关注三个部分,timers 表示存放计时器的回调函数,poll 叫做轮询队列,除了 timers 和 check 部分的回调函数都会进入到 poll 里面。读取文件,监听请求都会进入到这个队列中,如果 poll 中有回调,就依次执行回调,直到清空队列,如果其他队列没有回调,就会一直等待,等到其他队列出现回调,结束当前阶段,进入下一阶段,否则就会一直等待。

setTimeout(() => {
    console.log("setTimeout")
}, 5000);
const http=require('http')
http.createServer((req,res)=>{
    console.log('request in')
}).listen(3000)

像这个例子中,开始会一直等待请求,然后等到计时器任务触发,执行计时器任务,执行完成之后,继续等待。

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

这个例子会导致计时器的延迟不准,原因是没有执行 timers 队列的时候,poll 队列自己执行回调函数等待了 300 ms ,但这时计时队列已经开始了,导致在执行完这一阶段的 poll 之后,再执行计时队列时,已经超过了设定的 200 ms 。

setImmediate(() => {
    console.log("setImmediate")
})
setTimeout(() => {
    console.log("setTimeout")
}, 0);

check 表示检查阶段。它会将 setImmediate 里面的回调放到这个队列中。setImmediate 和 setTimeout 的周期为 0 很相似,但不一样。setImmediate 是直接把回调放到队列,而 setTimeout 是使用一个循环一个一个查看计时线程里面有没有到达的回调函数。这样会使得增加运算和计时线程的消耗,没有使用 setImmediate 更有效率。

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

当使用 setTimeout 的时候,当执行到计时器时,就会开辟一个计时器,然后进入下一次循环,在计时器里检查是否到达当前时间,如果到达就直接运行,然后又创建一个计时器。它花费的时间多就是由于需要每次都进行计时的运算。而 setImmediate 里根本就没有计时器,相当于直接把它加到一个数组中去,所以可以直接运行,速度提升特别快。

setTimeout(() => {
    console.log("setTimeout")
}, 0);
setImmediate(() => {
    console.log("setImmediate")
})

上面的例子两个结果谁先打印出来并不确定,因为在执行过程中,运行环境和运行速度的不同,会导致可能产生卡顿,由于 setTimeout 在执行时并不为 0 至少为 1 ms 。所以可能执行 setTimeout 的时候还没有加入到计时线程中,而 setImmediate 是一定会执行的,所以会导致两个打印结果不确定。

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

像这种情况一定会输出 2 ,1 ,因为在运行到 poll 之前,计时线程里没东西,读取完成后,即使没有卡顿,立即开辟了一个计时线程,但 check 也会在执行的过程中直接创建,所以会先运行 setImmediate ,然后才是 setTimeout 。

setImmediate(()=>{
    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)
    })
})

这个例子中,出现了 nextTick 和 Promise ,这两个相当于浏览器中的微队列,在每一次执行回调之前,或者一次操作之前,必须最先执行的队列,nextTick 优先级最高,Promise 次之,保证清空 nextTick 和 Promise 队列。
所以最后先执行当前上下文 1 ,然后是 nextTick 和 Promise 和 setImmediate 放入队列中,然后进行事件循环,先执行 nextTick 和 Promise ,2,6,4,5,然后为 check 里的 1 。
实际上,js 之前想要通过 setImmediate 来解决事件循环,虽然简化异步但仍然优先级不够,它只能在 poll 执行之后再执行。于是直接在每一次回调之前加一个微队列,但名字无法更改,所以起了一个新名字叫 nextTick 。