• 0

  • 470

手摸手给你们讲讲我所理解的Event Loop

云哥

关注云计算

3星期前

Event Loop

此文章是鄙人在b站上看了两个视频后的一些理解, 欢迎大家一起来讨论哦~

(视频资源都在最底下, 强烈推荐!!!)

任务分发器

一般的, 下面这些标签或者API都可以让一些任务间接的进入各自的队列。

任务:script、setTimeout、setInterval,DOM事件绑定, 请求响应等

微任务:promise

Event Loop 周期组成

  1. TASK 阶段 (重点就在这)

    • 任务调度和执行。 任务中代码执行完毕,调用栈清空后,开始执行微任务队列中的微任务。循环如此, 直至没有微任务,开始下一个任务。如果没有下一个任务, 则进入rAF。
  2. rAF(requestAnimationFrame)阶段 - 所有任务执行完毕之后, 页面渲染之前。

  3. Style 阶段 - 样式计算, 计算应用到元素上的样式。

  4. Layout 阶段 - 创建渲染树, 找出页面上的所有内容及位置。

  5. Paint 阶段 - 绘制内容到页面上。

总的来分的话, 可以将上面五个阶段化为两个:

  1. TASK 阶段, 主要就是任务的调度和执行Webapis 推任务到任务队列,事件循环在合适的时候 将任务取出压入调用栈执行和弹出。
  2. 渲染绘制阶段,从2到4, 主要就是 绘制前的逻辑执行, 以及视图相关的绘制了。

注:微任务的执行,取决于调用栈。调用栈为空才会执行微任务。

深入JavaScript中的EventLoop

