随着 Ajax 和 Web 2.0 应用程序的出现,终端用户被快速响应的 web 应用程序宠坏了。要让 web 应用程序响应得更快,瓶颈一定要解决。瓶颈包括 JavaScript 和后台 I/O 庞大的计算量,这需要从主 UI 显示流程中移除,交给 Web Workers 处理。
Web Workers 规范提供不依赖任何用户界面脚本在后台运行脚本的能力。长期运行脚本不会被响应单击或其他用户交互的脚本中断。Web Workers 允许执行长期任务,同时也不影响页面响应。
Web Workers 出现之前,JavaScript 是现代 web 应用程序的核心。JavaScript 和 DOM 本质上都是单线程的:在任何时间都只能执行一个 JavaScript 方法。即使您的计算机有 4 个内核,在进行长期计算时,也只有一个内核比较繁忙。例如,您在计算到达月球的最佳轨道时,您的浏览器不能渲染一个显示轨迹的动画,以及 — 同时 — 对用户事件作出响应(比如鼠标单击或键盘输入)。
Web Workers 打破了传统 JavaScript 的单线程模式,引入了多线程编程模式。一个 worker 是一个独立的线程。有多个任务需要处理的 web 应用程序不再需要逐个处理任务。反之,应用程序可以将任务分配给不同的 workers。
在本文中,您将学习 Web Workers API。一个实例引导您逐步使用 Web Workers 来构建一个 web 页面。
从下面的 下载表格 下载本文示例的源代码。
Web Workers 的基本组成:
- Worker
- 一个新线程,在后台运行,不会阻塞任何主用户界面脚本(作为后台脚本被调用)。Workers 是相对重量级的,不要大规模使用。
一个 worker 可以执行不少任务,包括并行计算、后台 I/O、以及客户端数据库操作。worker 不应该中断主 UI 或直接操作 DOM;它应该向主线程返回一个消息,并让主线程更新主 UI。
- Subworker
- 在一个 worker 中创建的 worker。Subworkers 必须与父页面同根同源。subworkers 的 URI 是根据父 worker 的地址而不是自己页面地址确定的。
- Shared worker
- 一个可以被多个页面通过多个连接所使用的 worker,共享 worker 和普通 worker 的工作方式略有不同,只有一小部分浏览器支持这一特性。
本小节介绍 Web Workers API 的基本概念.
要创建一个新 worker,您只需要调用 worker 构造函数,worker 脚本 URL 是惟一参数。worker 创建完成的同时启动一个新线程(或者可能是一个新进程,根据您浏览的实现而定)。
worker 完成工作或者遇到一个错误时,您可以使用作业实例的 onmessage 和 onerror 属性从 worker 获取通知。清单 1 是一个样例 worker。
清单 1. 样例 worker myWorker.js
// receive a message from the main JavaScript thread
onmessage = function(event) {
// do something in this worker
var info = event.data;
postMessage(info + “ from worker!”);
};
|
如果您运行清单 2 中的 JavaScript 代码,您将得到 “Hello World from worker” 。
清单 2. 主 JavaScript 线程中的 Worker
// create a new worker
var myWorker = new Worker("myWorker.js");
// send a message to start the worker
var info = “Hello World”;
myWorker.postMessage(info);
// receive a message from the worker
myWorker.onmessage = function (event) {
// do something when receiving a message from worker
alert(event.data);
};
|
一个 worker 是一个线程,是一个高资源消耗的 OS 级对象。当分配给 worker 的任务完成后,或者想要终止时,调用 worker 的 terminate 方法来终止正在运行的 worker。worker 线程或进程即可终止,没有机会完成它的操作以及自身清理。清单 3 是一个示例。
清单 3. 终止 myWorker
myWorker.terminate();
|
和普通 JavaScript 代码类似,运行时错误也可出现在运行的 worker 中。要处理这些错误,您需要为 worker 建立 onerror 处理程序,如果在脚本运行期间出现错误,将会调用该处理程序。要防止发生默认活动,worker 可以调用 worker 错误事件的 preventDefault() 方法。
清单 4. 为 myWorker 添加错误句柄
myWorker.onerror = function(event){
console.log(event.message);
console.log(event.filename);
console.log(event.lineno);
}
|
错误事件有以下 3 个字段,可能对调试有帮助:
message:一个人们可读的错误消息filename:出现错误消息的脚本文件的名称lineno:出现错误消息的脚本文件的行数
Worker 线程可以访问一个全局函数,importScripts(),该函数支持将脚本和数据库导入它们的作用域。它可以不接收参数,也可以接收多个要导入的资源的 URL 作为参数。
清单 5. 导入脚本
//import nothing
importScripts();
//import just graph.js
importScripts('graph.js');
//import two scripts
importScripts('graph.js', 'controller.js');
|
本小节简要介绍 Web Workers 的一个实际用例。该示例包括显示一个含有多个基于 Dojo 的 Website Displayer 小部件的页面。这些小部件过去通常使用 iFrame 来显示一个网站。没有 Web Workers 时,您必须通过 Ajax 请求来获取小部件定义,然后在一个独立的 JavaScript 线程中显示它们。如果小部件定义含有大量数据,这个过程是非常慢的。
该示例创建一些 workers 来获取小部件定义。每个 worker 的任务是获取一个小部件定义,而且负责通知主 UI JavaScript 线程来显示它,这是一个较快的解决方案。
该示例用的是 Dojo 1.4。如果您想在您的浏览器中运行该示例,下载本文所使用的 Dojo 库(见 参考资料)和源代码(见 下载 )。图 1 展示了示例应用程序的结构。
图 1. Web Workers 应用程序
在图 1 中:
- lib 是一个 dojo 库。
- /widgets/WebsiteDisplayer.js 是一个基于 dojo 的 Website Displayer 小部件实现。
- /loadwidget/widgets/widgetDefinition[0....3] 是每个 Website Displayer 小部件的定义。
- /loadwidget/Workers.js 是 worker 实现。
- /loadwidget/XMLHttpRequest.js 是一个 js 库,含有一个创建
XMLHttpRequst的方法。 - /loadwidget/LoadWidget.html 是带有激活的 Web Workers 的演示的主页面,它将会是主 JavaScript 线程。
- /loadwidget/LoadWidget-none-web-workers.html 是在没有 Web Workers 的情况下实现的主页面。
Website Displayer 小部件是一个非常简单的基于 Dojo-TitlePane-dijit 的小部件。它将显示一个规范化标题栏的 UI,如图 2 所示。
图 2. Website Displayer 小部件
清单 6 是 WebsiteDisplayer.js 的代码。
清单 6. WebsiteDisplayer.js 的内容
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");
dojo.require("dijit.TitlePane");
dojo.declare("loadWidget.WebsiteDisplayer", [dijit.TitlePane], {
title: "",
url: "",
postCreate: function() {
var ifrm = dojo.create("iframe", {
src: this.url,
style: "width:100%;height:20%;"
});
dojo.place(ifrm, this.domNode.children[1], "first");
this.inherited(arguments);
var contentFrame = this.domNode.children[1].children[0];
if (contentFrame.attachEvent) {
contentFrame.attachEvent("onload",
function() {
dojo.publish("frameEvent/loaded");
}
);
} else {
contentFrame.onload = function() {
dojo.publish("frameEvent/loaded");
};
}
}
});
|
要实现 worker.js,导入一个全局 JavaScript 文件 XMLHttpRequest.js,其中含有全局方法 creatXMLHTTPRequest。该方法将返回一个 XMLHttpRequest 对象。
worker 主要将 XMLHttpRequest 发送到服务器端,然后检索小部件定义返回给主线程。清单 7 和清单 8 展示了一个示例。
清单 7. Worker.js 的内容
importScripts("XMLHttpRequest.js");
onmessage = function(event) {
var xhr = creatXMLHTTPRequest();
xhr.open('GET', 'widgets/widgetDefinition' + event.data + '.xml', true);
xhr.send(null);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status ==0) {
postMessage(xhr.responseText);
} else {
throw xhr.status + xhr.responseText;
}
}
}
}
|
清单 8. widgetDefinition0.xml
<div dojoType="loadWidget.WebsiteDisplayer" title="This is Test Widget 0" url="http://www.yahoo.com" ></div> |
主 web 页就是您进行这些操作的地方:创建几个 workers;发送消息到 workers 并启动 workers;从 workers 中检索消息;使用检索的消息操作主 UI。
清单 9. 主 web 页
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>
Load widgets with Web Workers
</title>
<style type="text/css">
@import "../lib/dijit/themes/soria/soria.css";
@import "../lib/dojo/resources/dojo.css";
@import "../lib/dojox/layout/resources/GridContainer.css";
@import "../lib/dojox/layout/resources/DndGridContainer.css"
</style>
<script type="text/javascript" src="../lib/dojo/dojo.js"
djConfig="parseOnLoad: true,isDebug:true">
</script>
<script>
dojo.require("dojo.parser");
dojo.require("dojo.io.script");
dojo.require("dojox.layout.GridContainer");
dojo.require("dijit.layout.LayoutContainer");
dojo.require("dijit.TitlePane");
dojo.require("dojox.layout.DragPane");
dojo.registerModulePath("loadWidget", "../../loadWidget");
dojo.require("loadWidget.WebsiteDisplayer");
</script>
<script type="text/javascript" language="javascript">
var workersCount = 4;
var haveLoadedCount = 0;
var widgetCount = 4;
var startTime = new Date().getTime();
var endTime = null;
var executeTime = 0;
try {
for (var i = 0; i < workersCount; i++) {
var loadWorker = new Worker("Worker.js");
loadWorker.postMessage(i);
loadWorker.onmessage = processReturnWidgetDefinition;
loadWorker.onerror = handleWorkerError;
}
} catch(ex) {
console.log(ex);
}
function processReturnWidgetDefinition(event) {
var txt = document.createElement("p");
txt.innerHTML = event.data;
var div = document.getElementById("loadingDiv");
div.appendChild(txt);
haveLoadedCount++;
if (haveLoadedCount == widgetCount) {
dojo.parser.parse();
}
}
function handleWorkerError(event){
console.log(event.message);
}
dojo.subscribe("frameEvent/loaded", dojo.hitch(null, handelFrameLoaded));
function handelFrameLoaded() {
if (haveLoadedCount == widgetCount) {
endTime = new Date().getTime();
executeTime = endTime - startTime;
dojo.byId("loading").innerHTML = "Loading cost time:" + executeTime;
}
}
</script>
</head>
<body class="soria">
<div dojoType="dijit.TitlePane" title="Load widgets with Web Workers"
style="border: 2px solid black; padding: 10px;"
id="main">
<div id="loadingDiv">
<div id="loading">
Widgets are loading......
</div>
</div>
</div>
</body>
</html>
|
将这个主页面嵌入到一个 web 应用程序中,然后运行它。结果如图 3 所示。
图 3. 使用 Web Workers 加载小部件
想要查看使用 Web Workers 和不使用 Web Workers 的区别,分别运行 LoadWidget.html 和 LoadWidget-none-web-workers.html,然后查看结果。注意,在这里没有运行 Web Workers 的页面比运行 Web Workers 的页面完成得要快,这是因为代码样例处理的数据太少。实际上,节省的时间平衡了启动 worker 的成本。
上面的示例只涉及 XMLHttpRequest 和计算;不是很大也不复杂。如果您让 worker 处理更复杂的任务,比如处理大量计算,它将会是一个功能强大的特性。在将这个很酷的技术运用到您的项目之前,了解一些使用技巧。
为了安全,workers 不能直接对 HTML 进行操作。同一 DOM 上的多线程操作可能会引发线程安全问题。优势是您不再担忧 worker 实现中的多线程安全问题。
这在开发 worker 时有一些局限性,您不能在 worker 中调用 alert(),这是一个非常流行的调试 JavaScript 代码的方法。您也不能调用 document.getElementById(),因为它只能检索和返回变量(可能是字符串、数组、JSON 对象,等等)。
尽管 worker 不能访问 window 对象,但可以直接访问 navigator。您也可以在 navigator 对象中访问 appName、appVersion、platform 和 userAgent。
location 对象可以以只读方式访问。您可以在 location 对象中获取 hostname 和 port。
在 worker 中也支持 XMLHttpRequest,如本文示例所示。有了这一特性,您就可以将大量感兴趣的扩展添加到 worker 中。
此外还有:
-
importScripts()方法(在同一个域上访问脚本文件) - JavaScript 对象,比如
Object、Array、Date、Math和String setTimeout()和setInterval()方法
postMessage 的使用十分频繁,因为它是主 JavaScript 线程的主要方法,用于和 workers 交互。然而,现在 postMessage 中携带的数据类型仅限于本地 JavaScript 类型,比如,Array、Date、Math、String、JSON 等等。结构复杂的自定义 JavaScript 对象不能被很好地支持。
| 描述 | 名字 | 大小 | 下载方法 |
|---|---|---|---|
| 本文样例代码 | source.zip | 4KB | HTTP |
学习
- 阅读 Web Workers 规范,了解如何定义一个允许您在您的主页上生产并行运行脚本的后台 workers 的 API。
- 探究 Dijit Reference Guide。 Dijit 是 Dojo 的 UI Library,作为一个独立的名称空间 dijit 存在。Dijit 需要 Dojo Core.
。
- “使用 HTML 5 创建移动 Web 应用程序,第 4 部分:使用 Web Workers 来加速您的移动 Web 应用程序”(developerWorks,2010 年 6 月)了解如何使用 Web Workers 并发现哪些任务最适合它们。
- 原来的 HTML 5 中,Web Workers API 被分离出去成为独立W3C Web Workers 规范。
-
developerWorks Web development
专区:通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。
-
developerWorks Ajax 资源中心:这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
-
developerWorks Web 2.0 资源中心,这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过 Web 2.0 新手入门 栏目,迅速了解 Web 2.0 的相关概念。
- 查看 HTML5 专题,了解更多和 HTML5 相关的知识和动向。
获得产品和技术
- 下载 Dojo 库。
-
IBM 产品评估试用版软件 或 IBM SOA 人员沙箱,并开始使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。
讨论
- 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。
- 现在就创建您的 developerWorks 社区个人档案,然后 建立一个关于 Web Workers 的关注清单。与 developerWorks 社区 建立连接并保持连接。
- 找到 对 web 开发感兴趣的其他 developerWorks 成员。
- 分享您的知识:加入一个关注 web 主题的 developerWorks 群组。
- Roland Barcia 在其博客中讨论了 Web 2.0 和中间件。
- 关注 developerWorks 成员 关于 web 主题的共享书签。
- 快速找到答案:访问 Web 2.0 Apps 论坛。

