3

我不该动你的,Event Loops

写在前面的话

本意是想好好研究下 Event Loops, 看了几篇博客后,才意识到作为前端打字员的我有多无知,这坑忒深了。

macrotask?,microtask?,MutationObserver? 这些都是啥?规范还没写清楚?不同浏览器运行的还未必一样?

但为了使自己养成经常总结的习惯,还是写点什么吧。

故事的开始

<body>
  <script>
    const fib = n => {
      if (n <= 1) {
        return 1
      } else {
        return fib(n - 1) + fib(n - 2)
      }
    }
    console.now = (text) => {
      console.log(`${text} ${new Date().toLocaleString()}`)
    }
    setTimeout(function () {
      console.now(0)
      document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
      console.now(1)
      fib(45)
      console.now(2)
    }, 1000)
  </script>
</body>

计算 fib(45) 是一个相当耗时的工作,在我的chrome里约需要15s左右。

问,页面什么时候会变成红色?在执行 console.now(1) 之前就变成红色了吗?

e01

可以看到即使在 console.now(1) 执行之后,页面仍旧没有变红。

关于这个现象,可以有两种解释:

  1. document.getElementsByTagName('body')[0].style.backgroundColor = 'red' 被当作一个异步事件,作为一个 task,被添加到 event loops
  2. 渲染引擎要等到 JS 引擎空闲时才开始工作

到底是哪一种?所以将上述代码修改下

<script>
    const fib = n => {
      if (n <= 1) {
        return 1
      } else {
        return fib(n - 1) + fib(n - 2)
      }
    }
    console.now = (text) => {
      console.log(`${text} ${new Date().toLocaleString()}`)
    }
    setTimeout(function () {
      console.now(0)
      document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
      console.now(1)
      fib(45)
      console.now(2)
    }, 1000)
    setTimeout(function () {
      console.now(3)
      fib(45)
      console.now(4)
    }, 1000)
  </script>

又增加了一个 setTimeout。这样的话,如果是第一种解释,应该在 console.now(3) 运行之前,页面就变成了红色;否则就应该采取第二种解释。

运行结果如下,

e02

可以看到在 console.now(3) 之后,页面依旧没有变色,看来就是渲染引擎要等到JS引擎完全空闲时才工作。

事情就这样结束了吗

没有,直到我看到文档

An event loop must continually run through the following steps for as long as it exists:

  1. Let oldestTask be the oldest task on one of the event loop's task queues, if any, ignoring, in the case of a browsing context event loop, tasks whose associated Documents are not fully active. The user agent may pick any task queue. If there is no task to select, then jump to the microtasks step below.
  2. Set the event loop's currently running task to oldestTask.
  3. Run oldestTask.
  4. Set the event loop's currently running task back to null.
  5. Remove oldestTask from its task queue.
  6. Microtasks: Perform a microtask checkpoint.
  7. Update the rendering: If this event loop is a browsing context event loop (as opposed to a worker event loop), then run the following substeps.

    …...

这段话第7点的意思,怎么理解起来像是每执行一次 Event Loops 的 task,最后都会更新视图。

后来看到从event loop规范探究javaScript异步及浏览器更新渲染时机

渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。

会不会两次 setTimeout 被合并了?

<script>
    const fib = n => {
      if (n <= 1) {
        return 1
      } else {
        return fib(n - 1) + fib(n - 2)
      }
    }
    console.now = (text) => {
      console.log(`${text} ${new Date().toLocaleString()}`)
    }
    setTimeout(function setTimeout0() {
      console.now(0)
      document.body.style.backgroundColor = 'red'
      console.now(1)
      fib(45)
      console.now(2)
    }, 1000)
    setTimeout(function setTimeout1() {
      console.now(3)
      fib(45)
      console.now(4)
    }, 1001)
  </script>

e04

这样调整之后,在运行 console.now(3) 之前,页面的颜色就变了

这样看来,就是在每一次task之后就可能会更新视图,而不是等到JS引擎空闲

在执行完setTimeout0后,Event Loops 中实际上仍有 setTimeout1 待执行,但是浏览器先渲染了视图,再执行了setTimeout ,这就推翻了之前渲染引擎要等到 JS 引擎空闲(Event Loops为空)时才开始工作。

同时我怀疑,之前代码

setTimeout(function () {
      console.now(0)
      document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
      console.now(1)
      fib(45)
      console.now(2)
    }, 1000)
    setTimeout(function () {
      console.now(3)
      fib(45)
      console.now(4)
    }, 1000)

会不会被优化成

setTimeout(function () {
      console.now(0)
      document.getElementsByTagName('body')[0].style.backgroundColor = 'red'
      console.now(1)
      fib(45)
      console.now(2)
      console.now(3)
      fib(45)
      console.now(4)
    }, 1000)

坑深,今天先到这,休息下了

参考资料


nbb3210
436 声望31 粉丝

优雅地使用JavaScript解决问题