一、概览
event loop(事件循环)是一个执行模型浏览器和NodeJS基于不同的技术实现了各自的Event Loop。本文主要讲述Node中的 EventLoop。
1. 浏览器的Event Loop
浏览器的Event Loop是在Html5规范(参考:html.spec.whatwg.org/multipage/w… Loop模型,具体的实现流程了浏览器厂商。
2. NodeJS的Event Loop
NodeJS的Event Loop是基于libuv(跨平台异步IO库)实现的。可以参考Node的官方文档以及libuv的官方文档。libuv已经对Event Loop做出了实现(参考:github.com/libuv/libuv…
二、浏览器中的EventLoop
参见: 浏览器中的EventLoop
三、Node中的EventLoop
1.Node Task分类
2. Node Task Queue说明
这一部分摘抄自node官网,加了部分说明。详细的说一下每种类型的Task Queue
- 定时器(timer):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。(也就是那些到时间应该执行的定时器回调函数。)
- 待定回调(pending callbacks):执行延迟到下一个循环迭代的 I/O 回调。(这个阶段参考poll阶段的说明2)
- idle, prepare:仅系统内部使用。
- 轮询(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。(说明:1. 适当的时候:已经没有其它可执行的任务的时候,此时还有异步的I/O没有返回;此时程序会在此阻塞,等待I/O的回调;2. 当队列用尽或达到回调限制,事件循环将移动到下一阶段执行,也就是到待定回调pending callbacks_阶段_执行 )
- 检测(check):setImmediate() 回调函数在这里执行。
- 关闭的回调函数(close callbacks):一些关闭的回调函数,如:socket.on('close', ...)。
3.js代码、函数调用栈、任务队列
通过下面的代码以及gif图了解一下js代码,函数调用栈,任务队列是如何配合执行代码的。
setTimeout(() => { console.log("timer"); Promise.resolve().then(() => { console.log("promise"); });});setTimeout(() => { console.log("timer2"); Promise.resolve().then(() => { console.log("promise2"); });});
4.代码示例
通过以下代码印证一下上面说到的流程
const fs = require('fs');
console.log('1.同步代码!'); // 同步代码一定比异步代码先执行
process.nextTick(() => { // 整个函数我们理解为一个宏任务,当整个代码执行完,
// 会执行此宏任务产生的微任务,process.nextTick 会立即执行 console.log('3.process.nextTick!');
});
setImmediate(() => { // check阶段执行
console.log('5.setImmediate!');
});
Promise.resolve().then(() => { // 微任务,等待宏任务执行完毕后执行,执行顺序在process.nextTick 之后
console.log('4.Promise.resolve then!');
});
setTimeout(() => { // 1s后会将此会将此setTimeout的回调推送到 timer阶段对应的task queue中
console.log('7.setTimeout');
process.nextTick(() => {
console.log('8.setTimeout process.nextTick!');
});
Promise.resolve().then(() => {
console.log('9.setTimeout Promise.resolve then');
});
}, 1000);
fs.readFile('./event.js', () => { // I/O 回调在poll阶段执行,具体何时执行以来fs.readFile异步读取文件的所需时间
console.log('6.fs.readFile');
});
console.log('2.同步代码!');
// 以下是输出:// 1.同步代码!
// 2.同步代码!
// 3.process.nextTick!
// 4.Promise.resolve then!
// 5.setImmediate!
// 6.fs.readFile
// 7.setTimeout
// 8.setTimeout process.nextTick!
// 9.setTimeout Promise.resolve then
5.流程图解析
下面流程图标注了几个关键点。
1.poll阶段是如何工作的
2.pending callbacks阶段的I/O指的是什么
3.poll阶段执行完毕后,不同的处理情况
4.poll阶段介绍中说在恰当的时候阻塞在此处,什么事恰当的时候
6.事件循环的源码
以下是node事件循环的核心代码(摘自网络)
// https://github.com/libuv/libuv/blob/v1.x/src/unix/core.c
int uv_run(uv_loop_t * loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
// 从uv__loop_alive中我们知道event loop继续的条件是以下三者之一:
// 1,有活跃的handles(libuv定义handle就是一些long-lived objects,例如tcp server这样)
// 2,有活跃的request
// 3,loop中的closing_handles
r = uv__loop_alive(loop);
if (!r) uv__update_time(loop);
while (r != 0 && loop -> stop_flag == 0) {
// 更新时间变量,这个变量在uv__run_timers中会用到
uv__update_time(loop);
// timers阶段
uv__run_timers(loop);
// 从libuv的文档中可知,这个其实就是I/O callback阶段,ran_pending指示队列是否为空
ran_pending = uv__run_pending(loop);
// idle阶段
uv__run_idle(loop);
// prepare阶段
uv__run_prepare(loop);
// 设置poll阶段的超时时间,以下几种情况下超时会被设为0,这意味着此时poll阶段不会被阻塞,在下面的poll阶段我们还会详细讨论这个
// 1,stop_flag不为0
// 2,没有活跃的handles和request
// 3,idle、I/O callback、close阶段的handle队列不为空
// 否则,设为timer阶段的callback队列中,距离当前时间最近的那个
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阶段
uv__run_closing_handles(loop);
// 如果mode == UV_RUN_ONCE(意味着流程继续向前)时,在所有阶段结束后还会检查一次timers,这个的逻辑的原因不太明确
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;
}
四、几个问题
1.setTimeout && setImmediate执行顺序
setTimeout(() => {
console.log('1.setTimeout');
},0);
setImmediate(() => {
console.log('2.setImmediate');
});
分两种情况
1.当 setTimeout() 和 setImmediate() 都写在 main 里面的时候 不一定谁先执行谁后执行;这个时候受线程性能的影响。setTimeout 0 在node中会被设置为1ms后执行。时间循环的准备需要时间,如果事件循环的准备时间大于1ms那 setTimeout 0先执行。反之 setImmediate 先执行。
2.当 setTimeout() 和 setImmediate() 都写在一个 I/O 回调 或者说一个 poll 类型宏任务的回调里面的时候 一定是先执行 setImmediate() 后执行 setTimeout()
2. process.nextTick&&setImmediate
setImmediate可理解为是个宏任务,在check阶段触发执行。
process.nextTick可以理解为是一个微任务,我们知道微任务是在宏任务执行的间隙去执行,所以当一个(种)任务队列执行完毕后,会执行微任务;而process.nextTick会最优先被执行。
3.以下代码执行顺序
setTimeout(() => {
console.log('1.setTimeout');
Promise.resolve().then(() => {
console.log('1.Promise.resolve');
})
});
setTimeout(() => {
console.log('2.setTimeout');
Promise.resolve().then(() => {
console.log('2.Promise.resolve');
})
});
// ==>执行结果1
// 1.setTimeout
// 2.setTimeout
// 1.Promise.resolve
// 2.Promise.resolve
// ==>执行结果2
// 1.setTimeout
// 1.Promise.resolve
// 2.setTimeout
// 2.Promise.resolve
TODO 原因暂时不理解待补充?
4.setTimeout的预设时间是否一定准确
Node 并不能保证 timers 在预设时间到了就会立即执行,因为 Node 对 timers 的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲,以下是官网的例子
const fs = require('fs');
function someAsyncOperation(callback) {
fs.readFile('/path/to/file', callback); // 假设这个任务95ms完成, callback执行需要花费10ms
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100); // 100ms后,此时主线程正在执行fs.readFile的回调;需要等待105ms时才会执行此定时器
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
while (Date.now() - startCallback < 10) {
// do nothing
}
});
参考文章: