HTML5 之 Web Worker

1. Web Worker 简介

1.1. 什么是 Web Worker

Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。工作线程允许开发人员编写能够长时间运行而不被用户所中断的后台程序, 去执行事务或者逻辑,并同时保证页面对用户的及时响应,可以将一些大量计算的代码交给 web worker 运行而不冻结用户界面。

1.2. Web Worker 问世背景

JavaScript 引擎是单线程运行的,JavaScript 中耗时的 I/O 操作都被处理为异步操作,它们包括键盘、鼠标 I/O 输入输出事件、窗口大小的 resize 事件、定时器(setTimeout、setInterval)事件、Ajax 请求网络 I/O回调等。当这些异步任务发生的时候,它们将会被放入浏览器的事件任务队列中去,等到JavaScript运行时执行线程空闲时候才会按照队列先进先出的原则被一一执行,但终究还是单线程。

很多人觉得异步(promise async/await),都是通过类似 event loop 在平常的工作中已经足够,但是如果做复杂运算,这些异步伪线程的不足就逐渐体现出来,比如 setTimeout 拿到的值并不正确,再者假如页面有复杂运算的时候页面很容易触发假死状态,为了有多线程功能,webworker 问世了。不过,这并不意味着 JavaScript 语言本身就支持了多线程,对于 JavaScript 语言本身它仍是运行在单线程上的, Web Worker 只是浏览器(宿主环境)提供的一个能力/API。

JavaScript 事件机制

1.3. Web Worker 类型

Web workers 可分为两种类型:专用线程 dedicated web worker,以及共享线程 shared web worker。Dedicated web worker 随当前页面的关闭而结束;这意味着Dedicated web worker 只能被创建它的页面访问。与之相对应的Shared web worker 可以被多个页面访问。在 Javascript 代码中,Work类型代表 Dedicated web worker,而SharedWorker类型代表 Shared web worker。
而 Shared Worker 则可以被多个页面所共享(同域情况下)。

1.4. 浏览器兼容性

Web Worker 浏览器兼容性

2. 使用方法

2.1. 创建 Web Worker

Web Worker的创建是在主线程当中通过传入文件的url来实现的。如下所示:

1
var webWorker = new Worker("webWorker.js");

2.2. 事件监听

返回的是 webWorker 实例对象,该对象是主线程和其他线程的通讯桥梁
主线程和其他线程可以通过如下相关的API进行通讯 。

onmessage: 监听事件
postmessage: 传送事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//主线程 main.js
var worker = new Worker("worker.js");
worker.onmessage = function(event){
// 主线程收到子线程的消息
};
// 主线程向子线程发送消息
worker.postMessage({
type: "start",
value: 12345
});

//web worker.js
onmessage = function(event){
// 收到
};
postMessage({
type: "debug",
message: "Starting processing..."
});

2.3. 终止 Web Worker

如果在某个时机不想要 Worker 继续运行了,那么我们需要终止掉这个线程,可以调用 在主线程 worker 的 terminate 方法或者在相应的线程中调用 close

1
2
3
4
5
6
7
// 方式一 main.js 在主线程停止方式
var worker = new Worker('./worker.js');
...
worker.terminate();

// 方式二、worker.js
self.close();

2.4. 错误机制

提供了onerror API

1
2
3
4
5
6
7
worker.addEventListener('error', function (e) {
console.log('MAIN: ', 'ERROR', e);
console.log('filename:' + e.filename + '-message:' + e.message + '-lineno:' + e.lineno);
});
// event.filename: 导致错误的 Worker 脚本的名称;
// event.message: 错误的信息;
// event.lineno: 出现错误的行号;

2.5. 简单实例

2.5.1 webWork.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webWorker</title>
</head>
<body>

<p>计数:
<output id="result"></output>
</p>
<button onclick="startWorker()">开始工作</button>
<button onclick="stopWorker()">停止工作</button>

<script>
var webWorker;

function startWorker() {
if (typeof(Worker) !== "undefined") {
if (typeof(webWorker) == "undefined") {
webWorker = new Worker("webWorker.js");
}
webWorker.onmessage = function (event) {
document.getElementById("result").innerHTML = event.data;
};
} else {
document.getElementById("result").innerHTML = "抱歉,你的浏览器不支持 Web Workers...";
}
}

function stopWorker() {
webWorker.terminate();
webWorker = undefined;
}
</script>

2.5.2 webWork.js

1
2
3
4
5
6
7
8
9
10
var i=0;

function timedCount()
{
i=i+1;
postMessage(i);
setTimeout("timedCount()",500);
}

timedCount();

3. sharedWorker

3.1. sharedWorker 介绍

对于 Web Worker ,一个 tab 页面只能对应一个 Worker 线程,是相互独立的;
而 SharedWorker 提供了能力能够让不同标签中页面共享的同一个 Worker 脚本线程;
当然,有个很重要的限制就是它们需要满足同源策略,也就是需要在同域下;
在页面(可以多个)中实例化 Worker 线程:

