node事件队列

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。

var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function (){}; req.onerror = function (){}; req.send();

它与下面的写法等价
var req = new XMLHttpRequest(); req.open('GET', url); req.send(); req.onload = function (){}; req.onerror = function (){}; 
因为req.send方法是Ajax操作向服务器发送数据,它是一个异步任务;而指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。


下面代码的运行结果
setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); (() => console.log(5))();


5
3
4
1
2


异步任务可以分成两种,本轮循环一定早于次轮循环执行;
Node 规定,process.nextTickPromise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeoutsetIntervalsetImmediate的回调函数,追加在次轮循环。
process.nextTick是在本轮循环执行的,而且是所有异步任务里面最快执行的。
Promise对象的回调函数,会进入异步任务里面的"微任务"(microtask)队列,微任务队列追加在process.nextTick队列的后面,也属于本轮循环。

本轮循环的执行顺序
  1. 同步任务
  2. process.nextTick()
  3. 微任务

事件循环的初始化

  • 同步任务
  • 发出异步请求
  • 规划定时器生效的时间
  • 执行process.nextTick()等等

上面这些事情都干完了,事件循环就正式开始了

  1. timers
    1. 处理setTimeout()setInterval()的回调函数。
  2. I/O callbacks
    1. 执行除了setTimeout、setInterval、setImmediate、关闭请求外的其他回调函数
  3. poll
    1. 等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等
  4. check
    1. 执行setImmediate()的回调
  5. close callbacks
    1. 执行关闭请求的回调函数
// 异步任务一:100ms 后执行的定时器
setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms`); }, 100);  // 异步任务二:文件读取后,有一个 200ms 的回调函数 fs.readFile('test.js', () => { const startCallback = Date.now(); while (Date.now() - startCallback < 200) {  // 什么也不做 } });


第一轮事件循环以后,没有到期的定时器,也没有已经可以执行的 I/O 回调函数,所以会进入 Poll 阶段,等待内核返回文件读取的结果。由于读取小文件一般不会超过 100ms,所以在定时器到期之前,Poll 阶段就会得到结果,因此就会继续往下执行。
第二轮事件循环,依然没有到期的定时器,但是已经有了可以执行的 I/O 回调函数,所以会进入 I/O callbacks 阶段,执行fs.readFile的回调函数。这个回调函数需要 200ms,也就是说,在它执行到一半的时候,100ms 的定时器就会到期。但是,必须等到这个回调函数执行完,才会离开这个阶段。
第三轮事件循环,已经有了到期的定时器,所以会在 timers 阶段执行定时器。最后输出结果大概是200多毫秒。

setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行。所以,setTimeout会早于setImmediate完成。
setTimeout(() => console.log(1)); setImmediate(() => console.log(2));
但是实际执行的时候,结果却是不确定,有时还会先输出2,再输出1
因为setTimeout的第二个参数默认为0。但是实际上,Node 做不到0毫秒,最少也需要1毫秒;
进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); });

一定是先输出2,再输出1。先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。


process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0)

// 1
// 2
// TIMEOUT FIRED
如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。
 
setImmediate(function (){ setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0); }); // 1 // TIMEOUT FIRED // 2

因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。
process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!




    



猜你喜欢

转载自www.cnblogs.com/raferxu/p/10089245.html