关于node.js eventloop

在node中的 eventloop ,也有Promise为代表的微任务,和以setTimeout为代表的宏任务,
并且这两个类型的任务行为和浏览器一样。
可以把下面这个案例在浏览器和node中都跑一边,看看结果是不是一样。
js
const a = new Promise((res) => { setTimeout(res); }); a.then(() => { console.log(1); }) .then(() => { console.log(2); }) .then(() => { console.log(3); }); a.then(() => { console.log(4); }) .then(() => { console.log(5); }) .then(() => { console.log(6); }); setTimeout(() => { console.log(7); Promise.resolve().then(() => { console.log(8); }); }); setTimeout(() => { console.log(9); }); Promise.resolve().then(() => { console.log(10); }); console.log(11);
打印的结果顺序都是 11,10,1,4,2,5,3,6,7,8,9
Promise 的具体行为在关于浏览器eventloop中已经说过了。
但是,node还加入了另外两个异步任务类型,process.nextTicksetImmediate ,这两个玩意就得好好说说了。

process.nextTick

可以说 process.nextTick 的性质和 Promise 一模一样,只不过 node 中的 process.nextTick 所在的任务队列的优先级比 Promise 所在的任务队列的优先级高!
Promise在浏览器中当大哥,在node中因为有了 process.nextTick,就只能当老二了。
我们暂且称process.nextTick产生的任务为 微任务Max微任务Max 所在的队列为 微队列Max
拿两组实验对比了解 process.nextTick 和 Promise。

第一组:

Promise 的执行时机
js
console.log('start') Promise.resolve().then(() => { console.log("1"); Promise.resolve().then(() => { console.log("2"); }); }); Promise.resolve().then(() => { console.log("3"); Promise.resolve().then(() => { console.log("4"); }); }); setTimeout(() => { console.log("5"); Promise.resolve().then(() => { console.log("6"); }); }); setTimeout(() => { console.log("7"); }); console.log('end')
结果为 start,end,1,3,2,4,5,6,7。
process.nextTick 的执行时机
js
console.log('start') process.nextTick(function () { console.log("1"); process.nextTick(function () { console.log("2"); }); }); process.nextTick(function () { console.log("3"); process.nextTick(function () { console.log("4"); }); }); setTimeout(function () { console.log("5"); process.nextTick(function () { console.log("6"); }); }); setTimeout(function () { console.log("7"); }); console.log('end')
结果为 start,end,1,3,2,4,5,6,7。
这一组对照实验说明 node 中 process.nextTick 和 浏览器中的 Promise 一样。
Node中每次事件循环开始时,会去微队列Max中去查看是否有东西,如果有,全部拿出来执行,如果执行过程中产生了新的微任务Max,会立即把产生的微任务Max放入微队列Max中。
当本次事件循环结束,下一次的事件循环开始时,还是会先去微队列Max中去查看是否有东西,有就执行,如此反复,直到微队列Max中没有东西的时候,才会跑到下一个级别的任务队列找任务执行,这点特性和Promise所在的微队列一模一样。

第二组:

验证 process.nextTick 和 Promise 的优先级
js
console.log('start') Promise.resolve().then(() => { console.log("1"); Promise.resolve().then(() => { console.log("2"); }); }); Promise.resolve().then(() => { console.log("3"); Promise.resolve().then(() => { console.log("4"); }); }); process.nextTick(function () { console.log("5"); process.nextTick(function () { console.log("6"); }); }); process.nextTick(function () { console.log("7"); process.nextTick(function () { console.log("8"); }); }); console.log('end')
结果为 start,end,5,7,6,8,1,3,2,4
这个如果有 Promise 的基础,理解不难
结论:看到 process.nextTick,就相当于看到了promise的亲哥哥,行为和promise一样,但是优先级比promise高。

setImmediate

这个玩意需要和 setTimeout 放在一起说,因为他们都属于宏任务,先回忆一下宏任务和微任务的区别,微任务是把微队列中所有的任务一起拿出来执行,但是宏任务是每次只能拿出来一个执行,哪怕是宏任务队列里已经存在了很多任务了,也得是每次事件循环执行一个任务,第二个任务要等到下一个事件循环才能执行,使用代码展示一下宏队列和微队列的区别。在关于浏览器eventloop中已经说过了。
回过头看 setImmediate,虽然 setImmediate 所在的也是宏队列,但是和 setTimeout 所在的不是一个宏队列,那么既然不在一起,就得有优先级的区分,那么他两个谁是大哥?我找到了下面的资料:
fee0c743-21aa-44f4-8f25-b3a414c6067a
  1. timers: 执行setTimeout和setInterval的回调
  2. pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  3. idle, prepare: 仅系统内部使用
  4. poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  5. check: setImmediate在这里执行
  6. close callbacks: 一些关闭的回调函数,如:socket.on('close', …)
