数据通信
主线程与Worker之间的通信内容,可以是文本,也可以是对象。但需要注意的是,这种通信是拷贝关系,也就是说传值不传址,Web Worker对通信内容的修改不会影响到主线程。
浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给Worker,Web Worker再将它还原。
主线程与Worker之间也可以交换二进制数据,比如:File、Blob、ArrayBuffer等类型,也可以在线程之间发送。For example:
// 主线程
let uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (let i = 0; i < uInt8Array.length; i++) {
uInt8Array[i] = i * 2; // [0,2,4,6,8,...]
}
worker.postMessage(uInt8Array);
// Worker 线程
self.onmessage = (e) => {
let uInt8Array = e.data;
postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
}
但是,拷贝方式发送二进制数据,会造成性能问题。比如:主线程向Worker发送一个500MB文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,Javascript允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这就使主线程可以快速把数据交给Worker,对于影像处理、声音处理、3D运算等就非常方便了,不会产生性能负担。
如果要直接转移数据,可以使用以下写法:
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// eg:
let ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
同页面的 Web Worker
通常情况下,Worker载入的是一个单独的Javascript脚本文件,但是也可以载入与主线程在同一个网页的代码。
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', () => {
postMessage('some message');
}, false)
</script>
</body>
</html>
上面是一段嵌入网页的脚本,注意必须指定 script 标签的type属性是一个浏览器认识的值,上例是app/worker。 然后,读取这一段嵌入页面的脚本,用Worker来处理。
let blob = new Blob([document.querySelector('#worker').textContent]);
let url = window.URL.createObjectURL(blob);
let worker = new Worker(url);
worker.onmessage = (e) => {
// e.data === 'some message'
}
上面的代码中,先将嵌入网页的脚本代码转成一个二进制对象,然后为这个二进制对象生成URL,再让Worker加载这个URL。这样就做到了,主线程和Worker的代码都在同一个网页上面。
实例:Worker线程完成轮询
有时,浏览器需要轮询服务器状态以便第一时间得知状态改变。这个工作可以放在Worker里面。For example:
createWorker (f) => {
let blob = new Blob(['(' + f.toString() + ')()']);
let url = window.URL.createObjectURL(blob);
let worker = new Worker(url);
return worker;
}
let pollingWorker = createWorker((e) => {
let cache;
compare (new, old) {
...
};
setInterval(() => {
fetch('/my-api-endpoint').then((res) => {
let data = res.json();
if(!compare(data, cache)) {
cache = data;
self.postMessage(data);
}
})
}, 1000)
});
pollingWorker.onmessage = () => {
// render data
}
pollingWorker.postMessage('init);
上面代码中,Worker 每秒钟轮询一次数据,然后跟缓存做比较。如果不一致,就说明服务端有了新的变化,因此就要通知主线程。
Worker 线程内部还能再新建Worker线程(目前只有Firefox浏览器支持)。
API
主线程
浏览器原生提供Worker()构造函数,用来供主线程生成Worker线程。
let myWorker = new Worker(jsUrl, options);
Worker()构造函数可以接受两个参数。
- 第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载JS脚本,否则会报错。
- 第二个参数是配置对象,该对象是可选的。它的一个作用就是指定Worker的名称,用来区分多个Worker线程。
// 主线程
let myWorker = new Worker('worker.js', { name : 'myWorker' });
// Worker 线程
self.name // myWorker
Worker() 构造函数返回一个 Worker 线程对象,用来供主线程操作 Worker。Worker 线程对象的属性和方法如下:
- Worker.onerror: 指定error事件的监听函数。
- Worker.onmessage: 指定message 事件的监听函数,发送过来的数据在Event.data属性中。
- Worker.onmessageerror: 指定messageerror事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
- Worker.postMessage(): 向Worker线程发送消息。
- Worker.terminate(): 立即终止Worker线程。
Worker 线程
Web Worker有自己的全局对象,不是主线程的window,而是一个专门为Worker定制的全局对象。因此定义在Window上的对象和方法不是全部都可以使用。
Worker线程有一些自己的全局属性和方法:
- self.name: Worker的名字,该属性只读,有构造函数指定。
- self.onmessage: 指定message事件的监听函数。
- self.onmessageerror: 指定messageerror事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
- self.close(): 关闭Worker线程。
- self.postMessage(): 向产生这个Worker线程发送消息。
- self.importScripts(): 加载JS脚本。