阅读 857

[译] 使用 Web Workers 优化事件监听器

使用 Web Workers 优化事件监听器

我最近一直在捣鼓 Web Worker API,结果,我真的后悔没有尽早去研究这个功能强大的工具。现代 Web 应用程序对浏览器主线程的要求越来越高,进而影响程序的性能和提供流畅用户体验的能力。而 Web Worker 正是应对这种挑战的一种方法。

点击后发生了什么

Web Workers 有很多优点,但当涉及到程序中多个 DOM 事件监听器(表单提交、窗口大小调整、点击按钮等)的时候,我真的被震撼到了。这些监听器都必须存在于浏览器的主线程上,当主线程因长时间运行的进程而阻塞时,监听器的响应能力会受到影响,在事件循环可以正常运行之前,整个应用程序都会被阻塞。

诚然,监听器之所以困扰我多时,是因为我一开就误解了 Workers 要解决的问题。最开始,我一直以为它主要是关于代码的执行速度。“如果我可以在不同的线程上并行执行更多操作,那么我的代码执行速度将大大提升!”但是!在通常情况下,一件事开始执行前需要另一件事发生为前提,例如当你希望一系列计算完成之后才能更新 DOM。所以我幼稚地想:“如果我仍然要等待事件完成后才进行别的操作,把一些任务移到单独的线程中执行的意义没有那么大”。

这是我想到的代码:

const calculateResultsButton = document.getElementById('calculateResultsButton');
const openMenuButton = document.getElementById('#openMenuButton');
const resultBox = document.getElementById('resultBox');

calculateResultsButton.addEventListener('click', (e) => {
    // "在它执行完前,我不能更新 DOM,
    // 所以我为什么要把它放到 Worker 里呢?"
    const result = performLongRunningCalculation();
    resultBox.innerText = result;
});

openMenuButton.addEventListener('click', (e) => {
    // Do stuff to open menu. 
});
复制代码

在这里,我在执行某种可能大计算量的操作后更新了 box 中的文本。并行执行这些操作没有什么意义(DOM 的更新取决于计算的结果),所以,理所当然,我希望所有操作都是同步的。但最开始我不了解的是,如果线程被阻塞,其它所有的监听器都无法被触发。这意味着,操作变得不可靠了。

举例说明不靠谱的场景

在下面的示例中,点击“Freeze”按钮将会在增加计数,但在这之前会执行3秒的同步暂停(来模拟长时间运行的计算),而点击“Increment”按钮将立即增加计数。在第一个按钮暂停期间,整个线程处于静止状态,在事件循环可以再次执行前,不会触发其它任何主线程的活动。

为了证明这一点,请单击第一个按钮,然后立即单击第二个按钮。

请在 CodePen 中查看 Alex MacArthur (@alexmacarthur)的 Event Blocking - No Worker

页面卡住是因为较长的同步暂停阻塞了线程。但造成的影响不止于此。再次执行此操作,但是这次,单击“Freeze”后立即尝试调整蓝色边框的大小。由于布局更改和重绘也在主线程中进行,因此在计时完成前,将再次被阻塞。

它们监听的远比你想象得多

任何普通的用户都不愿意经历这种体验,而我们不过是处理了几个事件监听器。不过,在现实世界中,我们要做的还很多。通过使用 Chrome 的getEventListeners方法,我使用以下脚本汇总了页面上每个 DOM 元素的事件监听器。将这段代码放到控制台中,它会返回监听器的总数。

Array
  .from([document, ...document.querySelectorAll('*')])
  .reduce((accumulator, node) => {
    let listeners = getEventListeners(node);
    for (let property in listeners) {
      accumulator = accumulator + listeners[property].length
    }
    return accumulator;
  }, 0);
复制代码

我在下列程序中任意的页面运行以上代码,得到的可用的监听器数如下。

Application Number of Listeners
Dropbox 602
Google Messages 581
Reddit 692
YouTube 6,054 (!!!)

注意这些特殊的数字。绑定到 DOM 中监听器的数量很多,而且即使应用程序中只有一个长时间运行的进程出错了,所有的监听器都将无响应。这很有可能就降低你程序的用户体验。

更靠谱一些的同样的示例(多亏了 Web Workers!)

考虑到以上种种情况,让我们升级上述示例。同样地方法,但是这次,我们把长时间运行的操作移至单独的线程中。再次执行相同的点击,你会发现点击“Freeze”仍然会延迟 3 秒钟更新点击次数,但是不会阻止页面上其他任何事件监听器。 相反,其它按钮仍可单击,并且 box 的大小仍然可以调整,这正是我们想要的。

请在 CodePen 中查看 Alex MacArthur (@alexmacarthur)的 Event Blocking - Worker

如果你深入研究该代码,你会注意到,虽然 Web Worker API 可能更符合人机工程学,但实际上并没有想象中那么可怕(恐惧更多是由于直接将众多代码示例放在一起)。为了变得不那么吓人,有一些好的工具可以简化其实现。以下是一些我觉得不错的内容:

  • workerize — 在 Web Worker 中运行模块
  • greenlet — 在 worker 中运行任意一端异步代码
  • comlink — 基于 Web Worker API 的抽象封装

开始线程编程吧(可以更有意义)

如果你的应用程序是典型的,则可能已经绑定了很多监听器。而且它可能还会执行很多不需要在主线程上进行的计算。因此,可以考虑使用 Web Workers 进行监听并提高用户体验。

需要明确的是,将所有非 UI 工作放到工作线程中可能不是一个好方法。可能只是给你的程序增加了很多重构和复杂性,而收效甚微。或许,可以考虑先确定有哪些明显的计算密集的进程,然后分别给这些进程分配一个小型 Web Worker。随着时间的推移,再逐步深入研究并考虑在更大地范围内使用 UI/Worker 架构。

无论如何,深入研究它吧。其强大的浏览器支持以及现代应用程序对性能的需求不断增长,我们没有理由不去研究这类工具。

愉快地开始线程编程吧!

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

关注下面的标签,发现更多相似文章
评论