使用 Web Workers:更聪明地工作,而不是更努力

本文由 Malte Ubl 撰写,他在 Bespin 项目中对 使用 Web Workers 做出了很多贡献。

近年来,Web 应用程序的用户体验越来越丰富。像 GMailMeeboBespin 这样的浏览器内应用程序让我们看到了 Web 未来的样子和感觉。创建优秀用户体验的关键方面之一是构建高度响应的应用程序。用户讨厌等待,也讨厌应用程序在运行一段时间后停止响应用户输入的情况。

现代客户端 Web 应用程序的核心是 JavaScript 编程语言。JavaScript 及其所使用的 DOM 本质上是单线程的。这意味着在 JavaScript 中,一次只能发生一件事。即使您的计算机有 32 个核心,它在进行长时间计算时也只会使用其中一个核心。例如,如果您计算前往月球的最佳轨迹,它将无法同时渲染显示该轨迹的动画,也无法在进行计算时响应任何用户事件,例如点击或键盘输入。

并发

为了在执行密集计算时保持响应能力,并发 是大多数现代编程语言的一部分。过去,并发通常通过使用线程来实现。然而,线程使得程序员越来越难理解程序流程,这往往会导致难以理解的错误,以及不同线程同时操作相同数据时产生的混乱行为。

Web Workers 由 WHATWG 推荐,并于 Firefox 3.5 中引入,旨在为 JavaScript 应用程序添加并发性,而不会引入与多线程程序相关的那些问题。启动工作者很容易 - 只需使用新的 Worker 接口即可。

在此示例中,将加载 worker.js 文件,并创建一个新线程来执行该代码。

// Start worker from file "worker.js"
var worker = new Worker("worker.js");

主 UI 线程和工作者之间的通信通过使用 postMessage 方法传递消息来完成。postMessage 是在 Firefox 3 中添加用于跨窗口通信的。要从工作者发送消息回页面,只需发布消息即可

// Send a message back to the main UI thread
postMessage("Hello Page!");

要捕获来自工作者的消息,您需要在 worker 对象上定义一个“onmessage”回调。这里我们只是将传递给回调函数的事件数据进行提醒。在本例中,“event.data”包含上面发送的“Hello Page!”字符串。

worker.onmessage = function (event) {
  alert(event.data);
  // Send a message to the worker
  worker.postMessage("Hello Worker");
}

要向工作者发送消息,我们调用 worker 对象上的 postMessage 方法。要在工作者内部接收这些消息,只需定义一个 onmessage 函数,该函数将在每次向工作者发布消息时被调用。

错误处理

您可以从工作者中发生的运行时错误中恢复,有两个级别。首先,您可以在工作者中定义一个 onerror 函数。其次,您可以通过在 worker 对象上附加 onerror 处理程序来从工作者外部处理错误

worker.onerror = function (event) {
  alert(event.message);
  event.preventDefault();
}

event.preventDefault() 方法阻止默认操作,默认操作会将错误显示给用户,或者至少将其显示在错误控制台中。这里我们改为提醒错误消息。

无共享

工作者与他们关联的页面或任何其他工作者绝对不共享任何状态;他们唯一可以交互的方式是通过 postMessage。工作者也无法访问 DOM,因此它们不能直接操作网页。因此,当多个工作者想要同时操作相同数据时,没有数据完整性问题的风险。

使用工作者的标准设置将包含一个页面 JavaScript 组件,该组件正在监听用户事件。当发生触发密集计算的事件时,会向工作者发送消息,工作者开始计算。然而,页面上的脚本可以立即终止并监听更多用户事件。一旦工作者完成,它就会向页面发送一条返回消息,然后页面可以例如显示结果。


当使用 Web Workers 时,浏览器在脚本执行时间过长时显示的无响应脚本警告已经成为过去。

斐波那契数列示例

接下来是一个工作者的示例,该工作者在后台计算从 0 到 99 的斐波那契数列。实际上,由于使用这种非常低效的方法计算斐波那契数列对于较大的数字(比如大于 30 的数字)可能需要很长时间(或者由于堆栈溢出而崩溃),但如果在工作者中执行,这对主网页的响应性没有影响。因此,您仍然可以绘制复杂的动画,使等待下一个数字的时间更有趣一些。

