随着浏览器与网页技术的飞速发展,JavaScript正在扮演着越发重要的角色。传统的JavaScript在多个任务同时处理时容易将用户界面冻结,Web Worker是允许浏览器将某些JavaScript独立至另一个线程的技术。Web Worker是与HTML5一同发展起来的一项技术标准,它并不属于HTML5的一部分,但和HTML5能够非常良好的一起工作。
本文内容翻译自http://www.html5rocks.com/en/tutorials/workers/basics/,部分内容有所删改,如果您对其中的任何技术细节存在疑问,请以原文为准。
传统的JavaScript是单线程的,这样就限制了一些计算密集的程序进行并发。在过去,开发者们尝试过一些其它措施来模拟并发,比如使用定时器定点触发某些函数,但如果一项计算的所需的时间较多,仍然可能让程序显得阻塞起来了。Web Worker将多线程带给了JavaScript,能够在很大程度上缓解。
Web Worker最大的好处即是一些计算密集或者需要长时间运行的代码,单独置于一个线程当中进行,使其不致于对用户交互造成干扰,这就像一个后端代码一样。
目前,Web Worker分为两种,一种是 Dedicated Worker ,另一种则是 Shared Worker 。其中Dedicated Worker的使用相对简单一些,我们先从它入手。
Dedicated Worker
如果不是大规模依赖于Web Worker,通常我们都选择Dedicated Worker。创建一个Dedicated Worker非常简单:
var worker = new Worker('task.js');
上面的代码将创建一个新的Web Worker。Worker所需的初始化参数是一个URI,指向目标代码文件,代码文件将有浏览器异步进行下载执行,在下载完毕之前Dedicated Worker都不会被彻底实例化。如果文件下载失败,比如发生404错误,那么Dedicated Worker的初始化就将失败。注意,使用Worker创建的就是Dedicated Worker。
在建立之后,我们需要某种手段与Dedicated Worker进行通信了。
与Dedicated Worker通信
Web Worker的通信手段主要依靠的是消息传递,主线程通过消息的发送,将所要执行的任务通知Web Worker,Web Worker在执行结束过后再将结果以消息的形式反馈给主线程。
主线程:
var worker = new Worker('doWork.js'); worker.addEventListener('message', function(e) { //监听Worker发送回主线程的消息 console.log('Worker said: ', e.data); }, false); worker.postMessage('Hello World'); // 向worker发送消息
doWork.js:
self.addEventListener('message', function(e) { self.postMessage(e.data); //这将向主线程发送消息 }, false);
主线程首先新建一个Web Worker,紧接着增加了一个对Web Worker消息事件的监听函数,最后将消息发送至Web Worker对象。在Web Worker执行完毕的时候,会向主线程发送消息,主线程通过之前安置好的监听函数进行处理。
在这个例子中,原本位于一个线程中的数据被发送至了另一个线程。看起来这很神奇,但实际上主线程与Worker线程直接的消息数据是通过复制传递的,而不是我们可能期待的引用传递。比如在JSON的传递过程中,数据将被串行化,之后进行复制,最后再在目标线程重新组装起来。
在Chrome 17当中,提供了一种额外的方式,借由Transferable Objects(可传输对象),能够实现数据非复制的传递。这类似于C++当中的引用类型,但这种传递将彻底转移数据的所有权,原上下文将无法继续获取该对象。当然,这是为了线程安全。
现在来看一个稍微复杂一些的例子:
主线程:
doWork2.js:
self.addEventListener('message', function(e) { var data = e.data; switch (data.cmd) { case 'start': self.postMessage('WORKER STARTED: ' + data.msg); break; case 'stop': self.postMessage('WORKER STOPPED: ' + data.msg + '. (buttons will no longer work)'); self.close(); // 自我销毁 break; default: self.postMessage('Unknown command: ' + data.msg); }; }, false);
上面的例子就传递了JSON来进行交互,同时也提及了两种结束web worker线程的方式:在主线程中worker.terminate()
或在worker当中直接调用self.close()
。
细看Worker
对于Worker,使用self
和使用this
是等价的,因此,上面的代码也可以写成这样(this
直接省写了):
addEventListener('message', function(e) { var data = e.data; switch (data.cmd) { case 'start': postMessage('WORKER STARTED: ' + data.msg); break; case 'stop': ... }, false);
不仅如此,onmessage
的事件处理函数也可以直接赋值:
onmessage = function(e) { var data = e.data; ... };
Worker的特性
Web Worker是多线程的,在很多实现当中,它甚至是在操作系统级别多线程的,因此它所能够使用的特性仅仅是普通JavaScript的一个子集:
navigator
对象location
对象(只读)XMLHttpRequest
setTimeout()
/clearTimeout()
以及setInterval()
/clearInterval()
- 应用缓存(Application Cache)
- 通过
importScripts()
导入其它脚本 - 开启更多的Web Worker
特别注意,以下特性将不能在Web Worker上下文当中使用:
- DOM(因其非线程安全)
window
对象document
对象parent
对象
现在我们来看看这当中一些有用的特性:
导入脚本
在worker当中,我们可以引入更多的脚本用以支持,比如一些常用的库和框架。例子:
importScripts('script1.js'); importScripts('script2.js'); importScripts('script3.js', 'script4.js'); //也可以这样
子线程Web Worker
Web Worker还能够继续产生Web Worker,这对于一些大型计算任务来说非常有帮助。不过在产生更多子线程时应当注意:
- 子线程的文件读取必须与主线程页面同域。也就是不能跨域读取。
- URI以其父线程所在的位置开始计算相对地址,而不是主线程。
内联Web Worker代码
仅仅通过脚本文件来开启新的线程对于一些情景可能还不够好,有时候甚至需要执行的代码都是动态构造的,比如某某网站需要让用户来输入那些子线程代码。这时候,我们需要依赖 BlobBuilder
来进行辅助了。
// Webkit, Chrome 12, 及FF6需要前缀:window.WebKitBlobBuilder, window.MozBlobBuilder var bb = new BlobBuilder(); bb.append("onmessage = function(e) { postMessage('msg from worker'); }"); // 获取Blob的URL,来作为worker初始化的文件地址参数 // 注意,在Chrome 10+中需要使用:window.webkitURL.createObjectURL() var blobURL = window.URL.createObjectURL(bb.getBlob()); var worker = new Worker(blobURL); worker.onmessage = function(e) { // e.data == 'msg from worker' }; worker.postMessage(); // 开启worker
Blob URL
Blob所在位置的URL是什么?实际上这是createObjectURL
函数的一个小魔术。这个小魔术能够产生一个指向本页内File
或Blob
对象的一个URL,比如:
blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1
这样的URL在整个页面生存期都是独一无二的,如果你在代码当中需要创建许多blob对象,那么通过window.URL.revokeObjectURL()
及时释放掉不需要的将大有裨益。
window.URL.revokeObjectURL(blobURL); // window.webkitURL.createObjectURL() in Chrome 10+.
完整示例
引入外部脚本
如果使用内联代码来开启新的线程,importScripts()
函数所导入的外部脚本地址就不得不采用绝对地址。如果尝试使用相对地址,将使得浏览器提示你与blob所在地址的协议名不同(一个是blob
一个是http
),最终产生跨域错误。
解决这一问题目前没有什么好主意,只能利用主线程所在页面的地址来进行加工处理。假设主页面所在地址为http://example.com/index.html,则我们用以下代码来绕过这一限制:
...
异常处理
web worker中的代码和普通JavaScript代码一样能够抛出异常,其异常将通过触发ErrorEvent来通知主线程。ErrorEvent
有三项属性对我们很有帮助:
filename
– 发生错误的worker所执行的代码文件名lineno
– 错误行号message
– 错误的详细描述
来看一个例子:
workerWithError.js:
self.addEventListener('message', function(e) { postMessage(1/x); // Intentional error. };
Shared Worker
Shared Worker是对Dedicated Worker的一个增强,它允许一个web worker线程同时与多个调用者进行通信,能够将web worker线程当中的数据进行共享。这对于某些应用非常友好。例如一个提供地图服务的应用可能需要很多个不同的视图,但它的数据更新源只有单独的一个,这样我们就可以用Shared Worker来实现。我们来看一个简单的实例:
主页面:
Shared workers: demo 3 Log:
次页面:
Shared workers: demo 3 inner frame Inner log:
sharedWorker.js:
var count = 0; // 每次调用shared worker会触发onconnect事件 onconnect = function(e) { count += 1; var port = e.ports[0]; port.postMessage('Hello World! You are connection #' + count); port.onmessage = function(e) { port.postMessage('pong'); } }
我们现在有两个HTML文件了,其中主页面是我们访问的页面,而次页面则是通过iframe
嵌入在主页面当中的。在这两个页面当中,我们都调用同一个shared worker。如果你有兴趣跑一下这个例子,最终会看到计数器确实累计起来了,也就是说,两次shared worker调用数据共享了。
在上面这个例子中,我们可以看到,shared worker和dedicated worker的相异之处:
Shared Worker | Dedicated Worker |
使用SharedWorker作为类名 | 使用Worker作为类名 |
通过worker.port.xxx调用函数 | 通过worker.xxx调用函数 |
worker中数据共享 | worker中数据独立 |
一语安全
安全与代码息息相关。在Web Worker的使用当中,我们看起来已经得到了许多保障,但有些敏感的问题仍旧值得一提——说出来总比不说强:
- 线程安全。JavaScript中的Web Worker限制了代码对一些容易产生问题的访问,比如DOM,这样损失了一些易用性,但换来了安全性。不过这不意味着我们可以忘掉线程安全了,只是不容易犯错。
- 跨域读取。跨域读取在HTML当中一直是一个敏感问题,庆幸的是,Web Worker对这个问题有所关注,同样也意味着你可能会丧失一些你觉得非常便捷的功能。