概述
JavaScript 语言采用的是单线程模型,在同一时刻只能处理一个任务。我们会通过 setTimeout()、setInterval()、ajax 和Promise等技术模拟“并行”,但都不是真正意义上的并行。
Web Workers 是 HTML5 标准的一部分,就是为了Javascript 创造多线程环境。在主线程中创建Worker线程,将一部分程序分配给后者运行。在主线程执行任务的同时,worker 线程也可以在后台执行它自己的任务,两者互不干扰。
一些计算密集型或高延迟的任务可以交由 Worker 线程执行,执行完成后将消息返回主线程,主线程(通常负责 UI 交互)能够保持流畅,不会被阻塞或拖慢。
主线程
创建worker对象
主线程调用new Worker()构造函数,新建一个 Worker 线程,构造函数可以接受两个参数。具体可见示例。
第一个参数是脚本的网址,生成这个 url 的方法有两种:
一.脚本文件
var worker = new Worker('http://~work.js');
由于Worker有以下2个限制,这个脚本必须来自网络:
-
同源限制:分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
-
文件限制:Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
二.字符串形式
const data = `
// worker线程 do something
`;
// 转成二进制对象
const blob = new Blob([data]);
// 生成url
const url = window.URL.createObjectURL(blob);
// 加载url
const worker = new Worker(url);
先将待执行的程序放在字符串中,转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。
也可以载入与主线程在同一个网页的代码:
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
// worker线程 do something
</script>
</body>
</html>
注意必须指定<script>标签的type属性是一个浏览器不认识的值,上例是app/worker。
然后,读取这一段嵌入页面的脚本,用 Worker 来处理。
var blob = new Blob([document.querySelector('#worker').textContent]);
// 生成url
const url = window.URL.createObjectURL(blob);
// 加载url
const worker = new Worker(url);
第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程
// 主线程
var worker = new Worker(url, { name : 'myWorker' });
// Worker 线程
self.name // myWorker
主线程通信
一. 向 worker 线程发送消息
主线程调用postMessage方法,向 Worker 进程发消息。
worker.postMessage({type: 'msg', data: 'hello world!'});
它们之间通信是通过深拷贝的形式来传递数据的,进行传递的对象需要经过序列化,接下来在另一端还需要反序列化。这就意味着:
-
我们不能传递不能被序列化的数据(比如函数),会抛出错误的,可以传递对象和数组。
-
在一端改变数据,另外一端不会受影响,因为数据不存在引用,是深拷贝过来的。
二. 监听 worker 线程返回的信息
主线程通过onmessage指定监听函数,接收子线程发回来的消息
worker.onmessage = function (event) {
// event对象的data属性可以获取 Worker 发来的数据
console.log('worker返回的消息', event.data);
}
三. 监听错误
监听错误主要分2种:
一种是主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error事件。
// worker线程报错
worker.onerror = e => {
// e.filename - 发生错误的脚本文件名;
// e.lineno - 出现错误的行号;
// e.message - 可读性良好的错误消息
console.log('onerror', e);
};
另一种是指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发主线程的onmessageerror事件。
// messageerror报错
worker.onmessageerror = e => {
// e.filename - 发生错误的脚本文件名;
// e.lineno - 出现错误的行号;
// e.message - 可读性良好的错误消息
console.log('onerror', e);
};
四. 关闭 worker 线程
Worker 比较耗费计算机的计算资源(CPU)的原因,一旦使用完毕,就应该关闭 worker 线程。
// 主线程关闭worker线程
worker.terminate();
Worker线程
一. 全局变量
worker 线程的执行上下文是一个为WorkerGlobalScope,与主线程的上下文(window)不一样,很多接口拿不到。
我们可以使用self/WorkerGlobalScope来访问全局对象。
在Worker线程中,存在以下的限制:
-
DOM 限制:无法读取主线程所在网页的 DOM 对象,也无法使用
document、window、parent这些对象。 -
全局对象限制:Worker 的全局对象
WorkerGlobalScope,只定义了Navigator接口和Location接口。注:浏览器实际上支持 Worker 线程使用
console.log,保险的做法还是不使用这个方法。 -
脚本限制:Worker 线程不能执行
alert()方法和confirm()方法,但可以使用XMLHttpRequest对象发出AJAX请求以及定时器、应用缓存。
二. 监听主线程传过来的信息
在Worker线程中,使用onmessage方法监听主线程传过来的信息。
self.onmessage = function (event) {
// event对象的data属性可以获取主线程传过来的信息
console.log('主线程的消息', event.data);
}
三. 发送信息给主线程
在Worker线程中,使用postMessage方法发送信息给主线程。
self.postMessage({"name": '张三'});
四. 监听错误
监听错误为 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发主线程的onmessageerror事件。
// messageerror报错
self.onmessageerror = e => {
// e.filename - 发生错误的脚本文件名;
// e.lineno - 出现错误的行号;
// e.message - 可读性良好的错误消息
console.log('onerror', e);
};
五. 关闭 Worker 线程
在Worker线程中,调用close方法,可以关闭 Worker 线程。
self.close();
六. 加载脚本
Worker 线程能够访问一个全局函数 imprtScripts()来引入脚本,该函数接受 0 个或者多个 URI 作为参数。
importScripts('http://~1.js','http://~2.js');
-
脚本中的全局变量都能被 worker 线程使用。
-
脚本的下载顺序是不固定的,但执行时会按照传入 importScripts() 中的文件名顺序进行,这个过程是同步的。
七. 多个 worker 线程
在主线程内可以创建多个 worker 线程,使用同源的脚本文件创建。
八. 线程间转移二进制数据
前面说过,主线程与 Worker 之间的通信是一种深拷贝关系,在传递大文件时,会产生性能问题,比如主线程向 Worker 发送一个 30MB 文件。
为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。
这种方法叫做Transferable Objects。
// 创建二进制数据
var uInt8Array = new Uint8Array(1024 * 1024 * 30); // 30MB
// Transferable Objects 格式,即使用格式(a,[a]) 来转移二进制数据
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
结语
Web Workers的出现,让我们把耗能的操作分配给后台来做,在很大程度上缓解了主页面的UI渲染阻塞,提升了页面性能。