浏览器和Node中不同的Event Loop
方凌琳 27 April
浏览器
Node
内容
js是单线程的,EL机制实现异步
前提 所有代码皆在主线程调用栈完成执行
时机 当主线程任务清空后,轮询任务队列中的任务
setTimeout(() => console.log('setTimeout1'), 0);�setTimeout(() => {� console.log('setTimeout2');� Promise.resolve().then(() => {� console.log('promise3');� Promise.resolve().then(() => {� console.log('promise4');� })� console.log(5)� })� setTimeout(() => console.log('setTimeout4'), 0);�}, 0);�setTimeout(() => console.log('setTimeout3'), 0);�Promise.resolve().then(() => {� console.log('promise1');�})
做道题:)
EL在HTML规范中的定义
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
为了协调事件、用户交互、脚本、UI渲染、网络请求等行为,用户引擎必须使用Event Loop。EL包含两类:基于browsing contexts,基于worker。二者独立。
(我们今天讨论的EL基于browsing contexts)
同步任务直接进入 主执行栈 中执行
等待主执行栈中任务执行完毕,
由EL将异步任务推入主执行栈中执行
一个EL中有一个或多个task队列
来自不同任务源的task会放入不同的task队列中
task执行顺序是由进入队列的时间决定的,先进队列的先被执行
task
典型任务源
还有:)
一个EL中只有一个microtask队列
通常下面几种任务被认为是microtask
microtask
EL循环过程
while (true) {� 宏任务队列.shift()� 微任务队列全部任务()�}
one task,all microtasks (,update rending),
one task,all microtasks (,update rending),
one task,all microtasks (,update rending),
… ...
好了,回顾一下一开始的题目:)
Node
下半场开始 :)
Node中的EL由 libuv库 实现,它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,自然也是event loop的源泉
6 main phases
概览
Timers
在这个阶段检查是否有到达阈值的timer(setTimeout/setInterval),有的话就执行他们的回调
但timer设定的阈值不是执行回调的确切时间(只是最短的间隔时间),node内核调度机制和其他的回调函数会推迟它的执行
由poll阶段来控制什么时候执行timers callbacks
I/O callbacks
处理异步事件的回调,比如网络I/O,比如文件读取I/O。当这些I/O动作都结束的时候,在这个阶段会触发它们的回调。
poll
poll 阶段有两个主要的功能
由于其它各个阶段的操作都有可能导致新的事件发生,并使得内核向poll queue中添加事件,所以在poll阶段处理事件的时候可能还会有新的事件产生,最终,长时间的调用回调函数将会导致定时器过期,所以在poll阶段与定时器会有"合作"
一次性的模式 里面就会重新检查一下timers,阈值达到就执行回调
event loop进入了poll阶段,未设定timer
event loop进入了 poll阶段,设定了timer
const fs = require('fs');��function someAsyncOperation(callback) {� // Assume this takes 95ms to complete� fs.readFile('/path/to/file', callback);�}��const timeoutScheduled = Date.now();��setTimeout(() => {� const delay = Date.now() - timeoutScheduled;�� console.log(`${delay}ms have passed since I was scheduled`);�}, 100);��someAsyncOperation(() => {� const startCallback = Date.now();�� // do something that will take 10ms...� while (Date.now() - startCallback < 10) {}�});
105ms have passed since I was scheduled
check
close
一旦poll队列闲置下来或者是代码被setImmediate调度,EL会马上进入check phase
关闭I/O的动作,比如文件描述符的关闭,连接断开等。
如果socket突然中断,close事件会在这个阶段被触发
循环过程
循环开始之前
开始循环
while (true) {� loop.forEach((阶段) => {� 阶段全部任务()� nextTick全部任务()� microTask全部任务()� })� loop = loop.next�}
process.nextTick()
process.nextTick() 不是Node的EL中的一部分(虽然它也是异步API),但是,任意阶段的操作结束之后 nextTickQueue 就会被处理。
process.nextTick() vs setImmediate()
通过process.nextTick()触发的回调会在进入下一阶段前被执行结束,如果用户递归调用 process.nextTick() 造成I/O被榨干,使EL不能进入poll阶段。
因此node作者推荐我们尽量使用setImmediate,因为它只在check阶段执行,不至于导致其他异步回调无法被执行到
process.nextTick()
nextTickQueue & microtasks
日常应用中经常会将 promise、process.nextTick、nextTickQueue、microtask 混为一谈,其实真正注册为 microtask 的任务的目前只有 promise。但是问题来了,v8 目前是没有暴露 runMicrotasks ,也就是说我们目前还没有办法通过内核的 API 执行 microtask queue 的任务。
Node.js 最终选择的实现方法是将 microtask queue 的任务通过一个 runMicrotasks 对象暴露给上游,然后通过 nextTick 方法把它们推进了 nextTickQueue,也就是说最终 microtask queue 的任务变成了 nextTickQueue 的任务,所以我们用 promise.then 和 process.nextTick 可以实现相同的效果。
setTimeout vs setImmediate
只要是在同一个I/O周期内,不管存在多少定时器,setImmediate()设置的回调总是在setTimeout()回调之前执行
fs.readFile(__filename, () => {� setTimeout(() => {� console.log('timeout');� }, 0);� setImmediate(() => {� console.log('immediate');� });�});
以上,谢谢 :)