1、Event Loop?
Event Loop 其实也是在面试中经常会出现的一个题,前端程序员回答不上来是正常的,因为 Event Loop 是 C++ 实现的,实现原理和 JavaScript 没什么关系,我个人在之前的面试中遇到这个题的时候,也是看一些 C++ 程序员对 Event Loop 的分析,如果你想对 Event Loop 有一个深入的了解,那这里推荐你一篇 Event Loop 的官方文档,这有中文版:Event Loop。
在了解 Event Loop 之前我们可能需要一点计算机操作系统的知识,比如:当开发人员在电脑键盘上敲击了 a 键,那操作系统是如何知道的呢?大概是这样的,当 a 键盘被按下时,a 键的信息会通过键盘内的电路传递给操作系统,然后操作系统再通知浏览器,浏览器在得到 a 键的信息之后,就会将 a 显示到响应的位置。一句话,浏览器会接收到操作系统传递给它的事件。
那什么是 Event 事件?比如 setTimeout(fun1, delay) 在 delay 到时后需要执行的回调函数 fun1 就是事件,或者 fs.readFile("/1.txt', fun2) 文件读取后执行的 fun2 也是事件,再或者 http 网络请求相关的 server.on('close', fun3) ,在服务器关闭时执行的 fun3 也是事件。
那什么是 Loop 循环?Loop 就是循环的意思,比如我们学习编程过程中的 for、while 循环,它们是一个道理的。但是由于 Event 事件是分优先级的,所以在处理的时候也是要分先后顺序的,Node.js 就是按照顺序来轮询每种事件的。
Event Loop?回到上述所说的 Event 事件的问题,假如上述的场景,操作系统同时触发了三种事件,那 Node 会怎么办?答案是这几种事件有一个优先级分类,然后根据优先级排序执行,这就是 Event Loop 对事件处理顺序的管理。那这个顺序是怎样的呢?
2、Event Loop 顺序?
JavaScript 是单线程的,有了 event loop 的加持,Node.js 才可以非阻塞地执行 I/O 操作,把这些操作尽量转移给操作系统来执行。
浏览器使用了 V8 自带的 Event Loop 事件循环机制,它只有宏任务和微任务之间的循环,而 Node.js 是基于 libuv 自己做了一个 Event Loop,它的事件循环就相对比较复杂。首先看一下浏览器的事件循环:
JS 的任务队列分 macro-task(宏任务)与 micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
macro-task 包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task 包括: process.nextTick, Promise., Object.observe(已废弃), MutationObserver(html5新特性)
Node.js 的事件循环分为六个阶段,如下图所示:其中每个方框都是 event loop 中的一个阶段。每个阶段都有一个 先入先出队列,这个队列存有要执行的回调函数(注:存的是函数地址)。不过每个阶段都有其特有的使命,一般来说,当 event loop 达到某个阶段时,会在这个阶段进行一些特殊的操作,然后执行这个阶段的队列里的所有回调。
┌───────────────────────────────────┐ ┌─>│ timers 检查计时器的callback │ │ └──────────┬────────────────────────┘ │ ┌──────────┴────────────────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────────────────┘ │ ┌──────────┴────────────────────────┐ │ │ idle, prepare │ │ └──────────┬────────────────────────┘ ┌───────────────┐ │ ┌──────────┴────────────────────────┐ │ incoming: │ │ │ poll 轮询,检查系统事件 │<─────┤ connections, │ │ └──────────┬────────────────────────┘ │ data, etc. │ │ ┌──────────┴────────────────────────┐ └───────────────┘ │ │ check 检查 setImmediate 回调 │ │ └──────────┬────────────────────────┘ │ ┌──────────┴────────────────────────┐ └──┤ close callbacks │ └───────────────────────────────────┘
timer 阶段:用于监测是否含有计时器的回调事件,如果有的话就执行,没有的话就进入下一阶段。
I/O callbacks:监测是否含有其他的回调,比如 TCP 报错,如果一个 TCP socket 开始连接时出现了 ECONNREFUSED 错误,一些 *nix 系统就会(向 Node.js)通知这个错误。这个通知就会被放入 I/O callbacks 队列。
idle,prepare:空闲阶段,一般用来清理一下内存相关,忽略不计。
poll:轮询,检查系统事件。这个阶段用来处理大部分事件,比如读文件,处理 http 请求等。
check:检查 setImmediate 回调,如果有的话就执行,没有的话就进入下一阶段。
close callbacks:执行关闭事件的回调函数,如 socket.on('close', fun) 里的 fun。
一个 Node.js 程序结束时,Node.js 会检查 event loop 是否在等待异步 I/O 操作结束,是否在等待计时器触发,如果没有,就会关掉 event loop。需要注意的是:大部分时间,Node.js 都停在 poll 轮询阶段,大部分事件都在 poll 阶段被处理,如文件、网络请求。
3、setImmediate() vs setTimeout()
setImmediate 和 setTimeout 很相似,但是其回调函数的调用时机却不一样。setImmediate() 的作用是在当前 poll 阶段结束后调用一个函数。setTimeout() 的作用是在一段时间后调用一个函数。这两者的回调的执行顺序取决于 setTimeout 和 setImmediate 被调用时的环境。
如果 setTimeout 和 setImmediate 都是在主模块(main module)中被调用的,那么回调的执行顺序取决于当前进程的性能,这个性能受其他应用程序进程的影响。举例来说,如果在主模块中运行下面的脚本,那么两个回调的执行顺序是无法判断的。但是,如果把上面代码放到 I/O 操作的回调里,setImmediate 的回调就总是优先于 setTimeout 的回调。
/* timeout_vs_immediate.js*/ /* timeout_vs_immediate.js*/ setTimeout(() => { const fs = require('fs'); console.log('timeout'); fs.readFile(__filename, () => { }, 0); setTimeout(() => { console.log('timeout'); setImmediate(() => { }, 0); console.log('immediate'); setImmediate(() => { }); console.log('immediate'); }); /*运行结果可能不同: */ }); $ node timeout_vs_immediate.js //运行结果皆为: timeout $ node timeout_vs_immediate.js immediate immediate timeout $ node timeout_vs_immediate.js immediate timeout
setImmediate 的主要优势就是,如果在 I/O 操作的回调里,setImmediate 的回调总是比 setTimeout 的回调先执行。