daily-plan icon indicating copy to clipboard operation
daily-plan copied to clipboard

从 HTML 官方规范解读 event loop

Open sl1673495 opened this issue 4 years ago • 0 comments

https://html.spec.whatwg.org/multipage/webappapis.html#task-queue

事件循环

  1. 从任务队列中取出一个任务并执行。

  2. 检查微任务队列,清空微任务,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。

  3. 进入更新渲染阶段,判断是否需要渲染,这里有一个 rendering opportunity 的概念,也就是说不一定每一轮 event loop 都会对应一次浏览器渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。(所以多个 task 很可能在一次渲染之间执行)

    • 浏览器会尽可能的保持帧率稳定,例如页面性能无法维持60fps(每16.66ms渲染一次)的话,那么浏览器就会选择30fps的更新速率,而不是偶尔丢帧。
    • 如果浏览器上下文不可见,那么页面会降低到4fps左右甚至更低。
    • 如果满足以下条件,也会跳过渲染:
      1. 浏览器判断更新渲染不会带来视觉上的改变。
      2. map of animation frame callbacks 为空,也就是帧动画回调为空,可以通过 requestAnimationFrame 来请求。
  4. 对于需要渲染的文档,如果窗口的大小发生了变化,执行监听的 resize 方法。

  5. 对于需要渲染的文档,如果页面发生了滚动,执行 scroll 方法。

  6. 对于需要渲染的文档,执行帧动画回调,也就是 requestAnimationFrame 的回调。

  7. 对于需要渲染的文档, 执行IntersectionObserver的回调,也许你在图片懒加载的逻辑里用过这个api。 待续 https://w3c.github.io/requestidlecallback/#start-an-idle-period-algorithm。

requestIdleCallback

以下内容中 requestAnimationFrame简称为rAFrequestIdleCallback简称rIC

MDN文档中的幕后任务协作调度 API 介绍的比较清楚,来根据里面的概念做个小实验:

屏幕中间有个红色的方块,把MDN文档中requestAnimationFrame的范例部分的动画代码直接复制过来。

这个动画的例子很简单,就是利用rAF在每帧渲染前的回调中把方块的位置向右移动10px。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #SomeElementYouWantToAnimate {
        height: 200px;
        width: 200px;
        background: red;
      }
    </style>
  </head>
  <body>
    <div id="SomeElementYouWantToAnimate"></div>
    <script>
      var start = null
      var element = document.getElementById("SomeElementYouWantToAnimate")
      element.style.position = "absolute"

      function step(timestamp) {
        if (!start) start = timestamp
        var progress = timestamp - start
        element.style.left = Math.min(progress / 10, 200) + "px"
        if (progress < 2000) {
          window.requestAnimationFrame(step)
        }
      }
      // 动画
      window.requestAnimationFrame(step)

      // 空闲调度
      window.requestIdleCallback(() => {
        alert('rIC')
      })

      // TODO +timeout
    </script>
  </body>
</html>

注意在最后我加了一个 requestIdleCallback 的函数,回调里会 alert('rIC'),来看一下演示效果:

alert 在最开始的时候就执行了,为什么会这样呢一下,想一下「空闲」的概念,我们每一帧仅仅是把 left 的值移动了一下,做了这一个简单的渲染,没有占满空闲时间,所以可能在最开始的时候,浏览器就找到机会去调用 rIC 的回调函数了。

我们简单的修改一下 step 函数,在里面加一个很重的任务,1000次循环打印。

function step(timestamp) {
  if (!start) start = timestamp
  var progress = timestamp - start
  element.style.left = Math.min(progress / 10, 200) + "px"
  let i = 1000
  while (i > 0) {
    console.log("i", i)
    i--
  }
  if (progress < 2000) {
    window.requestAnimationFrame(step)
  }
}

再来看一下它的表现:

其实和我们预期的一样,由于浏览器的每一帧都"太忙了",导致它真的就无视我们的 rIC 函数了。

如果给 rIC 函数加一个 timeout 呢:

// 空闲调度
window.requestIdleCallback(
  () => {
    alert("rID")
  },
  { timeout: 500 },
)

浏览器会在大概 500ms 的时候,不管有多忙,都去强制执行 rIC 函数,这个机制可以防止我们的空闲任务被“饿死”。

sl1673495 avatar May 20 '20 10:05 sl1673495