1 of 30

浏览器和Node中不同的Event Loop

方凌琳 27 April

2 of 30

浏览器

  • 在HTML规范中EL中的定义
  • EL在浏览器中的循环过程
  • task/microtask

Node

  • 6 phase 概览/详述
  • EL在Node中的循环过程
  • process.nextTick/setImmediate
  • setImmediate/setTimeout

内容

3 of 30

js是单线程的,EL机制实现异步

前提 所有代码皆在主线程调用栈完成执行

时机 当主线程任务清空后,轮询任务队列中的任务

4 of 30

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');�})

做道题:)

5 of 30

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)

6 of 30

同步任务直接进入 主执行栈 中执行

等待主执行栈中任务执行完毕,

由EL将异步任务推入主执行栈中执行

7 of 30

一个EL中有一个或多个task队列

来自不同任务源的task会放入不同的task队列中

task执行顺序是由进入队列的时间决定的,先进队列的先被执行

task

8 of 30

典型任务源

  • DOM操作任务源:响应DOM操作
  • 用户交互任务源:对用户交互作出反应,例如键盘或鼠标输入
  • 网络任务源:响应网络活动
  • history traversal任务源:当调用history.back()等类似的api时,将任务插进task队列

还有:)

  • script同步代码
  • setTimeout/setInterval定时器
  • I/O
  • UI交互
  • setImmediate(nodejs环境中)

9 of 30

一个EL中只有一个microtask队列

通常下面几种任务被认为是microtask

  • promise(promise的thencatch才是microtask,创建Promise实例是同步执行的,视为task)
  • MutationObserver
  • process.nextTick(nodejs环境中)

microtask

10 of 30

EL循环过程

  1. 在所有task队列中选择一个最早进队列的task,用户代理可以选择任何task队列,如果没有可选的任务,则跳到 6 Microtasks
  2. 将前一步选择的task设置为currently running task
  3. Run: 运行被选择的task
  4. 运行结束后,将event loop的 currently running task 置为 null
  5. 将运行过的task从它的task queue中移除
  6. Microtasks: 执行microtasks任务检查点(也就是执行microtasks队列里的任务)
  7. 更新渲染
  8. 回到第1步

11 of 30

while (true) {� 宏任务队列.shift()� 微任务队列全部任务()�}

12 of 30

one task,all microtasks (,update rending),

one task,all microtasks (,update rending),

one task,all microtasks (,update rending),

… ...

13 of 30

好了,回顾一下一开始的题目:)

14 of 30

15 of 30

Node

下半场开始 :)

Node中的EL由 libuv库 实现,它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,自然也是event loop的源泉

16 of 30

6 main phases

17 of 30

概览

  • Timers 执行setTimeout/setInterval设定的回调函数
  • I/O callbacks 一般是一些系统调用,比如tcp连接失败error的callback
  • Idle(空转), prepare 此阶段只在内部使用
  • Poll(轮询) 检索新的I/O事件; 执行几乎所有的回调,除了timers、check、close callbacks阶段的回调
  • Check 执行setImmediate() 设定的回调函数
  • Close callbacks 执行关闭事件的回调,比如 socket.on('close', ...)

18 of 30

Timers

在这个阶段检查是否有到达阈值的timer(setTimeout/setInterval),有的话就执行他们的回调

但timer设定的阈值不是执行回调的确切时间(只是最短的间隔时间),node内核调度机制和其他的回调函数会推迟它的执行

由poll阶段来控制什么时候执行timers callbacks

19 of 30

I/O callbacks

处理异步事件的回调,比如网络I/O,比如文件读取I/O。当这些I/O动作都结束的时候,在这个阶段会触发它们的回调。

20 of 30

poll

poll 阶段有两个主要的功能

  • 处理poll queue的callbacks
  • 回到timers phase执行timers callbacks(当到达timers指定的时间时)

由于其它各个阶段的操作都有可能导致新的事件发生,并使得内核向poll queue中添加事件,所以在poll阶段处理事件的时候可能还会有新的事件产生,最终,长时间的调用回调函数将会导致定时器过期,所以在poll阶段与定时器会有"合作"

一次性的模式 里面就会重新检查一下timers,阈值达到就执行回调

21 of 30

event loop进入了poll阶段,未设定timer

  • poll queue不为空,event loop将同步的执行queue里的callback,直到清空或执行的callback到达系统上限
  • poll queue为空
    • 如果有设定setImmediate() callback, event loop将结束poll阶段进入check阶段,并执行check queue (check queue是 setImmediate设定的)
    • 如果代码没有设定setImmediate() callback,event loop将阻塞在该阶段等待callbacks加入poll queue

event loop进入了 poll阶段,设定了timer

  • 如果poll进入空闲状态,event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将回到 timers 阶段执行timers queue

22 of 30

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

23 of 30

check

close

一旦poll队列闲置下来或者是代码被setImmediate调度,EL会马上进入check phase

关闭I/O的动作,比如文件描述符的关闭,连接断开等。

如果socket突然中断,close事件会在这个阶段被触发

24 of 30

循环过程

循环开始之前

  • 所有同步任务
  • 同步任务中的异步操作发出异步请求
  • 规划好同步任务中的定时器生效时间
  • 清理执行process.nextTick()

开始循环

  1. 清空当前循环内的 Timers Queue,清空NextTick Queue,清空Microtask Queue
  2. 清空当前循环内的 I/O Queue,清空NextTick Queue,清空Microtask Queue
  3. poll情况比较复杂(前面已经分析过了)
  4. 清空当前循环内的 Check Queue,清空NextTick Queue,清空Microtask Queue
  5. 清空当前循环内的 Close Queue,清空NextTick Queue,清空Microtask Queue
  6. 进入下一轮循环

25 of 30

while (true) {� loop.forEach((阶段) => {� 阶段全部任务()� nextTick全部任务()� microTask全部任务()� })� loop = loop.next�}

26 of 30

process.nextTick()

process.nextTick() 不是Node的EL中的一部分(虽然它也是异步API),但是,任意阶段的操作结束之后 nextTickQueue 就会被处理。

process.nextTick() vs setImmediate()

通过process.nextTick()触发的回调会在进入下一阶段前被执行结束,如果用户递归调用 process.nextTick() 造成I/O被榨干,使EL不能进入poll阶段。

因此node作者推荐我们尽量使用setImmediate,因为它只在check阶段执行,不至于导致其他异步回调无法被执行到

27 of 30

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 可以实现相同的效果。

28 of 30

setTimeout vs setImmediate

  • setImmediate 一旦当前poll阶段结束(poll queue为空或执行任务到达上限)就执行一次脚本
  • setTimeout 设定一个最短的调度该脚本的时间阈值

只要是在同一个I/O周期内,不管存在多少定时器,setImmediate()设置的回调总是在setTimeout()回调之前执行

fs.readFile(__filename, () => {� setTimeout(() => {� console.log('timeout');� }, 0);� setImmediate(() => {� console.log('immediate');� });�});

29 of 30

30 of 30

以上,谢谢 :)