面试后再谈 event loop 时间循环
先看 microtasks
这是一道常见的无聊的面试题:
setTimeout(() => {
console.log(1);
}, 0);
new Promise((resolve, reject) => {
console.log(2);
for (let i = 0; i < 10000; i++) {
i === 9999 && resolve();
}
console.log(4);
}).then(() => {
console.log(5);
});
console.log(6);
相信仔细看过之前的 JavaScript 中的 tasks 与 microtasks 可以很快给出答案,在 Chrome 60.0
浏览器中的输出顺序为:2、4、6、5、1。
这道题主要是两个点:
Promise
属于microtask
,而setTimeout
回调属于task
,对于JavaScript
引擎而言,event loop
的优先级是不同的,所以Promise
resolve会先于setTimeout
的回调输出Promise
的构造函数参数是同步调用的,故2和4会先于6输出
Node 是单线程吗?
我们通常理解的是, JavaScript
引擎是单线程的,那问题就是,没有多线程,如何处理非同步I/O、网络请求逻辑?
其实我们知道,在 Node.js
中,我们使用的是异步I/O,通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程做计算处理,通过线程之间的通信将I/O得到的数据进行传递,这种异步I/O的方式就是线程池。
所以,对于 Node.js
而言,这些非同步的逻辑均是由线程池实现的,因此,说 Node
是单线程这个说法是不准确的,仅对于你书写的 JavaScript
代码而言,可以说 Node
是单线程。
事件循环
当 Node.js
进程启动时, Node.js
会创建一个类似于 while(true)
的循环,每执行一次循环体的过程我们称之为一次 Tick
。每次 Tick
的过程就是查看是否有事件待处理,如果有,就取出事件相关回调函数。如果存在关联的回调函数,就执行它们。然后进入下一次循环,如果不再有事件处理,就退出进程。
在每次 Tick
的过程中,如何判断事件需要处理呢?这里引入的概念是观察者。每个事件循环都有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。
在 Node
中,事件主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等,观察者对事件进行分类。
事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。
这时,在 JavaScript
线程层面的异步调用第一阶段就结束了, JavaScript
线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到 JavaScript
线程的后续执行。一旦线程池中的任务执行完毕,便会通知到 JavaScript
线程执行相关回调。
process.nextTick() 方法与 setImmediate() 方法
除 JavaScript
自身所有的 setTimeout
和 setInterval
两个非I/O的异步API之外, Node.js
自身还具有 process.nextTick
和 setImmediate
方法。
process.nextTick
不是 setTimeout(fn, 0)
的别名。它更加有效率。事件轮询随后的 Tick
调用,会在任何I/O事件(包括定时器)之前运行。
setImmediate
也是延时执行一个回调, callback
函数会按照它们被创建的顺序依次执行。 每次事件循环迭代都会处理整个回调队列。 如果一个立即定时器是被一个正在执行的回调排入队列的,则该定时器直到下一次事件循环迭代才会被触发。
如果 callback 不是一个函数,则抛出 TypeError
。
按照观察者来分, process.nextTick
属于 idle
观察者, setImmediate
属于 check
观察者。在每一轮事件循环中, idle
观察者先于 I/O
观察者, I/O
观察者先于 check
观察者。
看下面这个例子就明白了:
process.nextTick(() => {
console.log('nextTick延时执行1');
});
process.nextTick(() => {
console.log('nextTick延时执行2');
});
setImmediate(() => {
console.log('setImmediate延时执行1');
process.nextTick(() => {
console.log('强势插入');
});
});
setImmediate(() => {
console.log('setImmediate延时执行2');
});
console.log('正常执行');
这段代码摘自 《深入浅出Node.js》 by 朴灵
,但在我实际操作时,实际上由于现在已经是 Node 8.4.0
版本了,运行结果已经与原书不一致
读者可以自行下来运行一遍,便大概懂的几个观察者的优先级。
高性能 Node 服务器
一般说来服务器模型分为以下几种:
- 同步式的服务,一次只能处理一个请求,处理时别的请求都处于等待状态。
- 多线程的服务,如
Apache
,对每个请求启动一个线程来处理,但线程的创建和销毁以及切换需要消耗CPU资源,因而并不如单线程服务高效。 - 基于事件的服务,如
Node.js
、Nginx
,无需进行线程上下文切换,消耗CPU资源较少。
然而,比起 Nginx
,虽然其自身具有反向代理和负载均衡机制,但其性能仍然不如 Node.js
服务器。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论