看thinkjs源码的时候发现下面这段代码。
1 2 3 4
| cluster.on('exit', worker => { think.log(new Error(think.locale('WORKER_DIED', worker.process.pid)), 'THINK'); process.nextTick(() => cluster.fork()); });
|
这段代码的意思很简单,就是cluster挂了以后重新fork一个。
但是注意到其中的process.nextTick(() => cluster.fork());
这行,刚开始想了一下没有理解为什么不直接fork
,后面仔细想了一下,发现如果直接fork
,在fork
的过程中又出现错误导致进程退出,而cluster
又监听到exit
的事件,就会不断的重复这个过程,阻塞Node进程。
如果使用process.nextTick(() => cluster.fork());
则不会阻塞Node的事件循环,只会在Event Loop
的close callbacks
阶段执行fork
,即使程序一直fork
失败也不会导致程序假死。(如果有疑问可以阅读文章末的扩展阅读)。
下面的Demo说明了为什么使用了nextTick
不会导致程序假死。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| var EventEmitter = require('events').EventEmitter; var event = new EventEmitter(); var count = 0; var num = 10; event.on('some_event', function() { count++; console.log('some_event 事件触发' + count); if (count < num) { event.emit('some_event') } }); event.emit('some_event'); console.log('what ?')
|
运行这段代码就会输出
1 2 3 4 5 6 7 8 9 10 11
| some_event 事件触发1 some_event 事件触发2 some_event 事件触发3 some_event 事件触发4 some_event 事件触发5 some_event 事件触发6 some_event 事件触发7 some_event 事件触发8 some_event 事件触发9 some_event 事件触发10 what ?
|
可以发现 what ?
在最后才输出。如果把num设置的非常大就会报错
1 2 3 4
| internal/process/next_tick.js:148 nextTickQueue.push({ ^ RangeError: Maximum call stack size exceeded
|
V8不断的向事件队列里添加任务,最终导致出现溢出,把event.emit('some_event')
改写成
1 2 3
| process.nextTick(function(){ event.emit('some_event') });
|
就会发现输出成了
1 2 3 4 5 6 7 8 9 10 11
| ome_event 事件触发1 what ? some_event 事件触发2 some_event 事件触发3 some_event 事件触发4 some_event 事件触发5 some_event 事件触发6 some_event 事件触发7 some_event 事件触发8 some_event 事件触发9 some_event 事件触发10
|
what ?
并不会被阻塞,而且无论num
改成多少,都不会出现栈溢出的错误。
Node的Event loop
执行流程如图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ | nextTick(队列执行) │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ | nextTick(队列执行) │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ | nextTick(队列执行) ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ | | | nextTick(队列执行) │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ | nextTick(队列执行) │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
|
直接event.emit('some_event')
的时候,Node不断的把收集到的事件塞到I/O callbacks
这个队列,如果有大量的事件塞入就会最终导致溢出,就是上面的Maximum call stack size exceeded
错误。
如果加了process.nextTick
则会不断的把emit
的事件回调加到nextTickQueue
队列,在各个主队列切换的时候执行,见上图的 nextTick(队列执行)
。上面的那段Demo把event.emit('some_event')
修改后的执行顺序就是
1、发送事件
2、把事件回调函数添加到nextTickQueue
(注意,这个时候nextTickQueue
队列里只有一个事件回调函数,如果当前队列尚未执行完毕并且没有发生切换,则nextTickQueue
队列里的事件永远不会执行)
3、执行nextTickQueue
里的第一个事件回调(当前队列执行完毕或者执行到一定数量发生切换时,事件回调又会重新创建一个新的nextTickQueue
队列并添加一个事件回调)
4、然后同上
这样就没有阻塞Node的事件循环,无论num多大都不会撑爆I/O callbacks
队列。其实最核心的思想就是将任务拆解到若干次事件循环中,逐步执行。
扩展阅读
Node.js的event loop及timer/setImmediate/nextTick
Node.js 原理简介
深入理解Node.js:核心思想与源码分析