随着浏览器与网页技术的飞速发展,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函数的一个小魔术。这个小魔术能够产生一个指向本页内FileBlob对象的一个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对这个问题有所关注,同样也意味着你可能会丧失一些你觉得非常便捷的功能。

About liuyanghejerry

富有激情的前端工程师,专注GUI开发。

Comments are closed.

Post Navigation