深入理解Event Loop

浏览器环境

基本概念

为了协调事件、用户交互、脚本、呈现、网络等,UA必须使用事件循环的机制。事件循环有两种模式:用于browsing contexts(浏览环境上下文)的循环,以及用于workers的循环。

  • browsing contexts事件循环
    每个browsing contexts event loop个至少含有一个browsing context上下文环境,该事件循环依赖与环境,环境消失的话该事件机制也将销亡.
  • workers 事件循环
    workers与此类似 每个worker 有一个事件循环,并通过worker processing model 管理事件循环的生命周期.

    task queue(任务队列)

    一个事件循环会有一个或者多个任务队列。任务队列是一个有序的list集合,用来处理下面的任务:

    • Events 任务的分发
    • Parsing 解析处理,例如HTML parser。
    • Callbacks 处理回调任务
    • Using a resource 异步获取一个资源
    • DOM manipulation 操作DOM

task(任务)

  • 任务被定义是来自指定的任务源,来自同一个指定任务源的任务总是会被注入到指定事件循环的同一个任务队列,但是来自不同任务源的任务是可能被放到不同的队列,也可能放到同一队列。
  • 每个事件循环有一个当前运行时任务,初始化时候为空,被用来处理被注入的事件。每个事件循环同事还有个microtask的flag用来检测微任务默认情况为false。来用防止注入事件时调用microtask检测点算法。

Processing model(事件循环处理器)

一个事件循环其实就是在不断运行下面这些步骤的操作:

1. 把*oldestTask*标记为*oldest task*. UA可以选择任意任务队列,如果没有选择跳到*Microtasks*微任务处理
2. 将**当前运行任务**设置成 *oldestTask*.
3. 运行 *oldestTask*.
4. 将事件循环的**当前运行任务** 置为`null`
5. 从任务队列中移除*oldestTask*.
6. Microtasks: 微任务检测点,执行微任务检测.
7. 更新渲染(Update rendering) 主要是浏览器渲染过程不详细展开.
8. worker事件循环判断.

微任务检测

  1. 将微任务检测点flag设置为true
  2. 当事件循环的微任务队列不为空,执行下面操作与上述操作类似:
    1. oldestMicrotask ->oldest microtask.
    2. 当前运行任务设置成 oldestMicrotask.
    3. 运行 oldestMicrotask.
    4. 将事件循环当前运行任务 置为null
    5. 从微任务队列中移除oldestMicrotask.
  3. 每次与事件循环相关的环境对象设置.
  4. 清理索引数据库事务
  5. 将微任务检测点flag设置为false

事件循环

上述概念为whatwg规范概念,具体实现还得看浏览器厂商。之所以称为事件循环,是因为它经常被用于类似如下的方式来实现:

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

由于js的运行环境是单线程的,函数调用会形成了一个栈帧,对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域 ,除此之外JavaScript 运行时还包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈拥有足够内存时,从队列中取出一个消息进行处理。
看图来分析如下:
js_runtime
主线程运行时函数会被压入函数调用栈等待执行,当调用栈中的函数被调用时,任务分发器会根据任务源把对应任务放入不同的event队列中。如webaips产生的回调会被放入回调队列,微任务microtask会被放入微任务队列。事件循环处理器也就是event loop按照前面的步骤做loop。

注意

  • 一个进程中,事件循环是唯一的,但是任务队列可以拥有多个。
  • 每个任务会在任务结束后进行微任务检查并处理微任务。
  • 任务队列又分为macrotask(宏任务)与microtask(微任务),在最新标准中,它们被分别称为task与jobs.
  • 常见macrotask和microtask
    • macrotask ->script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering.
    • microtask -> process.nextTick, Promise, Object.observe, MutationObserver
  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
  • 同种任务顺序优先级
    • macrotask:script(整体代码)->setTimeout(setInterval同源)->setImmediate
    • microtask:process.nextTick->Promise(then)

Node.js 环境

概念

当Node.js启动时会初始化event loop,下图是一个简化版的事件循环执行顺序,大体分为六个阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

  • timers 阶段: 这个阶段执行setTimeoutsetInterval的回调.
  • I/O callbacks 阶段: 执行除了被close或者由定时器,setImmediate所产生的回调外的所有回调.
  • idle, prepare 阶段: 内部使用.
  • poll 阶段: 检索新的IO事件,node有时会阻塞这里.
  • check 阶段: 调用setImmediate的回调.
  • close callbacks 阶段: 一些关闭回调的操作,例如,socket.on('close', ...).

详细

timer 定时器

对于定时器和浏览器API类似,需要注意的是定时器何时执行是由poll阶段决定的,所有精确度不够有时可能被延迟。

poll

poll阶段主要做两件事:

  • 执行到达timer时间的脚本.
  • 处理poll队列中的事件.

如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:

  • 如果poll队列不为空,event loop将同步的执行队列里的callback,直至队列为空,或执行的callback到达系统内存限制
  • 如果poll 队列为空,将会发生下面情况:
    • 如果代码已经被setImmediate设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的事件.
    • 如果代码没有设定setImmediate,event loop将阻塞在该阶段等待callbacks加入poll队列.

一旦poll队列为空,事件循环将检查timer是否达到时间,如果有一个或者多个timer到达时间事件循环就会返回到timers阶段执行timers的回调.

check

这个阶段容许立即执行一个回调在poll阶段完成后,如果poll阶段处于空闲状态并且setImmediate的回调已经入队。那么事件循环将进入check阶段而不是等待.

参考

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context
https://www.jianshu.com/p/de7aba994523
https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
https://cnodejs.org/topic/57d68794cb6f605d360105bf