简介: Web Workers,一个新的 JavaScript 编程模型,可以提高您 web 应用程序的交互性。有了它您就可以以一种多线程方法运行 JavaScript,而且可以在后台运行脚本而不依赖任何用户界面脚本。本文介绍了 Web Workers,并引导您了解一个实践示例,向您展示如何将 Web Workers 运用到您的 web 应用程序中。
随着 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 对象不能被很好地支持。
示例源代码下载:http://www.ibm.com/developerworks/apps/download/index.jsp?contentid=769424&filename=source.zip&method=http&locale=zh_CN