itboos / accumulation

各种知识的积累,以及前端博客
11 stars 0 forks source link

再谈 js 事件循环(Event-loop) #34

Open itboos opened 4 years ago

itboos commented 4 years ago

再谈事件循环

事件循环这个东西, 在 JavaScript 算是一个基础知识但也算是难点了。网络上关于事件循环的文章也是层出不穷,一是事件循环确实在分析代码执行顺序的时候会用到,二则是越来越多的公司在面试的时候会出此类型的题目来考查候选人JavaScript 基础内功了。

所以,不管是出于面试的角度还是提高自己js内功来看,掌握事件循环都是收益挺高的一件事情。事件循环,我在刚毕业的时候研究过一段时间,当时没写成文章。所以,一段时间之后,就又忘记了。然后又去看,然后又忘记,然后陷入了一个死循环。更重要的是,每次看不同的文章,都挺费时间的。于是,想着把自己的理解写成文章,一则可能帮助一些同学理解事件循环,二则加深自己的印象,当忘记的时候回顾可以快速想起。

好了,废话说了这么多,下面进入正题。

首先,澄清一个概念,本文讲的事件循环是指 窗口事件循环(window event loop), 因为 事件循环在挺多语言中都有,不同的语言,执行顺序也不太一样。

首先,我会大致介绍一下事件循环的规范, 然后结合具体的列子来分析。

  1. 每一个事件循环 有一个当前运行的任务,可以是一个 task 或者是 null. 起初, 它是 null. 这是用来处理可重入性。
  2. 每一个事件循环 有一个微任务队列,这个一个队列的微任务, 初始为空。 一个 microtask 是指一个的口语化方式 的任务,其通过 队列 microtask算法 创建。
  3. 每一个事件循环都有一个执行微任务检查点的布尔值,该布尔值最初为 false.它 是用来防止执行微任务检查点算法的重入调用。
  4. 一个事件循环有一个或多个宏任务队列。一个任务队列是一组的任务。 note: (宏任务队列) 是 set 而不是 queues, 因为事件循环处理模型的第一步是从选定的队列里 抓取第一个可以执行的任务, 而不是使第一个任务出队。即 宏任务队列不是标准的 queue, 而是 一系列 task 的集合。
  5. 微任务队列是标准的 queue, 执行任务时,就是先取队头的任务进行执行,新的任务放在队尾。
  1. 执行 script 里的代码,(这里可以看做是一个宏任务) 遇到宏任务就添加到宏任务队列里,遇到微任务就放到微任务队列里。
  2. 执行微任务检查点
    1. 如果事件循环的执行微任务检查点为true,则跳到 3
    2. 如果将事件循环的执行微任务检查点设置为true
    3. 当事件循环的微任务队列不为空时, 取出队列里队头的任务
    4. 执行此微任务,(遇到宏任务就添加到宏任务队列里,遇到微任务就放到微任务队列里。)PS: 这里推测,只有一个微任务队列。虽然规范里没有明说,但根据后面的 demo 演示推测出来的。 5 重复 2.3, 直到微任务队列为空。
  3. 选取一个宏任务队列,选一个最老的可以执行的宏任务
  4. 执行宏任务:(遇到宏任务就添加到宏任务队列里,遇到微任务就放到微任务队列里。)
  5. 后续一些其它操作
  6. 跳到 2

事件循环的处理流程的流程图

可以结合流程图看上面的文字描述,当然,有时间的话,还是建议去看下官方文档,链接贴在本文下方。

