时间分片

《浏览器UI线程更新机制》文章中介绍大多数浏览器在 JavaScript 运行时停止 UI 线程队列中的任务,也就是说 JavaScript 任务必须尽快结束,以免对用户体验造成不良影响。

尽管你尽了最大努力,还是有一些 JavaScript 任务因为复杂性原因不能在 100 毫秒或更少时间内完成。这种情况下,理想方法是让出对 UI 线程的控制,使 UI 更新可以进行。让出控制意味着停止 JavaScript 运行,给 UI 线程机会进行更新,然后再继续运行 JavaScript。于是 JavaScript 定时器进入了我们的视野。

定时器与 UI 线程交互的方式有助于分解长运行脚本成为较短的片断。调用 setTimeout() 或 setInterval() 告诉 JavaScript 引擎等待一定时间然后将 JavaScript 任务添加到 UI 队列中。例如:

function greeting() {
  alert("Hello world!");
}
setTimeout(greeting, 250);

此代码将在 250 毫秒之后,向 UI 队列插入一个 JavaScript 任务运行 greeting() 函数。在那个点之前,所有其他 UI 更新和 JavaScript 任务都在运行。请记住,第二个参数指什么时候应当将任务添加到 UI 队列之中,并不是说那时代码将被执行。这个任务必须等到队列中的其他任务都执行之后才能被执行。

一个常见的长运行脚本就是循环占用了太长的运行时间。那么定时器就是你的下一个优化步骤。其基本方法是将循环工作分解到定时器序列中。典型的循环模式如下:

for (var i = 0, len = items.length; i < len; i++) {
  process(items[i]);
}

这样的循环结构运行时间过长的原因有二,process() 的复杂度,items 的大小,或两者兼有。

此处理过程必须是同步处理吗?数据必须按顺序处理吗?如果这两个回答都是“否”,那么代码将适于使用定时器分解工作。一种基本异步代码模式如下:

function processArray(items, process, callback) {
  var todo = items.concat();
  function updateProgress () {
    if (todo.length > 0) {
      process(todo.shift());
      setTimeout(updateProgress, 25);
    } else {
      callback()
    }
  }
  updateProgress()
}
// 用例
processArray([1, 2, 3, 4], (item) => {
  console.log('当前项', item)
}, () => {
  console.log('执行完成')
})

在 Windows 系统上定时器分辨率为 15 毫秒,也就是说一个值为 15 的定时器延时将根据最后一次系统时间刷新而转换为 0 或者 15。设置定时器延时小于 15 将在 Internet Explorer 中导致浏览器锁定,所以最小值建议为 25 毫秒(实际时间是 15 或 30)以确保至少 15 毫秒延迟。

有时每次只执行一个任务效率不高。考虑这样一种情况:处理一个拥有 1’000 个项的数组,每处理一个项需要 1 毫秒。如果每个定时器中处理一个项,在两次处理之间间隔 25 毫秒,那么处理此数组的总时间是(25 + 1) × 1’000 = 26’000 毫秒,也就是 26 秒。如果每批处理 50 个,每批之间间隔 25 毫秒会怎么样呢?整个处理过程变成(1’000 / 50) × 25 + 1’000 = 1’500 毫秒,也就是 1.5 秒,而且用户也不会察觉界面阻塞,因为最长的脚本运行只持续了 50 毫秒。通常批量处理比每次处理一个更快。

如果你记住 JavaScript 可连续运行的最大时间是 100 毫秒,那么你可以优化先前的模式。我的建议是将这个数字削减一半,不要让任何 JavaScript 代码持续运行超过 50 毫秒,只是为了确保代码永远不会影响用户体验。

function processArray(items, process, callback) {
  var todo = items.concat();
  function updateProgress () {
    let start = Date.now()
    do {
      process(todo.shift());
    } while (todo.length > 0 && Date.now() - start < 50);
    if (todo.length > 0) {
      setTimeout(updateProgress, 25);
    } else {
      callback()
    }
  }
  updateProgress()
}

我们通常将一个任务分解成一系列子任务。如果一个函数运行时间太长,那么查看它是否可以分解成一系列能够短时间完成的较小的函数。可将一行代码简单地看作一个原子任务,多行代码组合在一起构成一个独立任务。某些函数可基于函数调用进行拆分。

其实上面代码存在一个问题,就是定时器的间隔时间问题,我设置的是25毫秒,也就是认为最快是25毫秒可以完成UI更新,但是如果UI更新比25毫秒还要快,那么就需要等待一段时间,这个等待时间是没有必要的,所以希望的是UI更新完成之后就立马执行。而实现的关键是两个新API。

requestIdleCallback 事件循环空闲期的回调函数

requestAnimationFrame 在UI更新完成之后的回调

所以根据实际情况去选择API,在我们任务分解的过程中我们可以把定时器改成 requestAnimationFrame,如果考虑兼容问题,那还是使用定时器吧。

function processArray(items, process, callback) {
  var todo = items.concat();
  function updateProgress () {
    if (todo.length > 0) {
      process(todo.shift());
      requestAnimationFrame(updateProgress)
    } else {
      callback()
    }
  }
  updateProgress()
}

解决同步阻塞的方法,通常有两种: 异步 与 任务分割。而 React Fiber 便是为了实现任务分割而诞生的。

Fiber 其实可以算是一种编程思想,在其它语言中也有许多应用(Ruby Fiber)。核心思想是任务拆分和协同,主动把执行权交给主线程,使主线程有时间空挡处理其他高优先级任务。当遇到进程阻塞的问题时,任务分割、异步调用和缓存策略是三个显著的解决思路。

发布了254 篇原创文章 · 获赞 200 · 访问量 21万+

猜你喜欢

转载自blog.csdn.net/wu_xianqiang/article/details/104857948
今日推荐