此 HTML 页面包含一个脚本,该脚本从文件“fib-worker.js”启动一个工作者。来自工作者的消息使用 console.log 显示在浏览器的控制台上。


    
      Web Worker API Demo 
      
    
    
    

实现工作者的 JavaScript 文件包含一个循环,该循环计算斐波那契数列并将结果发送到页面。

// File fib-worker.js
function fib(n) {
   return n < 2 ? n : fib(n-1) + fib(n-2);
}

for(var i = 0; i < 100; ++i) {
   postMessage({
      index: i,
      value: fib(i)
   })
}

在上面的示例中,我们看到我们也可以将复杂对象传递给 postMessage。这些对象可以包含通过 JSON 传输的所有内容。这意味着函数不能跨工作者边界传递,并且对象是按值而不是按引用传递的。

工作者 API

工作者支持一个名为 importScripts 的函数。您可以使用它将更多源文件加载到工作者中。

importScripts("file.js");
importScripts("foo.js", "bar.js");

当您向函数传递多个参数时,脚本将并行下载,但按定义顺序执行。该函数在所有脚本都下载并执行之前不会返回。

这里我们加载一个外部 JavaScript 文件,该文件计算来自字符串的 SHA-1 哈希和,然后我们使用它来哈希来自 AJAX 请求的响应。我们还使用标准的 XMLHttpRequest 对象来检索通过 onmessage 事件传入的 URL 的内容。有趣的是,我们不必担心使 AJAX 请求异步,因为工作者本身相对于页面渲染是异步的,因此等待 HTTP 请求的时间不会那么令人担忧。

importScripts("sha1.js")

function onmessage(event) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', event.data, false);
    xhr.send();
    postMessage(sha1(xhr.responseText));
}

工作者可用的其他 API

工作者可以使用 XMLHttpRequest 进行 AJAX 请求,如上所述,并使用 Web 存储 API 访问客户端数据库。这里的 API 与在常规 JavaScript 中的使用方式相同。

setTimeout 和 setInterval(以及 clearTimeout 和 clearInterval 同伴),它们允许在给定时间段后或以特定间隔执行代码,在工作者中也可用,就像众所周知的 navigator 对象 一样,可以检查该对象以获取有关当前浏览器的信息。

将来可能会添加更多 API。

浏览器兼容性

截至本文撰写时(作者所知),Firefox 3.5 是唯一支持通过 postMessage 传递复杂对象以及实现上述扩展 API 的浏览器。Safari 4 实现了一个非常基本的 Worker API 版本。对于其他浏览器,可以通过 Google Gears 使用工作者,该工具最初将该概念引入到浏览器中。

现实世界中的应用

Bespin 项目 中,这是一个基于浏览器的源代码编辑器,我们成功地使用了工作者来实现 CPU 密集型功能,例如实时源代码错误检查和代码补全。我们还创建了 一个垫片,它根据 Google Gears 实现 Worker API,并将缺失的功能添加到 Safari 4 的工作者实现中,并且 迁移到在 postMessage 接口之上使用透明的自定义事件。这些组件将作为独立库发布,以便将来可以在其他项目中使用。

Web Workers 将在使开放 Web 成为更强大、更复杂的应用程序平台方面发挥重要作用。因为它们最终只是执行 JavaScript,所以很容易使脚本在还没有 Web Workers 的客户端上运行。因此,请立即将它们添加到您的应用程序中,使您的应用程序更具响应性,使用起来更愉快。