console.log('begin)
setTimeout(function set1(){
  console.log('s1');
},100);

setTimeout(function set2(){
  console.log('s2');
  Promise.resolve(0).then(function p4() {
    console.log('p4')
  })
},0);

new Promise(function p1(resolve){
  console.log('p1');
  resolve();
  console.log('p1-1');
}).then(function p2(){
  console.log('p2');
});

console.log(6);
Promise.resolve().then(function p3() {
  console.log('p7')
})
console.log('end')
// 输出:p1, p1-1, 6, p2, p7, s1, s2

分析:脚本开始执行: 我们假定 当前有一个 宏任务 Set, macroSet: {} 假定此是的微任务队列为 microTaskQueue: []

  1. 输出 begin
  2. 遇到 宏任务(setTimeout), 将 set1 回调放到 宏任务队列里, 此时, macroSet = { set1 }
  3. 遇到 宏任务(setTimeout), 将 set2 回调放到 宏任务队列里, 此时, macroSet = { set1, set2 }
  4. 执行 new Promise, 由于 Promise 构造函数执行会立即调用 executor 函数,所以,会输出 p1, p1-1。同时, executor 执行了 resolve, 所以, 此 promise settled 了,将 resolveCb 放入到 微任务队列里, 此时 microTaskQueue = [p2]
  5. 输出 6
  6. 执行 Promise.resolve, 根据 Promise.resolve(v) 的定义,是创建一个 fulfilledpromise 对象,其值为 v。 然后将回调 函数 p3 放到微任务队列里, 此时 microTaskQueue = [p2, p3]

此时任务队列和执行栈大致如下: PS(忽略执行栈里的 setTimeout1, 偷了个懒,拿了一个之前的老图)

  1. 输出 end
  2. 此时,(第一次是 这个 script) 这个宏任务 执行完了,这个时候会执行我们上面说的执行微任务检查点了。
  3. 此时,事件循环的执行微任务检查点 不为 true, 将检查点设置为 true, 并且此时微任务队列不为空。
  4. 取出队头任务 p2, 执行任务 p2, 输出 p2 此时 microTaskQueue = [p3]
  5. 继续取出队头任务 p3, 执行任务 p3, 输出 p7 此时 microTaskQueue = []
  6. 此时,微任务队列为空,执行事件循环处理模型的第 8-13 步。

----------------- 第二轮循环 分割线 -----------------

  1. 执行事件循环处理模型的的第一步 (此时,选取 了我们的宏任务队列 macroSet )
  2. 选取一个最老可以执行的宏任务, 这里是 set2, 因为 set1 还不能执行。
  3. 执行宏任务 set2 , 输出 s2, 遇到微任务 p4, 把 p4 添加到位任务队列, 此时 microTaskQueue = [p4]
  4. 这个宏任务 执行完了,这个时候会执行我们上面说的执行微任务检查点
  5. 此时,事件循环的执行微任务检查点 不为 true, 将检查点设置为 true, 并且此时微任务队列不为空
  6. 取出队头任务 p4, 执行任务 p4, 输出 p4
  7. 微任务队列为空,执行事件循环处理模型的第 8-13 步。

----------------- 第三轮循环 分割线 -----------------

  1. 执行事件循环处理模型的的第一步 (此时,选取 了我们的宏任务队列 macroSet )
  2. 选取一个最老可以执行的宏任务, 这时是 set1
  3. 执行宏任务 set1 , 输出 s1
  4. ..... 重复事件循环的其它步骤, 到这里我们写的代码算是执行完了。

总结输出

begin
p1
p1-1
6
end
p2
p7
s2
p4
s1

总结: 从这里,我们可以猜测,一个事件循环里只有一个微任务队列(因为规范里没有说明只有一个,目前根据输出的结果猜测)

奇奇怪怪的 demo2

new Promise((resolve, reject) => {
  resolve();
})
  .then(() => {
    console.log('outer tick0');
    new Promise((resolve, reject) => {
      resolve();
    })
      .then(() => {
        console.log('inner tick0');
        // Promise.resolve()
        return Promise.resolve();
      })
      .then(() => {
        console.log('inner tick1');
      })
  })
  .then(() => {
    console.log('outer tick1');
  })
  .then(() => {
    console.log('outer tick2');
  })
  .then(() => {
    console.log('outer tick3');
  })
  .then(() => {
    console.log('outer tick4');
  })
// 猜猜上面的结果是啥。

更复杂一些的情况是有 async await , 和 Promise.resolve 的情况。由于本文是关于事件循环的, 所以先不展开讲了。具体的打算再写一篇关于 Promise 的原理,执行顺序,各种调用的文章。

whatwg-event-loop规范

promises-spec 规范

itboos commented 1 year ago

事件循环: 2023.04 更新 事件循环的存在是为了 协调事件、用户交互、脚本、渲染、网络, 由用户代理实现事件循环,每个代理都有一个关联的事件循环,为改代理所独有的。

只有事件循环存在, 就会不断的执行以下步骤:

1、 让 oldestTask 和 taskStartTime 为空。 2、如果事件循环中有一个包含至少一个 可运行任务的任务队列:

可运行任务的定义: 如果任务的文档(document)为 null 或fully active ,则该任务是可运行的

那么:

2.1、让 taskQueue 成为这样一个任务队列, 一般会选择宏任务队列,而不会选择微任务队列

2.2、将 oldestTask 设置为 taskQueue 中的第一个可运行 任务,并将其从 taskQueue 中移除。

2.3、将事件循环的当前运行任务设置为 oldestTask。

2.4、执行 oldestTask

2.5、将事件循环的当前运行任务设置为 null

2.6、执行微任务检查点。

3、更新渲染

4、如果是 Worker 事件循环,则做以下工作: ... 省略

执行微任务检查点的步骤如下:

1、如果事件循环执行微任务检查点为真,则返回。

2、将事件循环的执行微任务检查点设置为 true。

3、当事件循环的微任务队列不为空时:(循环执行此步骤)

3.1、让 oldestMicrotask 为从事件循环的微任务队列中出队的结果。

3.2、 将事件循环的当前运行任务设置为 oldestMicrotask

3.3、运行最旧的微任务

3.4、 将事件循环的当前运行任务设置回 null

4、 Cleanup Indexed Database transactions.(清理 indexed DB 数据库事物)

5、将事件循环的执行微任务检查点设置为 false

其它:

每个事件循环都有一个当前正在运行的任务,它要么是一个任务,要么是 null。最初,这是空的。它用于处理重入。

每个事件循环都有一个微任务队列,这是一个微任务队列 ,最初是空的。微任务是指通过队列微任务算法创建的 任务的通俗方式。

每个事件循环都有一个执行微任务检查点布尔值,最初为 false。它用于防止执行微任务检查点算法的重入调用。

每个窗口事件循环都有一个DOMHighResTimeStamp 最后的渲染机会时间,最初设置为零。

每个窗口事件循环都有一个DOMHighResTimeStamp 最后的空闲周期开始时间,最初设置为零。

要获取窗口事件循环循环的相同循环窗口,请返回其相关代理的 事件循环为循环的所有对象。 Window

源文件:

https://html.spec.whatwg.org/multipage/webappapis.html#event-loops