理解 Event Loop
参考文档:
The Node.js Event Loop, Timers, and process.nextTick() 官方文档
Node.js的事件循环(Event Loop)、Timer和process.nextTick()[翻译]
Node.js Event Loop 的理解 Timers,process.nextTick()
牛刀小试
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
process.nextTick(() => {
console.log(6);
});
2 3 5 6 4 1
Promise的构造函数里面的方法是同步执行的 所以先输出 2 3 5 然后 Promise 的 then 属于异步执行的,属于 MicroTask 在 nextTick执行完后执行 所以再输出 6 4 然后
什么是事件循环?
事件循环允许 Node.js 执行非阻塞 I/O 操作 - 尽管JavaScript是单线程的 - 通过尽可能将操作让系统内核执行。
由于大多数现代内核都是多线程的,因此它们可以处理在后台执行的多个操作。当其中一个操作完成时,内核会告诉Node.js,以便可以将相应的回调添加到轮询队列中以最终执行。我们将在本主题后面进一步详细解释。
Event Loop 概述
下图显示了事件循环操作顺序的简要概述。
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ pending callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
- timers:此阶段执行由setTimeout() 和调度的回调setInterval()
- pending callbacks:执行除了 close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks
- idle, prepare:仅在内部使用
- poll:获取新的I/O事件, 适当的条件下node将阻塞在这里
- check:setImmediate()在这里调用回调
- close callbacks:一些关闭回调,例如socket.on('close', ...)
事件循环示例
1.poll 阶段会阻塞
var fs = require('fs');
function someAsyncOperation (callback) {
// 花费2毫秒
fs.readFile(__dirname + '/' + __filename, callback);
}
var timeoutScheduled = Date.now();
var fileReadTime = 0;
setTimeout(function () {
var delay = Date.now() - timeoutScheduled;
console.log('setTimeout: ' + (delay) + "ms have passed since I was scheduled");
console.log('fileReaderTime',fileReadtime - timeoutScheduled);
}, 10);
someAsyncOperation(function () {
fileReadtime = Date.now();
while(Date.now() - fileReadtime < 20) {
}
});
定时器执行 20 ms 以上
- 读取一次文件的操作一般在 3 ms 左右
- 进入初始化时间循环
- timer 阶段,无 callback 到达,设置的是10ms
- 进入 poll, poll 阻塞,2ms 拿到数据,将 callback 加入到 poll queue 中,执行 callback 花费 20 ms, poll 处于空闲状态,但是设置的timer发现超过了该值,立即循环回到timer阶段执行 callback
- 所以最后 setTimeout 实际输出的时间是 22 ms
2.poll 阶段不阻塞
let start = new Date();
setTimeout(() => {
console.log('timer:'+(new Date() - start));
},10);
// 花费35ms
http.get('http://www.baidu.com', function (res) {
console.log('http:'+(new Date() - start));
});
//
timer:13
http:38
定时器执行 10 ms 以上
- 本机http.get 访问百度 35 ms
- 进入初始化时间循环
- timer 阶段,无 callback 到达,设置的是10ms
- 进入 poll, poll 阻塞,当运行了10ms是依然空闲,但设置的timer已到达时间,event loop 需要循环到 timer 阶段,然后执行完timer的callback, 然后继续回到poll阶段阻塞等待io执行完成,当在35ms的时候异步IO完成,将其的callback加入到poll queue中
3.poll 阶段不阻塞
let start = new Date();
setTimeout(() => {
console.log('timer:'+(new Date() - start));
},10);
fs.readFile(__filename, () => {
console.log('fs:'+(new Date() - start));
});
setImmediate(() => {
console.log('check:'+(new Date() - start))
});
//
check:3
fs:9
timer:11
- 没有timer
- 进入poll 阻塞,发现有 setImmediate ,立马进入check
- 没有timer,继续回到 poll 阻塞,然后文件读取完成,callback加入poll queue,执行callback,然后继续阻塞,发现有了timer
- 执行 10 ms 的timer
setTimeout 与 setImmediate
setImmediate并且setTimeout()是相似的,但根据它们被调用的时间以不同的方式表现。
- setImmediate()设计用于在当前poll阶段完成后的check阶段执行脚本 。
- setTimeout() 安排在经过最小阈值(ms)后运行的脚本。
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
上面这段代码的执行顺序是不确定的。
这是因为setTimeout的第二个参数默认为0。但是实际上,Node 做不到0毫秒,最少也需要1毫秒,根据官方文档,第二个参数的取值范围在1毫秒到2147483647毫秒之间。也就是说,setTimeout(f, 0)等同于setTimeout(f, 1)。
实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。
process.nextTick()
您可能已经注意到,process.nextTick()虽然它是异步API的一部分,但未在图中显示。这是因为 process.nextTick()从技术上讲,它不是事件循环的一部分。相反,nextTickQueue无论事件循环的当前阶段如何,都将在当前操作完成后处理。 process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。
var fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
process.nextTick(()=>{
console.log('nextTick3');
})
});
process.nextTick(()=>{
console.log('nextTick1');
})
process.nextTick(()=>{
console.log('nextTick2');
})
});
//
node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
从poll —> check阶段,先执行process.nextTick, nextTick1 nextTick2 然后进入check,setImmediate, setImmediate 执行完setImmediate后,出check,进入close callback前,执行process.nextTick nextTick3 最后进入timer执行setTimeout setTimeout process.nextTick()是node早期版本无setImmediate时的产物,node作者推荐我们尽量使用setImmediate。