Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从一道题浅说 JavaScript 的事件循环 #41

Open
juzhiyuan opened this issue Jul 8, 2018 · 0 comments
Open

从一道题浅说 JavaScript 的事件循环 #41

juzhiyuan opened this issue Jul 8, 2018 · 0 comments

Comments

@juzhiyuan
Copy link
Owner

juzhiyuan commented Jul 8, 2018

改自 dwqs/blog#61

资料


  • JavaScript 在单线程中执行,所有的任务可看作存放在两个队列中:
    1. 执行队列:存放同步代码的任务
    2. 事件队列:存放异步代码的 MacroTask
  • MicroTask 处于 执行队列 与 事件队列之间,当 JavaScript 执行时,优先执行完所有同步任务,遇到异步任务时就会根据其任务类型存放到对应的队列中。当执行完同步任务后,将执行 MicroTask ,最后执行 MacroTask

  • 浏览器与 Node.js 中 事件循环 有差异,本文基于 Browsing Context
  • Event Loop

有如下题目:

new Promise(resolve => {
  resolve(1)
  Promise.resolve().then(() => console.log(2))
  console.log(4)
}).then(t => {
  console.log(t)
})

console.log(3)

// 输出 4 3 2 1

事件循环

JavaScript 是 单线程 的,即同一时间只能做一件事。为了协调事件、用户交互、脚本、UI 渲染与网络处理等行为以及防止主线程被阻塞,事件循环(Event Loop)应运而生

Event Loop 包含两类:

二者运行是独立的,即每一个 JavaScript 运行的线程环境都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop

任务队列

事件循环是通过任务队列机制协调的:

  1. 在一个 Event Loop 中,可以有一个或多个任务队列(Task Queue)
  2. 一个任务队列便是一系列有序任务(Task)的集合
  3. 每个任务都有自己的任务源(Task Source)
  4. 来自同一个任务源的 Task 存放在同一个任务队列,否则存放在不同的任务队列

在事件循环中,每进行一次循环操作称之为 Tick,每一次 Tick 的任务处理模型是复杂的,关键步骤如下:

  1. 在此次 Tick 中选择最先进入队列的任务,如果有则执行(一次)
  2. 检查是否存在 MicroTasks,如果存在,则不停执行,直至清空 MicroTasks Queue
  3. 更新 Render
  4. 主线程重复上述步骤

阅读规范可知,异步任务分为 Task(MacroTask 宏任务) 与 MicroTask(微任务) 两类,不同的 API 注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行

MacroTask 包括:

  • Script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI 交互事件
  • PostMessage
  • MessageChannel
  • setImmediate(Node.js)

MicroTask

  • Promise.then
  • MutationObserver
  • Process.nextTick(Node.js)

注意:

  • 在 Node.js 中,会优先清空 nextTick Queue,再清空其它 Queue(如 Promise)
  • timers(setTimeout、setInterval)将优先于 setImmediate 执行,因为前者在 timer 阶段执行,后者在 check 阶段执行

setTimeout、Promise 等 API 便是任务源,而进入任务队列的是它们指定的具体任务,来自不同任务源的任务会进入到不同的任务队列:

image

示例

console.log("Script Start")

setTimeout(() => {
  console.log("timeout1")
}, 10)

new Promise(resolve => {
  console.log("promise1")
  resolve()
  setTimeout(() => console.log("timeout2"), 10)
}).then(() => {
  console.log("then1")
})

console.log("Script End")
  1. 事件循环从 MacroTask 队列开始,此时 MacroTask 队列中,只有一个 Script(整体代码)任务;当遇到 Task Source(任务源)时,则会先分发任务到对应的任务队列中。因此,上面例子第一步执行如下图所示:

image

接着遇到了 console.log() 语句,直接输出 Script Start。输出之后,Script 任务继续往下执行,遇到了 setTimeout,它作为一个 MacroTask,会将其任务分发到对应队列中:

image

Script 任务继续执行,遇到了 Promise 实例,Promise 构造函数中第一个参数是在 new 的时候执行的,执行时其中的参数进入执行栈执行;而后续的 .then 则被分发到 MicroTask 的 Promise 队列中去。因此会输出 Promise1,接着执行 resolve 并将 then1 分发到对应队列

构造函数继续执行,遇到了 setTimeout,然后将其对应的任务分发到对应队列:

image

Sciprt 任务继续执行,最后输出 Sciprt End,至此全局任务执行完毕。当执行完一个 MacroTask 后,会检查是否存在 MicroTasks,若存在则执行 MicroTasks 直至清空 MicroTasks

因此,在 Script 执行完毕后,开始查找清空 MicroTask Queue。此时,MicroTasks 中只有 Promise 队列的一个任务 then1,因此直接执行,输出 then1。当所有的 MicroTasks 执行完毕后,表示第一轮循环结束

image

此后开始第二轮循环,第二轮循环依然从 MacroTask 开始。此时,有两个任务:

  1. timeout1
  2. timeout2

取出 timeout1 执行,输出 timeout1。此时 MicroTask Queue 已经没有可执行的任务了,直接开始第三轮循环:

image

第三轮循环依旧从 MacroTask Queue 开始,此时 MacroTask Queue 只有一个 timeout2,取出并直接输出。此时,MacroTask Queue 与 MicroTask Queue 都没有任务了,因此将不会在输出其它东西。综上,输出结果如下:

Script Start
promise1
Script End
then1
timeout1
timeout2

最初的题目

本文最上方题目:

new Promise(resolve => {
  resolve(1)
  Promise.resolve().then(() => console.log(2))
  console.log(4)
}).then(t => {
  console.log(t)
})

console.log(3)

// 输出 4 3 2 1

这段代码流程如下:

  1. Script 任务先运行,首先遇到 Promise 实例,构造函数首先执行,因此输出4。此时 MicroTask 任务有 t2 与 t1
  2. Script 任务继续执行,输出3,至此第一个 MacroTask 执行完成
  3. 执行所有的 MicroTask,先后取出 t2 与 t1,分别输出 2 与 1
  4. 代码执行完毕,输出为 4 3 2 1

为什么 t2 先执行呢?

  • 根据 Promise/A+ 规范:实践中要确保 onFullFilled 与 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行
  • ES6:Promise.resolve 方法允许调用时不带参数,直接返回一个 resolved 状态的 Promise 对象。立即 resolved 的 Promise 对象,是在本轮 Event Loop 结束时,而不是在下一轮 Event Loop 开始时
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant