Web Workers
摘自《HTML5 for .NET Developers》的一章
|  | HTML5 for .NET Developers 作者:Jim Jackson II 和 Ian Gilman 在任何用户交互量大的 HTML 应用程序中,您都会遇到相互冲突的需求。本文基于《HTML5 for .NET Developers》的第 8 章,讨论了如何平衡响应式用户界面和执行复杂、CPU密集型任务的需求。 | 
Web Workers 是一组非常简单的接口,允许您在浏览器拥有的线程中开始工作,但该线程与用于更新界面的线程不同。这意味着 JavaScript 开发人员可以进行真正的异步编程。当前将一个线程从一项任务让给另一项任务的方法是由浏览器 JavaScript 引擎自动完成的,虽然结果通常非常快,但并非真正的并发处理,即两个任务同时进行。图 1 显示了传统的方法。

这种方法对于简单的应用程序来说是没问题的,而且随着 JavaScript 引擎速度的提高,它甚至可以用于许多更复杂的应用程序。已经进行了大量的研究和开发,以从传统的 JavaScript 应用程序中榨取每一丝性能。
提示:请参考 O’Reilly 出版的 Nicholas Zakas 的《High Performance JavaScript》,以了解如何充分利用 JavaScript 的单线程。
您可能已经意识到,单线程的问题在于,如果您使用 Canvas 构建图像或调用服务器进行大量数据包的往返通信,那么您所做的任何事情都会减慢您的用户界面、您的复杂任务或两者。随着 Web 应用程序客户端变得越来越复杂,您会发现单线程已经不够用了。图 2 显示了 Web Workers 的新世界是什么样的。

Web Worker 的生命周期只能持续到当前页面在浏览器中存在为止。如果刷新页面或导航到另一个页面,该 worker 对象将丢失,如果您想再次访问它,则需要从头开始重新启动。
Web Workers 的规则
Web Worker 将在浏览器中自己的线程中执行。它可以访问服务器资源以及从宿主线程传递给它的任何数据。它还有能力启动自己的 worker(称为子 worker),然后成为这些 worker 的宿主线程。Web Worker 还可以访问 navigator 对象,这意味着它可以与地理定位 API 通信。它可以进行自己的 ajax 调用并访问 location 对象。
Web Worker 无法访问用户界面的任何部分。这意味着 window 对象中的任何内容以及 document 对象都将被禁止访问。在 JavaScript 应用程序中,声明的任何变量都自动处于全局作用域中,这意味着它们附加到 window。因此,您的 Web Worker 将无法访问任何应用程序变量、函数,也无法响应应用程序其余部分的任何事件或回调。
提示:由于 worker 对象无法访问任何用户界面元素,因此通常不建议引入 jQuery 库。jQuery 是为在 UI 线程上运行而设计的,因此当加载到不包含 window 对象的进程时,它会立即抛出异常。
Web Worker 使用一个 JavaScript 文件来执行,在该脚本中,它可以在启动时引入其他文件。这为将整个应用程序逻辑保留在与用户界面线程不同的线程上提供了可能性,但这远远超出了本章的范围。
规范指出,Web Worker 的设计初衷不是为了短期执行,并且不应指望在启动后仍然可用。这意味着您发送给 Web Worker 的工作结果不应具有用户界面依赖性。Web Worker 很有用但也很“重”,所以尽量不要“杀鸡焉用牛刀”。
将工作发送到另一个线程
Web Worker 不是一个静态对象,像地理定位一样是现成的。相反,它是一个您创建并用变量引用的对象。您可以通过调用 Worker 对象的 new 来做到这一点,并为其传递一个脚本文件。该文件应通过完整 URL 或相对于您网站上当前页面位置的路径来引用。
var myWorker = new Worker('WorkerProceses.js');
创建后,worker 不会超时或停止工作,除非通过 worker.terminate 函数告知它这样做。由于脚本文件加载后 JavaScript 会立即执行到内存中,因此 worker 可以开始执行工作,例如调用地理定位或进行 ajax 数据调用。它也可以立即开始向其父线程发送消息。
宿主对象的 worker 有一个 postMessage(string) 方法,可用于将常规字符串或 JSON 数据传递给 worker。Worker 还有一个消息事件,可以使用 addEventListener 函数或将函数绑定到 onmessage 事件来连接。同样,在 worker 脚本内部,您可以调用 postMessage 并连接到消息事件。宿主进程还有一个 worker.terminate 函数,该函数会停止 worker 对象正在执行的所有工作。调用 terminate 后,worker 对象将不再发送或接收任何其他消息。
Web Worker 示例
创建一个新的 Web 应用程序。添加一个 index.html 页面、一个 Scripts 文件夹以及其中的两个 JavaScript 文件。一个命名为 main.js,另一个命名为 myWorker.js。在 index 页面中,在顶部添加一个指向最新版本 jQuery 的脚本引用,并在 </body> 结束标签之前的底部添加一个指向 scripts/main.js 的引用。接下来,添加一个输入按钮来测试我们的功能。<input type=button id="testWorkers" value="Test Workers" />
最后,在 *main.js* 中,添加以下代码来处理 document.ready 并创建一个简单的对象来包含我们的测试代码。
列表 1 创建一个 worker 并向其发送消息
$(document).ready(function () {
   workerTest.init();                                      #A
});
window.workerTest = {
   myWorker: null,
   init: function () {
      self = this;
      self.myWorker = new
Worker("/Scripts/myWorker.js");         #B
      $("#testWorkers").live("click", 
         function () {
            self.myWorker.postMessage("Test");                    #C
         }
      );
      self.myWorker.addEventListener("message",
         function (event) {
            alert(event.data);                             #D
         }, 
      false);
   }
};
#A Initializes our object using the regular jQuery ready function.
#B Creates a new worker object and assign it as a local variable.
#C Sends the worker a message whenever the test button is clicked.
#D When the worker sends a message back to the host, notify the user.
在 worker 对象中,我们将展示 JavaScript 在脚本加载后立即开始执行,并且它可以立即开始发送消息并响应 post。
列表 2 一个简单的 Web Worker 脚本示例
count = 0;                                          #A
init = function () {
   self.count++;                                    #B
   self.postMessage("start count: " + count);       #C
}
self.addEventListener("message", function (event) {
   self.count++;                                    #D
   setTimeout(function () {
      self.postMessage("Last Msg: " + event.data +  #E
         ", count: " + count);                      #E
   }, 1000);
}, false);
init();                                             #F
#A Since we do not have access to window, we cannot
assign variables to it by default. Variables here are scoped to the script
file.
#B Here, we update the count value to show that work
has been done.
#C Upon initialization, we can call the postMessage
event proving that messages can be sent not in response to any host request.
#D Update the count variable whenever a message is
received.
#E When a message is received, wait one second and respond with the message
sent and the current count value.
#F At the end of the script, we call the init function to start performing
work.
如您所见,宿主应用程序和 Web Worker 之间的整体接口非常简单。主要区别在于宿主在其 worker 变量上调用函数,而客户端在 self 上调用相同的函数。

MessageEvent 对象
在任一方向接收消息时,返回的对象都是 MessageEvent 对象。它有一个 data 属性,该属性是 posted 值的副本,而不是原始数据的一个引用。此外,它还有一小组用于跨文档消息传递的属性。跨文档消息传递是一项允许来自其他域的脚本进行通信的规范。
包含其他脚本
在 worker 脚本中引入其他脚本非常容易。您只需使用要包含的脚本文件的名称调用 importScripts 函数即可。请记住,脚本位置将相对于 worker 脚本在网站中的当前位置。如果您需要导入多个脚本,请使用单个 importScripts 函数调用,并用逗号分隔脚本名称。importScripts("inclA.js", "inclB.js")
在此代码中,您可以定义 varA 和 varB 变量,它们将立即在 worker 脚本中可用,因为它们也将立即执行。列表中脚本的顺序也将得到尊重,因此可以建立依赖关系。如果您需要有条件地加载脚本,您也可以在代码中的任何位置多次调用 importScripts,并使用不同的脚本或条件逻辑来加载不同的脚本。最后,您可以多次加载同一个脚本,但请注意,因为外部脚本中初始化的任何变量在您的主 worker 进程中更改后,当脚本重新加载时将被重置,更不用说脚本每次都会从服务器重新加载。
结束 Worker 进程
我们提到可以通过调用 terminate 来停止 Web Worker。发生这种情况时,worker 进程将不再接收或发送任何消息。这是 worker 进程的真正销毁。虽然对 worker 调用 postMessage 不会抛出异常,但不会执行任何工作。此外,在您的 worker 进程中,任何正在执行的计时器或其他代码都会立即停止。
Web Worker 错误
当 Web Worker 中抛出异常时,它会在宿主中使用 error 事件进行处理。生成的事件对象具有以下属性:
- message—脚本中遇到的实际错误消息。
- filename—发生异常的文件名。
- lineno—脚本文件中遇到异常的行号。
摘要
虽然浏览器肯定可以访问宿主系统的内存和处理器资源,但进行任何繁重处理通常会导致屏幕暂时挂起。您甚至可能会遇到浏览器消息,提示它认为您有一个失控的进程。Web Worker 规范通过允许您创建一个可以在不同线程上执行工作的后台 worker 来解决此问题,从而释放用户界面线程以执行屏幕重排和其他更具交互性的逻辑。
|  | 快速简单的 HTML5 和 CSS3 | 
|   | Sass 和 Compass 实战 | 
|  | JavaScript Ninja 的秘密 | 