3.2. 浏览器兼容性

sharedWorker 浏览器兼容性

3.3. 简单实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// main.js

var myWorker = new SharedWorker("worker.js");

myWorker.port.start();

myWorker.port.postMessage("hello, I'm main");

myWorker.port.onmessage = function(e) {
console.log('Message received from worker');
}

// worker.js
onconnect = function(e) {
var port = e.ports[0];

port.addEventListener('message', function(e) {
var workerResult = 'Result: ' + (e.data[0]);
port.postMessage(workerResult);
});
port.start();
}

4. 环境与作用域

在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象,所以一般来说他只能来执行纯 JavaScript 的计算操作。但是,他还是可以获取到部分浏览器提供的 API 的:

setTimeout()clearTimeout()setInterval()clearInterval():有了这些函数,就可以在 Worker : 线程中可以再创建 worker;XMLHttpRequest 对象:意味着我们可以在 Worker 线程中执行 ajax 请求;navigator 对象:可以获取到 AppNameappVersionplatformuserAgent 等信息;location 对象(只读):可以获取到有关当前 URL 的信息;Application CacheindexedDBWebSocketPromise

5. 库或外部脚本引入和访问

在线程中,提供了 importScripts 方法,如果线程中使用了 importScripts 一般按照以下步骤解析。

  1. 解析 importScripts 方法的每一个参数
  2. 如果有任何失败或者错误,抛出 SYNTAX_ERR 异常
  3. 尝试从用户提供的 URL 资源位置处获取脚本资源
  4. 对于 importScripts 方法的每一个参数,按照用户的提供顺序,获取脚本资源后继续进行其它操作
1
2
3
4
5
6
7
8
// worker.js
importScripts('math_utilities.js');
onmessage = function (event)
{
var first=event.data.first;
var second=event.data.second;
calculate(first,second); // calculate 是math_utilities.js中的方法
};

也可以一次性引入多个脚本:

1
2
//可以多起一次传入
importScripts('script1.js', 'script2.js');

6. 通讯原理

从一个线程到另一个线程的通讯实际上是一个值拷贝的过程,实际上是先将数据JSON.stringify之后再JSON.parse。主线程与子线程之间也可以交换二进制数据,比如FileBlobArrayBuffer等对象,也可以在线程之间发送。但是,用拷贝方式发送二进制数据,会造成性能问题。比如,主线程向子线程发送一个 50MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,转移后主线程无法再使用这些数据,这是为了防止出现多个线程同时修改数据的问题,这种转移数据的方法,叫做Transferable Objects

不过现在很多浏览器支持transferable objects(可转让对象) ,这个技术是零拷贝转移,能大大提升性能,可以指定传送的数据全都是零拷贝。

1
2
var abBuffer = new ArrayBuffer(32);
aDedicatedWorker.postMessage(abBuffer, [abBuffer]);
1
2
3
4
5
6
7
var objData = {
"employeeId": 103,
"name": "Sam Smith",
"dateHired": new Date(2006, 11, 15),
"abBuffer": new ArrayBuffer(32)
};
aDedicatedWorker.postMessage(objData, [objData.abBuffer]);

7. 应用场景

  1. 使用专用线程进行数学运算
    Web Worker 最简单的应用就是用来做后台计算,而这种计算并不会中断前台用户的操作
  2. 图像处理
    通过使用从 <canvas> 或者 <video> 元素中获取的数据,可以把图像分割成几个不同的区域并且把它们推送给并行的不同 Workers 来做计算
  3. 大量数据的检索
    当需要在调用 ajax 后处理大量的数据,如果处理这些数据所需的时间长短非常重要,可以在 Web Worker 中来做这些,避免冻结 UI 线程
  4. 背景数据分析
    由于在使用 Web Worker 的时候,我们有更多潜在的 CPU 可用时间,我们现在可以考虑一下 JavaScript 中的新应用场景。例如,我们可以想像在不影响 UI 体验的情况下实时处理用户输入。利用这样一种可能,我们可以想像一个像 Word(Office Web Apps 套装)一样的应用:当用户打字时后台在词典中进行查找,帮助用户自动纠错等等

8. 限制

  1. 兼容性
  2. 不能访问 DOMBOM 对象,locationnavigator 的只读访问,并且 navigator 封装成了 WorkerNavigator 对象,更改部分属性。无法读取本地文件系统
  3. 子线程和父级线程的通讯是通过值拷贝,子线程对通信内容的修改,不会影响到主线程。在通讯过程中值过大也会影响到性能(解决这个问题可以用transferable objects
  4. 并非真的多线程,多线程是因为浏览器的功能
  5. 条数限制,大多浏览器能创建 webWorker 线程的条数是有限制的,虽然可以手动去拓展,但是如果不设置的话,基本上都在20条以内,每条线程大概5M左右,需要手动关掉一些不用的线程才能够创建新的线程

9. 优秀开源框架

tagg2