【大量任务调用】来看事件循环

13 阅读2分钟

来看这么一个题目,有一个耗时任务,比如下面这样

<div id="time"></div>
<div>点击按钮后,会同时执行1000个耗时任务</div>
<div><span id="click-btn">开始执行任务</sapn></div>
function delay() {
  let now = Date.now()
  while(Date.now() - now < 5) {}
}
const clickBtn = document.getElementById('click-btn');
clickBtn.addEventListener('click', async() => {
  let now = Date.now()
  for (let i = 0; i < 1000; i++) {
    await runTask(delay)
  }
  let time = Date.now() - now

  const timeDiv = document.getElementById('time');
  timeDiv.textContent = `运算耗时${time}`
})

要求我们写一个runTask函数,满足以下两点

  • 要尽快完成任务,同时不能让页面产生卡顿
  • 尽量兼容更多浏览器

这个题目是什么意思呢,如果我们直接执行会发生什么

function runTask(task) {
    task()
}

当我们事件监听的任务开始执行时,其实是delay函数执行了1000次,导致渲染主线程一直在占用,渲染任务不能及时执行,从而导致页面卡死

那应该怎么办呢,可以使用微任务么?

function runTask(task) {
  return new Promise((resolve) => {
    // 微任务仍然会卡顿
    Promise.resolve().then(() => {
      task()
      resolve()
    })
  })
}

肯定是不行的,渲染主线程要把微任务队列清空才会去调用渲染任务的,所以同直接执行一样,仍然会产生阻塞

使用宏任务呢

function runTask(task) {
  return new Promise((resolve) => {
    setTimeout() => {
      task()
      resolve()
    }, 0)
  })
}

宏任务的话不会造成卡顿,但是画面不流畅

因为事件循环实际上是这样的

for(;;) {
  取出一个宏任务
  执行宏任务
  ...
  if(渲染机制到达) {
    渲染
  }
}

像我们这里1000个任务是都在任务队列中等着呢,关键就是这个渲染机制到达,不同浏览器处理是不一样的

像谷歌可能就会降低刷新频率来执行更多的任务,而safari则仍然遵从着16.6ms一次,所以在谷歌中看的就会有些卡顿

使用requestAnimationFrame呢

function runTask(task) {
  return new Promise((resolve) => {
    requestAnimationFrame() => {
      task()
      resolve()
    })
  })
}

requestAnimationFrame虽然不会造成卡顿,但是不满足尽快执行的规则

因为每一帧只执行了一个task,这就导致了执行损耗,白白浪费了时间

其实到这里我们应该已经想到了办法,既然这样我们是不是可以手动控制每一帧执行的任务呢

其实伪代码就是

if(当前帧是否有剩余时间) {
    task()
    callback()
} else {
    _runTask(task, callback)
}

最终代码

计算当前帧的剩余时间可以使用requestIdleCallback,由于兼容性不好,使用requestAnimationFrame改写一下

function runTask(task) {
  return new Promise((resolve) => {
      _runTask(task, resolve)
  }) 
}
function _runTask(task, callback) {
  // 但是这个方法有兼容性问题,safari就不认这个方法
  // requestIdleCallback((idle) => {
  //   if (idle.timeRemaining() > 0) {
  //     task()
  //     callback()
  //   } else {
  //     _runTask(task, callback)
  //   }
  // })

  // 所以我们使用requestAnimationFrame来实现
  // 得自己实现每一帧执行多少任务
  let now = Date.now()
  requestAnimationFrame(() => {
    if (Date.now() - now  < 1000 / 60) {
      task()
      callback()
    } else {
      _runTask(task, callback)
    }
  })
}