Jake Archibald: 在循环 - JSConf.Asia

115 阅读2分钟

问题

下面的代码在执行后会发生一闪之后消失的情况吗?

const div = document.createElement('div')
document.body.appendChild(div)
div.innerHTML = "test"
div.style.display = 'none'

setTimeout

The setTimeout(callback, ms) method, when invoked, must run the following steps:

  1. Run the following steps in parallel:

    1. Wait ms milliseconds

    2. Queue a task to run the following steps:

      1. Invoke callback.

Task Queues

如果没事情干,事件循环就以 CPU 经济的方式一直空转。

如果有事情做,那么就将要做的事情放到任务队列里面等待调度。

下图右边的这个开关就是每秒打开 60 次。

image.png

while 死循环时任务就没有办法执行完成,所以渲染就没法执行了。

setTimeout 死循环的时候,就是一直将任务加到任务队列,所以渲染是可以执行的。

微任务的死循环也会阻止渲染。

image.png

所以上面的代码是不会有问题的,因为它一定是在所有的代码执行完成之后才开始的渲染。

requestAnimationFrame

会在渲染之前执行 raf 回调。Safari 和 Edge 是在 Paint 之后调用的。

image.png

使用 setTimeout image.png

setTimeout(callback, 1000 / 60)

image.png

callback 有可能执行时间过长导致两次渲染间隔不一致。

image.png

使用 requestAnimationFrame,在回调之前执行

image.png

0-1000-500 的动画

实测这个代码并不能起作用

document.getElementById('button').addEventListener('click', (e) => {
    const box = document.getElementById('box')
    box.style.transform = 'translateX(500px)'
    box.style.transition = 'transform 1s ease-in-out'
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        box.style.transform = 'translateX(200px)'
      })
    })
  })

Micro Tasks

背景

90年代,想监听 DOM 的变动,给我们提供了 DOMNodeInserted 的事件。

下面这段代码会触发200次监听。这个时候如果监听里面任务哪怕执行的很短,也会有可能卡顿。能不能一次性添加完200个,然后再通知我们?

所以新建了一个微任务队列,可以在100次按钮添加完成之后再通知我们。

  document.body.addEventListener('DOMNodeInserted', () => {
    console.log('Stuff added to <body>!')
  })
  for (let i = 0; i < 100; i++) {
    const span = document.createElement('span')
    document.body.appendChild(span)
    span.textContent = `Hello ${i}`
  }

解决方案

使用 MutationObserver,这个应该就是 MutationObserver 诞生的背景。

const observer = new MutationObserver(() => {
  console.log('Stuff added to <body>!')
})
observer.observe(document.body, {
  childList: true
})

Promise

Promise 在回调执行时,浏览器会保证没有其他 JS 只执行到一半。这个就是为什么要采用微任务实现的原因。

任务执行

image.png 任务队列一次只处理一个。如果有另一个事件进来,就放到队尾。

动画回调会一次执行完,如果过程中又提交了新的,他们会延迟到下一帧。

微任务会执行到完成,包括插进来的,如果你的提交和处理速度一样快,会一直在处理微任务。

小测验

  const button = document.getElementById('button')
  button.addEventListener('click', () => {
    Promise.resolve().then(() => console.log('Microtask 1'))
    console.log('Task 1')
  })
  button.addEventListener('click', () => {
    Promise.resolve().then(() => console.log('Microtask 2'))
    console.log('Task 2')
  })
  button.click()

打印结果是什么?

第一次: Task 1 -> Task 2 -> Microtask 1 -> Microtask 2

后续点击: Task 1 -> Microtask 1 -> Task 2 -> Microtask 2