说到Node.js
的事件循环网上已经有了不少形形色色的文章来说述其中的原理,说的大概都是一个意思,学习了一段时间,对Node.js
事件循环有了必定的了解以后写一篇博客总结一下本身的学习成果。javascript
事件循环
在笔者看来事件与循环自己就是两个概念,事件是能够被控件识别的操做,如按下肯定按钮,选择某个单选按钮或者复选框。每一种控件有本身能够识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件。html
然而循环则是在GUI
线程中包含有一个循环,然而这个循环对于开发者和用户来说是看不见的,只有关闭了程序以后该循环才会结束。当用户触发了一个按钮事件以后,就会产生响应的事件,这些时间被加入到一个队列中,用户在前台不断的产生事件,然然后台也在不断的处理这些时间,在处理的时候被加入到一个队列中,因为主循环中循环的存在会挨个处理这些对应的事件。java
而对于JavaScript
来说的话因为JavaScript
是单线程的,对于一个比较耗时的操做则是使用异步的方法解决(Ajax...)。对于不一样的异步事件来也是由不一样的线程各司其职来处理的。node
Node.js中的事件循环
Node.js
的事件循环与浏览器的事件循环仍是有很大的区别的,当Node.js
启动后,它会初始化事件轮询;处理已提供的输入脚本(或丢入REPL
,本文不涉及到),它可能会调用一些异步的API
函数调用,安排任务处理事件,或者调用process.nextTick()
,而后开始处理事件循环。npm
有一点是很是明确的,事件循环一样运行在单线程环境下,JavaScript
的事件循环是依靠于浏览器来实现的,然而Node.js
则是依赖于Libuv
来实现的。promise
根据Node.js官方介绍,每次事件循环都包含了6个阶段,对应到Libuv
源码中的实现,以下图所示,图中显示了事件循环的概述以及执行顺序。浏览器
下面是Node.js
事件循环源代码:网络
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); // timers阶段 uv__run_timers(loop); // I/O callbacks阶段 ran_pending = uv__run_pending(loop); // idle阶段 uv__run_idle(loop); // prepare阶段 uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); // poll阶段 uv__io_poll(loop, timeout); // check阶段 uv__run_check(loop); // close callbacks阶段 uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
假设事件循环进入到某一个阶段,及时在这期间其余队列中的事件已经准备就绪,也会先将当前阶段对应队列中全部的回调方法执行完毕以后才会继续向下执行,结合代码也是可以很好的理解的。不难能够得出在事件循环系统中回调的执行顺序是有迹可循的,一样也会形成事件阻塞。异步
var fs = require("fs"); fs.readFile('input.txt', function (err, data) { if (err){ console.log(err.stack); return; } console.log(data.toString()); }); fs.readFile('test.txt', function (err, data) { if (err){ console.log(err.stack); return; } console.log(data.toString()); }); console.log("程序执行完毕");
对于整个事件循环有个一个大概的认知以后,接下来针对每一个阶段进行详细的说明。socket
该阶段主要用来处理定时器相关的回调方法,当一个定时器超市后一个事件就会加入到该阶段的队列中,事件循环会跳转至这个阶段执行对应的回调方法。
定时器的回调会在触发后尽量早的被调用,为何要说尽量早的呢?由于实际的触发事件可能要比预先设置的时间要长。Node.js
并不能保证timer
在预设时间到了就会当即执行,由于Node.js
对timer
的过时检查不必定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。
在这个阶段中除了timers、setImmediate
,以及close
操做以外的大多数的回调方法都位于这个阶段执行。例一个TCP socket
执行出现了一些错误,那么这个回调函数会在I/O callbacks
阶段来执行。名字会让人误解为执行I/O
回调处理程序,然而一些常见的回调则会再poll
阶段进行处理。
I/O callbacks
阶段主要通过以下过程:
对于Poll
阶段其主要的功能主要有两点:
当事件循环到达poll
阶段时,若是这时没有要处理的定时器的回调方法,则会进行以下判断:
poll
队列不为空,则事件循环会按照顺序便利执行队列中的回调方法,这个过程是同步的。poll
队列为空则会再次进行判断
setImmediate()
,事件循环将结束poll
阶段进入check
阶段,并执行check
阶段的任务队列setImmediate()
,那么事件循环可能会进入等待状态,并等待新事件的产生,这也是该阶段为何被命名为poll
的缘由。出了这些意外,该阶段还会不断的检查是否有相关的定时器超市,若是有就会跳转到timers
阶段,而后执行对应的回调方法该阶段执行setImmediate()
的回调函数。关于setImmediate
是一个比较特殊的定时器方法,setImmediate
的回调则会加入到check
队列中,从事件循环的阶段图能够知道,check
阶段的执行顺序是在poll
以后的。
通常状况下,事件循环到达poll
阶段后,就会检查当前代码是否调用了setImmediate
方法,这个在叙述poll
阶段的时候已经有说起了,若是一个回调函数是被setImmediate
方法调用的,事件循环则会跳出poll
阶段从而进入到check
阶段。(这一段有点重复...)
close
阶段是用来管理关闭事件,用于清理应用程序的状态。如程序中的socket
关闭等都会加入到close
队列中,当本轮事件结束后则会进入下一轮循环。
对于事件循环来讲每一个阶段都有一个任务队列,当事件循环到达某个阶段的时候,讲执行该阶段的任务队列,知道队列清空或执行的对调到达系统上限后,才会转入到下一个阶段。当全部的阶段被执行一次后,事件循环则就完成了一个tick
。
process.nextTick
这是Node.js
特有的方法,它不存在于任何浏览器(以及进程对象)中,process.nextTick
是一个异步的动做,而且让这个动做在事件循环中当前阶段执行完以后当即执行,也就是上面所说的tick
。
process.nextTick(() => { console.log("1") }) console.log("2") // 2 // 1
官方对于process.nextTick
有一段颇有意思的解释:从语义角度看,setImmediate
(稍后会说到)应该比process.nextTick
先执行才对,而事实相反,命名是历史缘由也很难再变。
然而对于process.nextTick
来讲该方法并非事件循环中的一部分,可是它的回调方法确是由事件循环调用的,该方法定义的回调方法会被加入到nextTickQueue
的队列中。相反地,nextTickQueue
将会在当前操做完成以后当即被处理,而无论当前处于事件循环的哪一个阶段。
Node.js
对process.nextTick
进行了限制,若递归调用process.nextTick
当倒带nextTickQueue
最大限制以后则会抛出一个错误。
function nextTick (i){ while(i<9999){ process.nextTick(nextTick(i++)); } } // Maxmum call stack size exceeded nextTick(0);
既然说process.nextTick
也是存在于队列中,那么其执行顺序也是根据程序所编写顺序执行的。
process.nextTick(() => { console.log(1) }); process.nextTick(() => { console.log(2) }); // 1 // 2
和其它回调函数同样,process.nextTick
定义的回调也是由事件循环执行的,若是process.nextTick
的回调方法中出现了阻塞操做,后面的要执行的回调函数一样会被阻塞。process.nextTick
会在各个事件阶段之间执行,一旦执行,要直到nextTickQueue
被清空,才会进入到下一个事件阶段,因此若是递归调用process.nextTick
,会致使出现I/O starving
的问题,好比下面例子的readFile
已经完成,但它的回调一直没法执行。
const fs = require('fs') const starttime = Date.now() let endtime; fs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime) }) let index = 0 function handler () { if (index++ >= 1000) return console.log(`nextTick ${index}`) process.nextTick(handler) } handler(); // nextTick 1 // nextTick 2 // ...... // nextTick 999 // nextTick 1000 // finish reading time: 170
process.nextTick() vs setImmediate()
seImmediate
方法不属于ECMAScript
标准,而是Node.js
提出的新方法,它一样将一个回调函数加入到事件队列中,不一样于setTimeout
和setInterval
,setImmediate
并不接受一个时间做为参数,setImmediate
的事件会在当前事件循环的结尾触发,对应的回调方法会在当前事件循环的末尾(check)执行。虽然它确实存在于某些浏览器中,但并未在全部浏览器中达到一致的行为,所以在浏览器中使用时,您须要很是当心。它相似于setTimeout(fn,0)
代码,但有时会优先于它。这里的命名也不是最好的。
process.nextTick
中的回调在事件循环的当前阶段中被当即执行。setImmediate
中的回调在事件循环的下一次迭代或tick
中被执行本质上,它们两个的名字应该互相调换一下。process.nextTick()
的执行时机比setImmediate()
要更及时(上面有提过)。实施这项改变将致使不少npm
包没法使用。天天都有不少新模块被加入,这意味着每等待一天,就会有更多潜在的破坏发生。虽然他们的名字相互混淆,但将它们调换名字这种事是不会发生的(建议开发者在全部地方使用setImmediate
,这样程序更容易让人理解)。
仍然使用上述例子,若把nextTick
替换成setImmediate
会怎样呢?
const fs = require('fs') const starttime = Date.now() let endtime; fs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime) }) let index = 0 function handler () { if (index++ >= 1000) return console.log(`setImmediate ${index}`) setImmediate(handler) } handler(); // setImmediate 1 // setImmediate 2 // finish reading time: 80 // ...... // setImmediate 999 // setImmediate 1000
这是由于嵌套调用的setImmediate()
回调,被排到了下一次事件循环才执行,因此不会出现阻塞。
setImmediate vs setTimeout
定时器在Node.js
和浏览器中的表现形式是相同的。关于定时器的一个重要的事情是,咱们提供的延迟不表明在这个时间以后回调就会被执行。它的真正含义是,一旦主线程完成全部操做(包括微任务)而且没有其它具备更高优先级的定时器,Node.js
将在此时间以后执行回调。
setImmediate()
被设计在poll
阶段结束后当即执行回调setTimeout()
被设计在指定下限时间到达后执行回调setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); // 结果一 // timeout // immediate /**--------华丽的分割线--------**/ // 结果二 // immediate // timeout
why?为何会有两个结果,笔者在研究这里的时候也是有些不太明白,因而又作了第二个例子:
var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) }); // 运行N次 // immediate // timeout
setImmediate
的回调永远先执行。虽然结论得出来了,可是这又是为啥呢?回想一下文章上半段所叙述的事件循环。首先进入timer
阶段,若是咱们的机器性能通常,那么进入timer
阶段时,1毫秒可能已通过去了(setTimeout(fn,0)
等价于setTimeout(fn,1)
),那么setTimeout
的回调会首先执行。若是没到一毫秒,那么咱们能够知道,在check
阶段,setImmediate
的回调会先执行。为何fs.readFile
回调里设置的,setImmediate
始终先执行?由于fs.readFile
的回调执行是在poll
阶段,因此,接下来的check
阶段会先执行setImmediate
的回调。咱们能够注意到,UV_RUN_ONCE
模式下,事件循环会在开始和结束都去执行timer
。
练习题
阅读完本文章有什么收获呢?不如看下下面的代码,预测一下输出结果是什么样的。先不要急着看答案额...
const fs = require('fs'); console.log('beginning of the program'); const promise = new Promise(resolve => { console.log('I am in the promise function!'); resolve('resolved message'); }); promise.then(() => { console.log('I am in the first resolved promise'); }).then(() => { console.log('I am in the second resolved promise'); }); process.nextTick(() => { console.log('I am in the process next tick now'); }); fs.readFile('index.html', () => { console.log('=================='); setTimeout(() => { console.log('I am in the callback from setTimeout with 0ms delay'); }, 0); setImmediate(() => { console.log('I am from setImmediate callback'); }); }); setTimeout(() => { console.log('I am in the callback from setTimeout with 0ms delay'); }, 0); setImmediate(() => { console.log('I am from setImmediate callback'); }); // beginning of the program // I am in the promise function! // I am in the process next tick now // I am in the first resolved promise // I am in the second resolved promise // I am in the callback from setTimeout with 0ms delay // I am from setImmediate callback // ================== // I am from setImmediate callback // I am in the callback from setTimeout with 0ms delay
总结
对于本文中一些知识点任然有些模糊,懵懵懂懂,一直都在学习中,经过学习事件循环也看了一些文献,在其中看到了这一句话:除了你的代码,一切都是同步的
,我以为颇有道理,对于理解事件循环颇有帮助。
Node.js
的事件循环分为6个阶段process.nextTick
不属于事件循环,可是产生的回调会加入到nextTickQueue
setImmediate
和setTimeout
的执行顺序会受到环境所影响文章略长若文章中有哪些错误,请在评论区指出,我会尽快作出修正。你们能够踊跃发言共同进步,交流。