node中的Event模块(上)
SunShinewyf opened this issue · 7 comments
前言:最近对
node
底层一些东西不是很深入,趁这段时间整理一些理论知识
js中的事件循环(Event Loop)
Event Loop
是指在js
执行环境中存在主执行线程和任务队列(Task Queue
),其中所有同步任务都在主执行线程中形成一个执行栈,所有异步任务都会放到任务队列中。Event Loop
会经历如下过程:
- 主线程执行同步任务,在主线程执行过程中,不断形成堆栈并执行出栈入栈的操作
- 主线程任务是否执行完毕,如否,继续循环第1步,如是,则执行下一步
- 系统读取任务队列里的任务,进入执行栈,开始执行
- 不断循环执行前三步
参考资料:
macrotask
和microtask
上面说的异步任务中,分为macrotask
(宏任务)和microtask
(微任务)两类,在挂起任务中,Js
引擎会按照类别将任务分别存放在这两种类型任务中。这两种任务执行的顺序如下:
- 先取出
macrotask
任务队列中的第一个任务进行执行 - 执行完毕后取出
microtask
中的所有任务顺序执行 - 再取
macrotask
中的剩余任务执行 - 重复前面三个步骤
这个步骤通过一个图来展示会比较直观:
图中stack
表示主执行线程中的同步任务,而Background Threads
则是指macrotask
,在执行完主线程之后,会取出Macrotask Queue
(也叫Task Queue
)中的第一个任务setInterval
执行,执行完毕之后就会顺序执行下面的Microtask Queue
,直到所有Microtask Queue
中的任务都执行完毕了之后,才会执行下一个Macrotask
。
其中macrotask
类型包括:
script
整体代码setTimeout
setInterval
setImmediate
I/O
UI rendering
microtask
类型包括:
process.nextTick
Promise
(这里指浏览器实现的原生promise
)Object.observe
MutaionObserver
参考资料:
- 理解事件循环二(macrotask和microtask)
- Difference between microtask and macrotask within an event loop context
通过一段代码来验证一下上面的理论:
console.log('start')
setTimeout(() => {
console.log('setTimeout1');
},0);
const myInterval = setInterval(() => {
console.log('setInterval');
},0)
setTimeout(() => {
console.log('setTimeout2');
Promise.resolve().then(() => {
console.log('promise3');
})
setTimeout(() => {
console.log('setTimeout3');
clearInterval(myInterval);
},0)
},0)
Promise.resolve()
.then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
})
console.log('end');
这段代码最后的输出结果如下:
start
end
promise1
promise2
setTimeout1
setInterval
setTimeout2
promise3
setInterval
setTimeout3
大概讲解一下流程:
- 首先整段
script
相当于一个Macrotask
,它是Macrotask Queue
中的第一个任务,先执行,所以打印出start
、end
Promise
相当于一个Microtask
,按照之前的理论,会先顺序执行完所有的Microtask
,所以此时会打印promise1
和promise2
。- 执行完所有的
Microtask
之后,会将setTimeout1
和setInterval
推进Macrotask Queue
中,并且会执行此时Macrotask Queue
的第一个任务,也就是setTimeout1
,此时打印出setTimeout1
。 - 而此时
Microtask
还是为空,所以会继续执行下一个Macrotask
,也就是setInterval
,此时打印出setInterval
- 在执行
setInterval
的task
时,会将下一个setTimeout
继续推进Macrotask Queue
,而且此时Microtask
仍然为空,继续执行下一个Macrotask
,所以打印出setTimeout2
- 在执行完
setTimeout2
的时候,setTimeout2
里面的promise
已经推进Microtask Queue
中,所以此时会执行完Microtask Queue
中的任务,打印出promise3
- 在执行
Microtask Queue
的时候,一直执行的setInterval
后面的setTimeout3
会继续被推进Macrotask Queue
中,并且依次执行,直到setInterval
被取消。
node中的Event Loop
根据node
官方文档的描述,node
中的Event Loop
主要有如下几个阶段:
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
各个阶段执行的任务如下:
timers
阶段: 这个阶段执行setTimeout
和setInterval
预定的callback
;- I/O callbacks 阶段: 执行除了
close
事件的callbacks
、被timers
设定的callbacks
、setImmediate()
设定的callbacks
这些之外的callbacks
; idle, prepare
阶段: 仅node
内部使用;poll
阶段: 获取新的I/O
事件, 适当的条件下node
将阻塞在这里;check
阶段: 执行setImmediate()
设定的callbacks
;close callbacks
阶段: 执行socket.on('close', ...)
这些callback
process.nextTick()
process.nextTick()
并没有在Event Loop
的执行阶段中,而是在Event Loop
两个阶段之间运行,根据上面说的,process.nextTick()
属于microtask
任务类型。
根据process.nextTick()
的运行性质,可以整理出下面的简图:
也就是process.nextTick()
有可能插入在Event Loop
各个阶段中
setTimeout(fn,0)
Vs setImmediate
Vs process.nextTick()
setTimeout(fn,0)
Vs setImmediate
setTimeout(fn,0)
在timer
阶段执行,并且是在poll
阶段进行判断是否达到指定的time
时间才会执行setImmediate
在check
阶段才会执行
两者的执行顺序要根据当前的执行环境才能确定,根据官方文档总结得出的结论是:
- 如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,即随机。
- 如果两者都不在主模块调用(即在一个 IO circle 中调用),那么setImmediate的回调永远先执行。
setImmediate
Vs process.nextTick()
setImmediate()
属于check
观察者,其设置的回调函数,会插入到下次事件循环的末尾,每次事件循环只执行链表中的一个回调函数。process.nextTick()
所设置的回调函数会存放到数组中,一次性执行所有回调函数。process.nextTick()
调用深度的限制,上限是1000,而setImmediate
没有;
先来看一段代码:
setImmediate(() => console.log('immediate1'));
setImmediate(() => console.log('immediate2'));
setTimeout(() => console.log('setTimeout1'), 1000);
setTimeout(() => {
console.log('setTimeout2');
process.nextTick(() => console.log('nextTick1'));
}, 0);
setTimeout(() => console.log('setTimeout3'), 0);
process.nextTick(() => console.log('nextTick2'));
process.nextTick(() => {
process.nextTick(console.log.bind(console, 'nextTick3'));
});
process.nextTick(() => console.log('nextTick4'));
在控制台中执行node index.js
,得到的结果如下:
nextTick2
nextTick4
nextTick3
setTimeout2
setTimeout3
nextTick1
immediate1
immediate2
setTimeout1
分析如下:
- 在
node
中,nextTick
的优先级高于setTimeout
和setImmediate()
,所以会先执行nextTick
里面的信息打印。 - 但是对于嵌套的
nextTick
,会慢于同步的nextTick
,所以nextTick4
会先于nextTick3
- 然后开始一个
Event Loop
过程,首先执行timer
阶段,而此时setTimeout
所需要等待的时间是0,所以立即执行setTimeout2
和setTimeout3
里面的逻辑。而setTimeout1
由于设置了执行时间,不满足执行条件,被放到下一轮Event Loop
- 当前
Event Loop
执行到check
阶段,于是打印出immediate1
、immediate2
- 执行后面的
Event Loop
,当setTimeout1
达到执行条件时执行
参考资料:
-Node.js的event loop及timer/setImmediate/nextTick
-Node.js Event Loop 的理解 Timers,process.nextTick()
对node
事件基础的一些总结,有不正确的地方还望指出,共同学习。
setTimeout(fn,0) Vs setImmediate
的执行顺序是随机的,具体要看两者是否在一个I/O循环中进行调用,如果在,setImmediate
始终会比 setTimeout
先执行。因为setImmediate
会在 event loop
中poll
完成之后立即执行,setTimeout
则是到下一个 timers
阶段。
还有,你后面的打印结果是我上面的例子吗
嗯 懂了 这个结果是你上面的例子
因为setTimeout(fn,0) Vs setImmediate
的结果可能是随机的,所以就会出现有的时候setImmediate
在setTimeout(fn,0)
之前执行,有的时候在setTimeout(fn,0)
之后执行
你好,我有两个疑问。
- node和浏览器的不同。在循环过程中,node中每个阶段的任务是一次性拿出,执行完毕后再清空执行microtask/nextTick,再进行下个阶段,而浏览器是执行一个macrotask,就执行清空microtask/nextTick,再进行下个macrotask任务,是这样吗?
- 比如说我在timer阶段执行setTimeout过程中创建的setTimeout是进入下轮loop吗,还是直接放入当前阶段?(我知道如果执行timer时创建了check,肯定会优先放入当前loop,有这个疑问主要是写了很多测试代码,不知道是不是哪里搞错了,过一会执行结果就变了)
@toBeTheLight 正好watch这个blog,看到了你的问题,第二个问题我可以回答你:
setTimeout(fn,time)
这里面的fn是以相同的time为基准,把fn存储到一个双向链表中。当:
setTimeout(() => setTimeout(fn,0),0)
在运行event-loop的timers阶段的时候:
外层的setTimeout会执行此时刻链表里面的所有fn,而只有在执行到外层的fn(即() => setTimeout(fn,0)
)的时候,才能把内部的fn注册到setTimeout中,所以会移到下一个event-loop的timers阶段运行。但是如果内层是setImmediate的时候,会把setImmediate注册到check阶段,而此轮event-loop的check阶段尚未运行,所以会放到此轮的event-loop中运行。有兴趣的话可以看一下timer.js的源码了解一下原理
setImmediate属于check观察者没错,但是并不是每次只执行链表中的一个回调函数,而是一次取出全部执行完:
setImmediate(() => {
console.log('setImmediate1');
process.nextTick(() => {
console.log('nextTick');
});
});
setImmediate(() => {
console.log('setImmediate2');
});
setImmediate(() => {
console.log('setImmediate3');
});
nextTick是最后输出的。