# WebWorker 的使用
# 有什么用
JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
Web Worker 有几个注意点,
同源限制
分配给Worker线程运行的脚本文件, 必须与主线程的脚本文件同源
DOM限制
Weoker线程所在的全局对象,无法读取主线程所在网页的DOM对象, 无法使用
docuemnt,window,parent这些变量, 但是可以使用navigator和location对象通信联系
Worker线程和主线程不在同一个上下文环境, 他们不能直接通信, 必须通过消息完成
脚本限制
worker线程不能执行
alert,confirm等方法, 但是可以使用XMLHttpRequest对象发出Ajax请求worker线程无法读取本地文件, 就是不能打开本机的文件系统(
file://), 加载的脚本必须来自网络
示例代码: WebWorker Demo (opens new window)
# 基本用法
可以先查看示例代码, 再查看。 这个示例代码要run的话, 简单点可以借助http-server (opens new window)即可。
主线程采用new命令, 调用Worker()构造函数, 新建一个Worker线程
if (window.Worker) {
let worker = new Worker('worker.js');
}
参数是一个脚本文件, 该文件就是Worker线程所要执行的任务。这个脚本文件必须来自网络, 如果下载失败 ,worker也会初始化失败。
主线程调用 worker.postMessage()方法, 给Woker发送消息。
worker.postMessage('hello, world');
worker.postMessage({name: 'obj'});
postMessage的参数就是发给worker的数据, 它可以是各种类型的数据, 甚至是二进制文件。
主线程通过 message事件监听worker线程的数据回调
// 等同于 worker.onmessage 接收来自worker线程的消息
worker.addEventListener('message', event => {
const { data } = event;
console.log(`%cdata from worker: ${data}`, "color: red;");
result_input.value = data
});
时间对象中的 data属性可以获取来自Worker线程的数据。
worker完成后, 主线程就可以关闭
function terminateWorker() {
if (worker) {
worker.terminate();
worker = null;
}
}
错误处理
主线程可以监听Worker是否发生错误, 如果发生错误, 可以使用主线程的 error 事件捕获。
// worker.onerror 或者下面的语法
worker.addEventListener('error', e => {
result_input.value = ` ERROR: Line , ${e.lineno}, in , ${e.filename}, : , ${e.message}`;
});
worker 内部也可以监听 error 事件
# worker线程
worker线程内部也需要有一个监听函数, 监听message事件
self.addEventListener('message', onMessageHandler);
function onMessageHandler(event) {
console.log(event);
// 接收来自主线程的消息
const { data } = event;
console.log('%cdata from main thread: ' + data, 'color: green;');
importScripts('./script.js')
// 对数据操作, 这里一般是一些比较耗时的操作,比如各种费时的递归和排序等等
const result = data.split('').reverse().join('');
// 传递数据操作结果给主线程
self.postMessage(result);
}
// 下面的方式是等价的
// this.addEventListener('message', onMessageHandler);
// onmessage = onMessageHandler
这里面的self和this 打印出来是DedicatedWorkerGlobalScope, 可以看做是这个worker对应的全局上下文。
我们也可以调用 self.close()在组件内部关闭自身。
# 加载外部脚本
worker内部如果需要加载其他的脚本,有一个专门的全局方法 importScripts可以加载其他的脚本
importScripts('./script.js')
// 支持同时加载多个脚本
importScripts('./script1.js', 'script2.js')
# 关闭worker
为了节省系统资源, 必须关闭Worker
// 主线程
worker.terminate();
// Worker 线程
self.close();
# 数据通信
主线程与Worker之间的通信内筒, 可以是文本, 也可以是对象。但是这种通信是值的拷贝, 不是引用的传递。worker对通信内容的修改, 不会影响到主线程。内部机制应该就是消息内容串行化之后发送给worker, worker再还原数据。
主线程与Worker中间也可以交换一些二进制数据, 比如Blob, File, ArrayBuffer等
const uIntArray = new Uint8Array(2);
uIntArray[0] = 100;
uIntArray[1] = 200;
worker.postMessage(uIntArray);
// Worker 线程
self.onmessage = function (e) {
var uInt8Array = e.data;
postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};
但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects (opens new window)。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。 下面的写法就是直接转义数据的控制权
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
# 同页面的WebWorker
一般的Web Worker载入的是一个单独的js脚本, 但是也可以载入与主线程在同一个网页的代码。
<script id="worker" type="text/worker">
self.addEventListener('message', event => {
self.postMessage(event.data + '----- from worker')
})
</script>
<script>
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
worker.onmessage = function (e) {
// e.data === 'some message'
console.log(e.data);
};
worker.postMessage('hello');
</script>
# 示例
利用WebWorker来封装轮询, 只有当返回的数据变化时 触发 消息传递。 polling demo (opens new window)
function createWorker(f) {
var blob = new Blob(['(' + f.toString() + ')()']);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
window.URL.revokeObjectURL(url);
return worker;
}
var pollingWorker = createWorker(function (e) {
// 模拟获取数据
function fetchData() {
return Promise.resolve(Math.random() > 0.5 ? 'A' : 'B');
}
var cache;
function compare(newV, oldV) {
if (newV !== oldV) {
return false;
}
return true;
}
self.addEventListener('message', ({ data }) => {
if (data === 'init') {
init();
}
});
function init() {
setInterval(function () {
fetchData().then(function (res) {
var data = res;
console.log(data);
if (!compare(data, cache)) {
cache = data;
self.postMessage(data);
}
});
}, 1000);
}
});
pollingWorker.onmessage = function ({ data }) {
// render data
const p = document.createElement('p');
p.textContent = `changed - ${data}`;
document.body.appendChild(p);
};
pollingWorker.postMessage('init');
# 总结一下
# 主线程
const myWorker = new Worker(jsUrl, options);
Worker()构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则会报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。
// 主线程
var myWorker = new Worker('worker.js', { name : 'myWorker' });
// Worker 线程
self.name // myWorker
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 脚本。