17 条评论

  1. Theodora Vorbix

    我们能否将函数传递给工作线程,而不是传递脚本?

    比如

    var worker = new Worker(fibonacci(data),oncallback);

    2009 年 7 月 8 日 下午 6:43

  2. Malte

    @Theodora:不行,这是不可能的,因为该函数可能是一个闭包,它会保留对 DOM 或其他对象的引用,这将违反安全性和隔离性保证。
    项目http://www.nonblocking.io/2009/03/offloading-arbitrary-js-objects-to.html 尝试使用 JS 对象做类似的事情(使用源代码序列化)。

    2009 年 7 月 8 日 下午 11:06

  3. Funtomas

    我可以在消息中传递 JSON 对象吗,比如“构建这些对象的图形”?

    2009 年 7 月 9 日 上午 3:42

  4. dataStorm

    感谢这篇文章!
    有没有办法(也许有人有链接?)用 Web 工作线程加载脚本,如果浏览器不支持 Web 工作线程,就以旧的方式加载脚本?(不重复脚本)。我想开始实施这项新技术,但它需要能够优雅地降级。

    2009 年 7 月 9 日 上午 8:25

  5. Malte

    如果你喜欢穿上潜水服,潜入危险的水域,你可能想尝试集成我们用在 Bespin 中的软件,它最终将成为一个独立的项目:http://www.nonblocking.io/2009/03/offloading-arbitrary-js-objects-to.html

    2009 年 7 月 9 日 下午 2:12

  6. datastorm

    你在跟我说话吗?

    2009 年 7 月 9 日 下午 4:07

  7. Jesse Ruderman

    你不能将函数传递给工作线程,因为它们“可能”是闭包?为什么不直接禁止用*确实*词法引用外部作用域的函数实例化工作线程,而允许其他情况呢?Spidermonkey 已经跟踪了这一点,以执行各种优化;所谓的“空闭包”优化最初是在 bug 452498 中添加的。

    将函数传递给工作线程对我来说比传递文件更自然。

    2009 年 7 月 10 日 下午 1:32

  8. [...] 评论!作为翻译 Mozilla Hacks 博客文章系列的一部分,今天我翻译了《使用 Web 工作线程:更高效地工作》一文。[...]

    2009 年 7 月 19 日 上午 8:29

  9. marcoos

    我已经在我的博客上发布了这篇文章的波兰语翻译:http://blog.marcoos.com/2009/07/19/mozilla-hacks-korzystanie-z-web-workers/ :)

    2009 年 7 月 19 日 上午 8:34

  10. [...] hacks.mozilla.org:使用 Web 工作线程:更高效地工作 [...]

    2009 年 7 月 22 日 下午 3:16

  11. [...] Mozilla Hacks 博客文章系列的一部分,今天我翻译了(简短的)《Firefox 3.5 中的不透明度》一文,作者是 Chris [...]

    2009 年 7 月 23 日 下午 12:11

  12. [...] 在过去几个月里,关于 Web 工作线程的一些很好的信息一直在流传。我不想在博客圈中再添加一篇关于这个主题的介绍,[...]

    2009 年 8 月 18 日 上午 6:01

  13. [...] Web 应用程序的未来 - 超高速 JavaScript、现代 CSS、HTML5、对各种 Web 应用程序标准的支持、可下载字体支持、离线应用程序支持、通过 canvas 和 WebGL 的原始图形处理、[...]

    2009 年 11 月 8 日 下午 4:46

  14. [...] 在过去五年中,显而易见的是,在众多现代浏览器(Firefox、Safari、Opera 和 Chrome)与世界最流行的浏览器 IE 之间,在各个方面都产生了巨大的差异。现代浏览器是为互联网应用的未来而构建的 - 超高速的 JavaScript、现代的 CSS、HTML5、支持各种互联网应用标准、支持可下载字体、支持离线应用、通过 canvas 和 WebGL 支持原生图像处理、原生视频支持、高级 XHR 支持以及高级安全工具和网络功能。[...]

    2009 年 11 月 9 日 上午 0:15

  15. [...] 在过去五年中,显而易见的是,在众多现代浏览器(Firefox、Safari、Opera 和 Chrome)与世界最流行的浏览器 IE 之间,在各个方面都产生了巨大的差异。现代浏览器是为互联网应用的未来而构建的 - 超高速的 JavaScript、现代的 CSS、HTML5、支持各种互联网应用标准、支持可下载字体、支持离线应用、通过 canvas 和 WebGL 支持原生图像处理、原生视频支持、高级 XHR 支持以及高级安全工具和网络功能。[...]

    2009 年 11 月 9 日 上午 5:02

  16. [...] 原作者:Eric Shepherd - 返回原文 [...]

    2010 年 6 月 18 日 上午 1:10

  17. Jason

    不幸的是,由于它们无法访问 DOM,因此在实际应用程序中几乎毫无用处,除非你有一些疯狂的要求(比如递归计算斐波那契数列)。如果我能用它们来渲染图形并将其传回,那就太棒了。它们只能传回字符串这一事实严重限制了它们的实用性。

    2010 年 7 月 9 日 下午 5:55

这篇文章的评论已关闭。