你就看我标记红色的部分就行了,就看这个顺序,就知道应该是 setTimeout 是大哥,setImmediate 是老二,验证一下:
js
setTimeout(() => { console.log("1"); }, 0); setImmediate(() => { console.log("2"); });
2, 1
结果却是:2 1,这是什么情况?
a6b2ab72-cd50-4f25-bd9d-fa6c9ad910f6
红框内容说明,在node中,setTimeout的delay参数最少只能为1,设置为0也没用。那么这就可能是上面例子怪异的原因了,模拟事件循环来验证一下:
【注意:为了演示方便,两个微任务我合成了一个】
js
setTimeout(() => { console.log("1"); }, 0); setImmediate(() => { console.log("2"); });
1. 执行全局代码(第一次eventloop)
任务当前执行代码
主线程全局代码
微任务
定时器的宏队列
setImmediate的宏队列
定时器的计时线程
全局代码执行顺序
setTimeout 中的 () => { console.log(“1”);} 被放入定时器的计时线程,等待1毫秒后会被送入定时器的宏队列。
setImmediate 中的 () => { console.log(“2”);} 被放入setImmediate的宏队列。
此时任务队列的内容为:
任务当前执行代码
主线程
微任务
定时器的宏队列
setImmediate的宏队列() => { console.log(“2”);}
定时器的计时线程() => { console.log(“1”);}
等1毫秒后送入定时器宏队列
2. 第二次eventloop
主线程先去微队列找,发现没东西,继续向下找。
这里就是关键了
由于示例代码实在是太少,以至于当第二次事件循环开始时,还没有超过1毫秒,所以这时主线程去查看定时器宏队列的时候,没有发现任务,因为定时器的计时线程发现还没过1毫秒,就没把() => { console.log(“1”);} 放入定时器宏队列。
那么主线程就继续往下找,发现下面setImmediate的宏队列有任务,就拿出来第一个执行,在执行过程中,定时器的计时线程发现已经到了1毫秒,随即把 () => { console.log(“1”);} 放入定时器的宏队列,姗姗来迟。
任务当前执行代码
主线程() => { console.log(“2”);}
微任务
定时器的宏队列() => { console.log(“1”);}
setImmediate的宏队列
定时器的计时线程
执行() => { console.log(“2”);}, 此时控制台打印 2
此时任务队列的内容为:
任务当前执行代码
主线程
微任务
定时器的宏队列() => { console.log(“1”);}
setImmediate的宏队列
定时器的计时线程
3. 第三次eventloop
主线程先去微队列找,发现没东西,继续向下找。发现定时器的宏队列 有任务,拿出第一个来执行。
任务当前执行代码
主线程() => { console.log(“1”);}
微任务
定时器的宏队列
setImmediate的宏队列
定时器的计时线程
执行() => { console.log(“1”);}, 此时控制台打印 1
全部代码执行结束
当前可以确定,确实是 setTimeout 的默认延迟1毫秒导致的执行后置的现象。
那么如果使第一次全局代码执行时,在执行了setTimeout后的时间超过1毫秒,是不是setTimeout就能优先于setImmediate执行了?

验证

js
setTimeout(() => { console.log("1"); }, 0); setImmediate(() => { console.log("2"); }); // 这个函数可以阻塞主线程 const syncFunc = (delay) => { const time = new Date().getTime(); while (true) { if (new Date().getTime() - time > delay) { break; } } }; // 阻塞主线程1秒的时间,这个时间内,定时器的计时线程肯定有充足的时间把 () => { console.log(“1”);} 放入定时器的宏队列 syncFunc(1000)
1, 2
结果确实是1,2
syncFync函数阻塞主线程1秒的时间,这个时间内,定时器的计时线程肯定有充足的时间把 () => { console.log(“1”);} 放入定时器的宏队列,所以在第二次事件循环执行时,定时器的宏队列里面已经有东西了,所以可以先打印1,再打印2。
所以目前可以得到初步结论:它俩之间的优先级是不确定的,有时 setImmediate 比 setTimeout优先级高,有时候setTimeout 比 setImmediate 高。

继续探索,这事没那么简单