个人理解的事件循环的一个流程

  1. 事件循环不断的判断 任务队列是否有值, 并且调用栈为空。(同时会在每隔16毫秒左右去进行一次视图绘制)
  2. Webapis 得到任务, 并在合适的时候将任务推入任务队列。
  3. 当有任务队列有值, 并且调用栈查询到的状态为空。则从任务队列中取出第一个任务, 压入调用栈,并开始执行该任务。(此时该任务在调用栈最底部)
    1. 任务代码执行时, 可能会分发新的任务, 比如设置定时器, 发起网络请求,监听事件这些操作。这些都会交与 Webapis 。 当定时器时间满足, 请求响应回来, 监听到事件这些时候,Webapis 会在下一轮事件循环, 将这些满足条件的任务推入任务队列,等待事件循环的调度。
    2. 任务代码执行时, 可能会分发新的微任务, 如 Promise.resolve().then(cb) 这种的, 此时,将cb函数交推入微任务队列。等待事件循环的调度。
    3. 当该任务中代码执行完毕后, 弹出任务,调用栈清空。
    4. 判断微任务队列是否有值, 如果有,则从中取出第一个微任务, 压入调用栈并执行,执行完毕 弹出。(反复此操作, 直至微任务队列为空)
    5. 当微任务队列为空, 且调用栈已清空时, 则开始下一个任务。
  4. 重复第三条,直至任务队列为空, 且调用栈为空。
  5. 进入 渲染绘制 阶段, 依次调用 requestAnimationFrame 队列中的任务(此任务中进行分发任务和微任务, 参考3-1 到 3-5)。然后进入 Style CalculationLayout CalculationPaint 阶段。完成后开始新的一轮事件循环。(对于新的一轮事件循环判定的标准是 走过了 渲染绘制 阶段
  6. 每轮 事件循环 开始时, 如果调用栈为空并且任务队列有值, 就会走上面的 第三到第五条的流程。
  7. 如果调用栈为空, 并且任务队列也为空, 则事件循环会进入空转状态(每隔16毫秒左右进行一次视图绘制, 即 每隔16ms 会走一次第五条)。

测试Demo

① 由 setTimeout 分发出来的任务

console.log("glob1");

setTimeout(function timeout1Cb() {
  console.log("timeout1");
  new Promise(function timeout1PromiseCb(resolve) {
    console.log("timeout1_promise");
    resolve();
  }).then(function timeout1ThenCb() {
    console.log("timeout1_then");
  });
});

new Promise(function promise1Cb(resolve) {
  console.log("glob1_promise");
  resolve();
}).then(function promise1ThenCb() {
  console.log("glob1_then");
});

setTimeout(function timeout2Cb() {
  console.log("timeout2");
  new Promise(function timeout2PromiseCb(resolve) {
    console.log("timeout2_promise");
    resolve();
  }).then(function timeout2ThenCb() {
    console.log("timeout2_then");
  });
});

new Promise(function promise2Cb(resolve) {
  console.log("glob2_promise");
  resolve();
}).then(function promise2ThenCb() {
  console.log("glob2_then");
});

/**
解析:
1. 代码开始执行, 第一个 log 是 "glob1" 无疑。
2. 执行 `setTimeout(timeout1Cb)`, 将 `timeout1Cb` 交与 ` `Webapis` `, 等待 ` `Webapis` ` 在下一轮事件循环推入 任务队列。
3. 实例化 promise, 传入 promise1Cb 执行器,执行 promise1Cb。输出 "glob1_promise"
4. 调用 `resolve();` 往微任务队列中推了一个任务 `promise1ThenCb`。
5. 执行 `setTimeout(timeout2Cb)`, 将 `timeout2Cb` 交与 ` `Webapis` `, 等待 ` `Webapis` ` 在下一轮事件循环推入 任务队列。
6. 实例化 promise, 传入 promise2Cb 执行器,执行 promise2Cb。输出 "glob2_promise"
7. 调用 `resolve();` 往微任务队列中推了一个任务 `promise2ThenCb`。
8. 到此, 代码执行完毕,调用栈清空。并询问微任务队列是否有值。(此时微任务队列中应该是: [promise1ThenCb, promise2ThenCb])。
9. 调用栈清空情况下, 取出微任务队列中的第一个任务 `promise1ThenCb`,压入调用栈中,并执行,输出 "glob1_then"。执行完毕, 调用栈弹出 `promise1ThenCb`。(此时调用栈为空, 微任务队列为: [promise2ThenCb])。
10. 取出微任务队列中的第一个任务 `promise2ThenCb`,压入调用栈中,并执行,输出 "glob2_then"。执行完毕, 调用栈弹出 `promise2ThenCb`。(此时调用栈为空, 微任务队列为: [])。
- 至此, 第一轮事件循环的 TASK阶段 结束, 进入渲染绘制阶段。
  - 第一轮事件循环,控制台的log应该为:     
        "glob1"、"glob1_promise"、"glob2_promise"、"glob1_then"、"glob2_then"
 
--- 绘制完成, 开始第二轮事件循环 ---
1.  `Webapis` 将上个循环中的 `timeout1Cb` 和 `timeout2Cb`依次放入任务队列。(为什么会在第二轮循环就放到任务队列了, 是因为 setTimeout 第二个参数没传, 默认为0,就放到下一个循环中去执行了, 如果第二个参数是一个大于16的数, 那这个任务就可能会到下下个循环中去执行)
2. 在调用栈为空的情况下, 取出第一个任务 `timeout1Cb`, 压入调用栈中, 并执行, 输出 "timeout1"
3. 实例化 promise, 传入 `timeout1PromiseCb` 执行器,执行 `timeout1PromiseCb`。输出 "timeout1_promise"
4. 调用 `resolve();` 往微任务队列中推了一个任务 `timeout1ThenCb`。
5. 到此, `timeout1Cb` 代码执行完毕,调用栈清空。并询问微任务队列是否有值。(此时微任务队列中应该是: [timeout1ThenCb])。
6. 调用栈清空情况下, 取出微任务队列中的第一个任务 `timeout1ThenCb`,压入调用栈中,并执行,输出 "timeout1_then"。执行完毕, 调用栈弹出 `timeout1ThenCb`。(此时调用栈为空, 微任务队列为: [])。
    - 至此, 第二轮事件循环的第一个任务执行完毕(目前任务队列: [timeout2Cb])

7. 在调用栈为空的情况下, 取出第一个任务 `timeout2Cb`, 压入调用栈中, 并执行, 输出 "timeout2"
8. 实例化 promise, 传入 `timeout2PromiseCb` 执行器,执行 `timeout2PromiseCb`。输出 "timeout2_promise"
9. 调用 `resolve();` 往微任务队列中推了一个任务 `timeout2ThenCb`。
10. 到此, `timeout2Cb` 代码执行完毕,调用栈清空。并询问微任务队列是否有值。(此时微任务队列中应该是: [timeout2ThenCb])。
11. 调用栈清空情况下, 取出微任务队列中的第一个任务 `timeout2ThenCb`,压入调用栈中,并执行,输出 "timeout2_then"。执行完毕, 调用栈弹出 `timeout2ThenCb`。(此时调用栈为空, 微任务队列为: [])。
    - 至此, 第二轮事件循环的第二个任务执行完毕(目前任务队列: [])

- 至此, 第二轮事件循环的 TASK阶段 结束, 开始进入 渲染部分, 依次走 rAF, Style, Layout, Paint。
  - 第二轮事件循环,控制台的log应该为: 
        "timeout1"、"timeout1_promise"、"timeout1_then"、"timeout2"、"timeout2_promise"、"timeout2_then"
  
--- 绘制完成, 开始第三轮事件循环 ---
1.  `Webapis`  上没有东西交给 任务队列, 所以此时 任务队列为空, 调用栈为空。于是事件循环进入空转状态。并每隔16ms左右 渲染一次页面。


**/

复制代码

② 由script 标签分发出来的任务

<script>
  console.log("script1 - begin");

  setTimeout(function cb1() {
    console.log("script1 - timeout1");
  });

  Promise.resolve().then(function microCb1(){
    console.log("script1 - then")
  });

  console.log("script1 - end");
</script>
<script>
  console.log("script2 - begin");

  setTimeout(function cb2() {
    console.log("script2 - timeout1");
  });

  Promise.resolve().then(function microCb2(){
    console.log("script2 - then")
  });

  console.log("script2 - end");
</script>


<!-- 

解析: 
 ----- 第一轮事件循环 ------ 
1. 初始化js环境,将两个script的任务由  `Webapis`  推入 任务队列。(此时任务队列中有两个任务, 分别为对应的的script)
2. 询问调用栈是否为空, 是, 则取任务队列最前面一个, 压入调用栈执行, 任务队列length减1。
3. 执行第一个 script 中的代码。
4. 执行 `console.log("script1 - begin");`, 输出 "script1 - begin"
5. 执行 setTimeout 逻辑, 将cb1交与 `Webapis` , 并让 `Webapis` 在满足条件的情况下将cb1推入任务队列。此时是 0 毫秒后,及下一轮事件循环, 也就是当前事件循环结束后。
6. 执行 Promise.resolve 语句, 并向微任务队列推入一个 `microCb1` 的任务
7. 执行 `console.log("script1 - end");`, 输出 "script1 - end"
8. 此时代码执行完毕, 询问 调用栈是否为空 的答案为 true, 则开始执行微任务。
9. 取出微任务队列中的第一个任务 microCb1, 压入调用栈, 开始执行。输出 "script1 - then", microCb1 执行完毕, 弹出调用栈。(如若还有, 继续重复此操作, 直到微任务队列为空)
10. 微任务此时length为0, 调用栈为空。
  - 此任务执行完毕, 依次输出 "script1 - begin"、"script1 - end" 、"script1 - then"

11. 开始执行下一个任务
12. 询问调用栈是否为空, 是, 则取队列最前面一个, 压入调用栈执行, 任务队列length减1, 则当前任务队列length为0。
13. ... 重复 3 - 10 ... 
 - 此任务执行完毕, 依次输出 "script2 - begin"、"script2 - end" 、"script2 - then"

14. 此时,调用栈为空, 且 任务队列为空。于是便走 渲染 阶段, 依次经过 requestAnimationFrame、 Style Calculation、Layout Calculation、 Pint 阶段(看下面 Event Loop 周期组成)
15. 直到 页面绘制完成, 这第一轮 事件循环 算是结束。于是便开始第二轮事件循环

 ----- 第二轮事件循环 ------ 

16. 我们在 script1 和 script2 中有向 `Webapis` 中分发了一个 cb1和 cb2 , 在上一轮事件循环中它们一直处于  `Webapis` 中, 未到达任务队列。而此时,  `Webapis` 则会把这两个任务交给 任务队列。
17. 询问调用栈是否为空, 得到回复是 true, 则将任务队列中的 cb1 取出, 压入调用栈。(队列的顺序和分发时候的先后顺序保持一致)
18. 执行 cb1, 输出 "script1 - timeout1"。此时任务执行完毕, 调用栈清空。开始执行微任务, 发现没有微任务。OK, 执行下一个任务。
19. ... 重复 17 - 18 两个步骤, 把调用的 cb1 换成了cb2 ...
 - 上面两个任务执行完毕, 依次输出 "script1 - timeout1" 、"script2 - timeout1"

20. 此时,调用栈为空,且任务全部执行完毕(微任务没执行完毕是不会走下一个任务, 所以此处任务执行完毕的前提条件就是微任务全部执行完毕)
21. ... 重复 14 - 15 两个步骤, 至此, 所有任务执行完毕, 事件循环进行空转(虽说是空转, 其实也是每次都会去判断任务队列是否有值, 当有值并且调用栈为空的时候, 就去进行对应的操作, 取任务, 压栈, 执行,出栈...),并每隔 16.6ms 进行一次视图绘制。...


-->

复制代码

③ 由事件分发的任务 - 1

... 
<button id="btn">
  按钮
</button>


<script>
const btn = document.querySelector('#btn');

btn.addEventListener('click', function click1Cb(){
  Promise.resolve().then(function click1ThenCb() {
    console.log('promise1_then')
  })
  console.log('listener1');
})

btn.addEventListener('click', function click2Cb(){
  Promise.resolve().then(function click2ThenCb() {
    console.log('promise2_then')
  })
  console.log('listener2');
})

</script>

<!--
解析:
1. 获取btn元素。
2. 给btn绑定click事件, 即 把 `click1Cb` 函数交给  `Webapis` , 当用户点击了 btn 后,  `Webapis` 会将这个 `click1Cb` 推入任务队列。
3. 给btn绑定click事件, 即 把 `click2Cb` 函数交给  `Webapis` , 当用户点击了 btn 后,  `Webapis` 会将这个 `click2Cb` 推入任务队列。
4. 第一轮事件循环完毕, 进行空转。


--- 一段时间后 ---
用户点击这个按钮。
1.  `Webapis`  得知按钮点击, 便将 `click1Cb` 、`Click2Cb` 依次放入 任务队列中。
2. 事件循环判断当前调用栈是否为空,以及 任务队列 中是否有值。得到true 的结果后,取出任务队列中的第一个任务 `click1Cb`,压入调用栈中,并执行。
3. 执行 `click1Cb`, 首先 分发了一个微任务 `click1ThenCb` 到微任务队列中。接着走下一行的console.log, 输出 "listener1"
4. `click1Cb` 执行完毕, 调用栈弹出 `click1Cb`, 开始执行微任务, 此时微任务队列为: [click1ThenCb]
5. 取出微任务队列中的第一个微任务 `click1ThenCb`, 压入调用栈中, 并执行。输出 "promise1_then"。`click1ThenCb` 执行完毕, 调用栈弹出 `click1ThenCb`
- 至此,调用栈为空, 微任务队列为空。 第一个任务队列中的第一个任务执行完毕。

1. 取出 任务队列 的第一个任务 `click2Cb`,压入调用栈中,并执行。
2. 执行 `click2Cb`, 首先 分发了一个微任务 `click2ThenCb` 到微任务队列中。接着走下一行的console.log, 输出 "listener2"
3. `click2Cb` 执行完毕, 调用栈弹出 `click2Cb`, 开始执行微任务, 此时微任务队列为: [click2ThenCb]
4. 取出微任务队列中的第一个微任务 `click2ThenCb`, 压入调用栈中, 并执行。输出 "promise2_then"。`click2ThenCb` 执行完毕, 调用栈弹出 `click2ThenCb`
5. 至此,调用栈为空, 微任务队列为空。 第一个任务队列中的第一个任务执行完毕。

综上分析, 此demo的输出就是 "listener1"、 "promise1_then"、"listener2"、"promise2_then"
-->



复制代码

④ 由事件分发的任务 - 2

... 
<button id="btn">
  按钮
</button>


<script>
const btn = document.querySelector('#btn');

btn.addEventListener('click', function click1Cb(){
  Promise.resolve().then(function click1ThenCb() {
    console.log('promise1_then')
  })
  console.log('listener1');
})

btn.addEventListener('click', function click2Cb(){
  Promise.resolve().then(function click2ThenCb() {
    console.log('promise2_then')
  })
  console.log('listener2');
})
  
btn.click();

</script>

<!--
解析:
1. 获取btn元素。
2. 给btn绑定click事件, 即 把 `click1Cb` 函数交给  `Webapis` , 当用户点击了 btn 后,  `Webapis` 会将这个 `click1Cb` 推入任务队列。
3. 给btn绑定click事件, 即 把 `click2Cb` 函数交给  `Webapis` , 当用户点击了 btn 后,  `Webapis` 会将这个 `click2Cb` 推入任务队列。
4. btn 自己触发 click 事件, 调用栈压入 btn.click
5. 内部创建事件对象,缺少的数据由默认值填充。(如 鼠标位置信息等)
6. 依次触发事件(注意, 此次是内部js触发, 相当于自己注册事件,自己触发, 不走 `Webapis` , 更不走任务队列)
7. 调用栈压入 `click1Cb`,执行 `click1Cb`, 此时调用栈:[ btn.click, click1Cb ]
8. 执行 `click1Cb`, 首先 分发了一个微任务 `click1ThenCb` 到微任务队列中。接着走下一行的console.log, 输出 "listener1"。(此时 微任务队列内容为: [click1ThenCb])
9. `click1Cb` 执行完毕, 调用栈弹出 `click1Cb`,此时调用栈内容为:[btn.click], 因为调用栈非空, 不能执行微任务, 所以, 进行下一个事件监听函数的触发。 
10. 调用栈压入 `click2Cb`,执行 `click2Cb`。此时调用栈:[ btn.click, click2Cb ]
11. 执行 `click2Cb`, 首先 分发了一个微任务 `click2ThenCb` 到微任务队列中。接着走下一行的console.log, 输出 "listener2"。(此时 微任务队列内容为: [click1ThenCb, click2ThenCb])
12. `click2Cb` 执行完毕, 调用栈弹出 `click2Cb`,调用栈内容为:[btn.click], 此时没有未触发的事件监听函数了, 所以 btn.click 执行完毕, 调用栈弹出 btn.click, 此时调用栈内容为:[]。 
13. 询问调用栈是否为空,并且是否有未执行的微任务。 得到结果为true。便开始执行微任务。
14. 取出微任务队列中的第一个微任务 `click1ThenCb`, 压入调用栈中, 并执行。输出 "promise1_then"。`click1ThenCb` 执行完毕, 调用栈弹出 `click1ThenCb`。
15. 进行下一个微任务执行, 取出微任务队列中的第一个微任务 `click2ThenCb`, 压入调用栈中, 并执行。输出 "promise2_then"。`click2ThenCb` 执行完毕, 调用栈弹出 `click2ThenCb`。此时, 微任务队列执行完毕。
16. 此时,任务队列为空, 且调用栈为空,便进入 渲染阶段, 渲染结束, 第一轮事件循环完毕。开始后面的事件循环。


综上分析, 此demo的输出就是 "listener1"、 "listener2"、"promise1_then"、"promise2_then"
-->



复制代码

两个 事件相关的demo总结:

其实第一个和第二个, 代码层面上看, 不同的就是第一个是需要用户与UI交互进行触发, 第二个是通过js触发。

但是从代码执行的时候看, 就会有这么一些区别:

  1. 通过 UI 交互进行触发, 就会走 Webapis , 因为dom 的事件监听都是通过 Webapis 来的。而通过 Webapis , 在事件触发的时候, Webapis 则会把事件处理函数推入 任务队列, 等待事件循环的调度与执行。
  2. 通过js触发, 这里就相当于一个简单的事件系统,一边去 监听, 一边去触发, 同步的操作,没涉及到异步, 更没涉及到 Webapis , 任务队列。所以, 在触发事件处理函数的时候,调用栈一直都会有一个 btn.click 函数,等到事件处理函数全部执行完毕, 有了返回值。而 btn.click 才算执行结束。并从 调用栈弹出。清空了调用栈, 才能去执行 事件处理函数中 分发的微任务。

到此, 事件循环我个人的理解就是这样。这也是我花了几天时间集中精力去理解得来的一个总结。希望对大家能有一些帮助。

另因为这个只是我个人理解, 所以各位看官看的时候当个参考即可。如果发现我的理解和你们的理解有些出入的, 也欢迎在评论里提出来,一起完善各自对 Event Loop 的理解。

参考资料:

  1. 深入:微任务与Javascript运行时环境
  2. 前端基础进阶(十四):深入核心,详解事件循环机制
  3. 【熟肉 | 内核】深入JavaScript中的EventLoop
  4. 【自制熟肉】Philip Roberts:到底什么是Event Loop呢?(JSConf EU 2014)
免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

云计算

470

相关文章推荐

未登录头像

暂无评论