前言
JavaScript语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没有做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核CPU的出现,单线程带来了很大的不便,无法充分发挥计算机的能力。
Web Worker的作用,就是为JavaScript创造多线程环境,允许主线程创建Worker线程,将一些任务分配给后者运行。在主线程运行的同时,Worker线程在后台运行,两者互不干扰。等到Worker线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集或高延迟的任务的任务,被Worker线程负担了,主线程(通常负责UI交互)就会很流畅,不会被阻塞或拖慢。
**Worker线程一旦新建成功,就会始终运行,不会被主线程上的活动**(比如用户点击按钮,提交表单)打断。这样有利于响应主线程的通信。但是,这也造成了Worker比较耗费资源,不应该过度使用,而且**一旦使用完毕,就应该关闭**。
Web Worker有以下几个使用注意点:
(1)同源策略
分配给 Worker 线程运行的脚本文件,必须与页面文档同源。
(2)DOM限制
Worker线程所在的全局对象,与主线程不一样,无法读取主线程所在的网页的DOM对象,也无法使用**document、window、parent**这些对象。但是,Worker线程可以使用**navigator**对象和**location**对象
(3)通信联系
Worker线程和主线程不在同一个上下文环境,它们不能直接通信,必须同个消息完成。
(4)脚本限制
Worker线程不能执行**alert()**方法和**confirm()**方法,但可以使用XMLHttpRequest 对象发出Ajax请求。
(5)文件限制
**Worker**线程无法读取本地文件,即不能打开本地的文件系统(**file://**),它所加载的脚本,必须来自网络。
基础用法
主线程
主线程采用new命令,调用**Worker()**构造函数,新建一个Worker线程
const worker = new Worker('work.js')
Worker()构造函数的参数是一个脚本文件,该文件就是Worker线程所要执行的任务。由于Worker不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker就会默默地失败。
然后,主线程调用worker.postMesage()方法,向Worker发消息
work.postMessage('我是主线程');
work.postMessage({method:'echo',args:['work']});
worker.postMesage()方法的参数,就是主线程传给Worker的数据。它可以是各种数据类型,包括二进制数据。
接着,主线程通过worker.onmessage指定监听函数,接受子线程发回来的消息。
worker.onmessage = function (event){
console.log('Received Message'+event.data);
doSomething();
}
function doSomething() {
// 执行任务
worker.postMessage('work done!')
}
上面代码中,事件对象的data属性可以获取Worker发来的数据
Worker完成任务以后,主线程就可以把它关掉
worker.terminate();
Worker线程
Worker线程内部需要一个监听函数,监听message事件
// 写法一
self.addEventListener('message', function(e) {
this.postMessage('You said:'+e.data);
},false);
// 写法二
addEventListener('message', function(e) {
postMessage('You said:'+e.data);
},false)
除了使用self.addEventListener()指定监听函数,也可以使用self.onmessage指定。监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。self.postMessage()方法向主线程发送消息。
根据主线程发来的数据,Worker线程可以调用不同的方法,下面是一个例子:
self.addEventListener('message', function (e) {
const data = e.data
switch (data.cmd) {
case 'start':
self.postMessage('WORKER STARTED: ' + data.msg);
stop;
case 'start':
self.postMessage('WORKER StOPED: ' + data.msg);
break;
case 'start':
self.postMessage('WORKER StOPED: ' + data.msg);
self.close(); // Terminates the worker.
break;
default:
self.postMessage('Unknown command: ' + data.msg);
}
},false)
上面代码中,**
self.close()
**用于在 Worker 内部关闭自身。
Worker加在脚本
Worker内部如果要加在其他脚本,有一个专门的方法importScripts()
importScript('script1.js');
该方法可以同时加在多个脚本
importScripts('script1.js', 'script2.js');
错误处理
主线程可以监听Worker是否发生错误,如果发生错误,Worker会触发主线程的error事件
worker.onerror = function (event) {
console.log([
'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join(''))
}
// 或者
worker.addEventListener('error', function (event) {
// ...
});
Worker 内部也可以监听
error
事件。
关闭Worker
使用完毕,为了节省系统资源,必须关闭 Worker。
// 主线程
worker.terminate();
// Worker 线程
self.close();
数据通信
前面说过,主线程与Worker之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信时拷贝关系,即是传值而不是传址,Worker对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是先将通信内容串行化,然后把串行化的字符串发给Worker,后者再将它还原,后者再将它还原。
主线程与Worker之间也可以交换二进制数据,比如File、Blob、ArrayBuffer等类型,也可以在线程之间发送,下面是一个例子:
// 主线程
const uInt8Array = new Uint8Array(new ArrayBuffer(10));
for(const i = 0;i<uInt8Array.length;i++){
uInt8Array[i] = i * 2; //[0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);
// Worker线程
self.onmessage = function (e){
const 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]);
// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
同页面的Web Worker
通常情况下,Worker载入的是一个单独的JavaScript脚本文件,但是也可以载入与主线程在同一个网页的代码。
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function () {
postMessage('some message');
}, false);
</script>
</body>
</html>
上面是一段嵌入网页的脚本,注意必须指定<script>标签type属性是一个浏览器不认识的值,上例是
app/worker。
然后读取这一段嵌入页面的脚本,用Worker来处理。
const blob = new Bolb([document.querySelect('#worker').textContent]);
const url = window.URL.createObjectURL(blob);
const worker = new Worker(url)
worker.onmessage = function (e) {
// e.data === 'some message'
}
上面代码中,先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成URL,再让Worker加载这个URL。这样就做到了,主线程和Worker的代码都在一个网页上。
实例1:Worker线程完成轮询
有时,浏览器需要沦胥服务器状态,以便第一时间得知状态改变。这个工作可以放在Worker里面。
function createWorker(f) {
const blob = new Bolo(['(' + f.toString() +')()']);
const url = window.URL.createObjectURL(blob);
const worker = new Worker(url);
return worker;
}
const pollingWorker = createWorker(function (e) {
let cache;
function compare(new,old) {...};
setInterval(function() {
fecth('/my-api-endpoint').then(function (res) {
const data = res.json();
if(!compare(data,cache)){
cache = data;
self.postMessage(data);
}
})
},1000)
});
pollingWorker.onmessage = function () {
//render data
}
pollingWorker.postMessage('init');
上面代码中,Worker每秒钟轮询一次数据,然后跟缓存做比较。如果不一致,就说明服务端有了新的变化,因此就要通知主线程。
实例2:Worker新建Worker
Worker线程内部还能再新建Worker线程(目前只有Firefox浏览器支持)。下面的例子将是一个计算密集的任务,分配10个Worker。
主线程代码如下:
const worker = new Worker('worker.js');
worker.onmessage = function (event) {
document.getElementById('result').textContent = event.data;
}
Worker线程代码如下:
// worker.js
// settings
const num_workers = 10;
const items_per_worker = 100000;
// start the workers
let result = 0;
let pending_workers = nun_workers;
for(let i = 0; i < num_workers; i++){
cosnt worker = new Worker('core.js');
worker.postMessage(i*items_per_worker);
worker.postMessage((i+1)* items_per_worker);
worker.onmessage = storeResult;
}
// handle the results
function storeResult(event) {
result += event.data;
pedding_worker -= 1;
if (pedding_worker <= 0)
postMessage(result);// finished!
}
上面代码中,Worker线程内部新建了10个Worker线程,并且依次访问10个Worker发送消息,告知了计算的起点和终点。计算任务脚本的代码如下:
// core.js
let start;
onmessage = getStart;
function getStart(event) {
start = event.data
onmessage = getEnd;
}
let end;
function getEnd(event) {
end = event.data;
onmessage = null;
work();
}
function work(
let result = 0;
for(let i = start; i < end; i += 1) {
// perform some complex calculation here
result += 1;
}
postMessage(result);
close();
)
实例2实现的即使,在Worker中开了10个Worker线程去计算,给每个线程传入的开始值与结束值:[0,1000000],[1000000,2000000],[2000000,3000000]...,每个worker线程计算完毕时都会通知主线程进行页面显示。但是在storeResult方法中进行了判断,只有pending_workers <= 0即计算完毕之后才会通知主线程进行显示。
API
主线程
浏览器原生提供Worker()构造函数,用来供主线程生产Worker线程。
const myWorker = new Worker(jsUrl, options);
Worker()构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵循同源政策),该参数是必须的,且只能加载JS脚本,否则会报错。第二个参数是配置对象,该对象可选。它的作用就是指定Worker的名称,用来区分多个Worker线程。
// 主线程
const 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.importScript():加载JS脚本
版权归原作者 学学学无止境 所有, 如有侵权,请联系我们删除。