再看一个例子:
js
// 这个函数可以阻塞主线程 const syncFunc = (delay) => { const time = new Date().getTime(); while (true) { if (new Date().getTime() - time > delay) { break; } } }; setTimeout(() => { setTimeout(() => { console.log("1"); }); setImmediate(() => { console.log("2"); }); // 阻塞主线程1秒的时间 syncFunc(1000) });
2, 1
结果是:2 1,这是什么情况?
这里唯一的区别就是在外面套了一个 setTimeout,模拟执行一下看看是什么原因。
1. 执行全局代码(第一次eventloop)
任务当前执行代码
主线程全局代码
微任务
定时器的宏队列
setImmediate的宏队列
定时器的计时线程
全局代码执行顺序
setTimeout 中的
() => {
setTimeout(() => { console.log("1");});
setImmediate(() => {console.log("2");});
syncFunc(1000)
}
被放入 定时器的计时线程,等待1毫秒后会被送入定时器的宏队列。
此时任务队列的内容为:
任务当前执行代码
主线程
微任务
定时器的宏队列
setImmediate的宏队列
定时器的计时线程() => {
setTimeout(() => { console.log("1");});
setImmediate(() => {console.log("2");});
syncFunc(1000)
}
等1毫秒后送入定时器宏队列
由于代码执行速度太快,导致一毫秒内完成了当前的eventloop,定时器的计时线程 没有动作。
2. 第二次eventloop
由于执行栈都是空的,第二次eventloop什么也没做,但是此时 定时器的计时线程 的1毫秒时间已经到了,线程内的待执行代码被放入到定时器的宏队列中。
此时任务队列的内容为:
任务当前执行代码
主线程
微任务
定时器的宏队列() => {
setTimeout(() => { console.log("1");});
setImmediate(() => {console.log("2");});
syncFunc(1000)
}
setImmediate的宏队列
定时器的计时线程
3. 第三次eventloop
主线程先去微队列找,发现没东西,继续向下找。发现定时器的宏队列 有任务,拿出第一个来执行。
任务当前执行代码
主线程() => {
setTimeout(() => { console.log("1");});
setImmediate(() => {console.log("2");});
syncFunc(1000)
}
微任务
定时器的宏队列
setImmediate的宏队列
定时器的计时线程
这一部分的执行需要细说:
  1. 先执行的是 setTimeout(() => { console.log("1"); }); 但是注意了,这里有一个隐藏的1毫秒延迟,所以此时的 () => { console.log("1"); } 被放入 定时器的计时线程,等待1毫秒后会被送入定时器的宏队列。
  2. 再执行的是 setImmediate(() => {console.log("2");}); 由于 setImmediate 是立即放入队列,那么 () => {console.log("2"); 被立即放入到 setImmediate 的宏队列。
  3. 最后执行的是 syncFunc(1000),它的作用是延迟一秒,这期间,定时器的计时线程 发现已经到了1毫秒,随即把 () => { console.log(“1”);} 放入定时器的宏队列
此时任务队列的内容为:
任务当前执行代码
主线程
微任务
定时器的宏队列() => { console.log("1");}
setImmediate的宏队列() => {console.log("2");}
定时器的计时线程
注意,关键点来了,此时,当前的 eventloop 并没有结束!
因为如果结束了,那么会开启第四次eventloop,回到顶部继续从微任务开始向下找,然后到定时器的宏队列,那么一定会先打印1,而不是先打印2了。
主线程这次来 setImmediate 的宏队列找任务,发现有,拿出第一个来执行
任务当前执行代码
主线程() => {console.log("2");}
微任务
定时器的宏队列() => { console.log("1");}
setImmediate的宏队列
定时器的计时线程
执行() => { console.log(“2”);}, 此时控制台打印 2
此时任务队列的内容为:
任务当前执行代码
主线程
微任务
定时器的宏队列() => { console.log("1");}
setImmediate的宏队列
定时器的计时线程
4. 第四次eventloop
这接下来的事就不在详细说明了...
执行() => { console.log("1");}, 此时控制台打印 1
回顾一下为什么打印的是2,1?因为 setImmediate的宏队列定时器的宏队列 的后面,定时器的宏队列 执行完后产生了一个新的 setImmediate的宏队列 的任务,当 定时器的宏队列 执行完后,还要继续 setImmediate的宏队列 的执行,所以2就在1的前面打印了。
以下代码的执行结果是什么?
js
setImmediate(() => { setTimeout(() => { console.log("1"); }); setImmediate(() => { console.log("2"); }); });
最后打印 1 2。

总结:在node中,任务执行优先级是,process.nextTick > Promise > (setTimeout 和 setImmediate)。setTimeout 和 setImmediate 这两玩意谁先执行,看情况!