第77节 Web Workers零点程序员-王唯

JavaScript是单线程的,但Web Workers规范允许Web应用程序可以在独立于主线程的后台线程中,创建子线程,运行一个脚本操作,从而可以在独立的线程中执行费时的处理任务,进而不会阻塞主线程(通常是UI线程),Worker线程计算结束后,再把结果返回给主线程,这样就解决了Javascript运行时会干扰用户界面的问题;

例如:


worker.js:

var num = 0;
setInterval(function(){
postMessage(num++);
},1000);

浏览器实现Web Workers的方式有多种,可以使用线程、后台进程或者运行在其他处理器核心上的进程等等;Web Workers起初是作为HTML5标准的一部分,但后来独立成一个相近的标准;IE9及以下不支持Workers。

应用场景:
Web Workers的实现为Web应用带来了后台计算的能力,它可以将一些耗时的数据处理操作从主线程中剥离,从而极大减轻了因计算量大造成的UI阻塞而出现的界面渲染卡、掉帧的情况,使主线程更加专注于页面渲染和交互,更大程度地利用了终端硬件的性能;

其应用的场景:数学运算、大数据处理、懒加载、文本分析、流媒体数据处理、canvas图形绘制、图像处理等等。

专用Worker和共享Worker:

worker有两大类,一个是专用Worker(dedicated worker),是专门为某个特定的页面服务的;二是共享Worker(shared worker),这种worker可以在浏览器的多个标签中打开的同一个页面间共享。

专用Worker:

只能由生成它的脚本所使用;

一个Worker包含两部分,一部分是Worker对象,该对象用于创建worker线程;第二部分是DedicatedWorkerGlobalScope,这是一个用来表示新创建的Worker内部的全局对象,也就是Worker线程内部使用的上下文对象;

使用Worker(URL[, options])构造函数实例化Worker对象,该对象执行指定的URL脚本;

worker.js线程:

console.log("my is worker");

主线程:

var worker = new Worker("worker.js");
console.log(worker); // Worder

参数URL指定的JavaScript文件,包含了将运行独立于主线程的worker线程中运行的代码;

参数URL指定的worker JS文件必须遵守同源策略,如果该URL是一个无效的URL,或者违反同源策略,将抛出SECURITY_ERR异常;如果此URL的MIME类型不是text/javascript类型,将抛出NetworkError异常;如果URL无法解析,将引发SyntaxError异常;

参数options是可选的配置对象,可用属性如下:

如主线程:

var worker = new Worker("worker.js", {
type:"classic",
credentials:"omit",
name: "myWorker"
});
console.log(worker);

虽然可以在参数中指定options,但该参数的值只能在Worker线程中才能访问;如,Worker线程:

postMessage({
name: self.name,
type: self.type,
credential: self.credential
});

主线程:

worker.onmessage = function(event){
console.log(event.data);
};

worker线程和主线程之间的数据传递,是通过这样的消息机制进行的:双方都使用postMessage()方法发送各自的消息,再使用onmessage事件处理函数来响应消息,且消息被包含在onmessage事件的data属性中;

当页面在Worker对象上调用postMessage()时,数据会以异步方式被传递给worker线程,进而触发worker线程中的message事件;

它们的通信是双向的,反过来也是一样;

使用Worker对象的postMessage(message[, transferList])方法向worker传递消息,参数message为要发送的消息数据,参数transferList是可选的,它是一个Transferable对象的数组,用于转移所有权;如主线程:

worker.postMessage("wangwei");

Worker线程是通过onmessage和onerror事件来接收数据和处理错误的,如:

onmessage = function(event){
console.log(event); // MessageEvent
console.log(event.data);
}
onerror = function(event){
console.log(event);
}

该事件类型是MessageEvent类型;

worker线程向主线程传递消息:

与主线程向worker线程传递消息一样,只不过由worker线程通过postMesage()发送消息,主线程通过onmessage和onerror事件接收鼠数据,如,主线程:

worker.onmessage = function(event){
console.log("Main:" + event.data);
};
worker.onerror = function(event){
console.log(event);
}

worker线程:

postMessage("我是worker线程");

参数message可以是任何能够被序列化的值,也可以是对象,如:

worker.postMessage({
type: "command",
message: "start"
});

该方法一次只能发送一个值或对象,因此,如果想传递多个值,可以使用数组,如,主线程:

worker.postMessage(["wangwei",18]);

worker线程:

onmessage = function (event) {
console.log(event.data);
}

Worker如果不能完成给定的任务时会触发error事件;如:

worker.onerror = function(event){
console.log(event); // // ErrorEvent
console.log("Error: " + event.filename + " (" + event.lineno + "): " + event.message);
};

建议在使用Worker时,始终都要使用onerror事件,因为,不注册onerror事件,Worker会在发生错误的时候,静默失败;

在message事件中,还可以使用postMessage()方法回送数据;如,主线程:

var worker = new Worker("worker.js");
worker.postMessage("Main向Worker发送的消息");
worker.onmessage = function(event){
console.log("Main: " + event.data);
}
worker.onerror = function(event){
console.log(event);
}

worker.js线程:

onmessage = function(event){
console.log("worker: " + event.data);
postMessage("Worker向Main回发的消息");
}

示例:数字排序,主线程:

var data = [18,45,11,98,3,6,768];
worker.postMessage(data);
worker.onmessage = function(event){
console.log(event.data);
};

worker线程:

self.onmessage = function(event){
var data = event.data;
data.sort(function(a,b){
return a - b;
});
self.postMessage(data);
};

示例:计算两个数的乘积:











结果: 0

worker.js:

onmessage = function(e) {
console.log('Worker: 从主线程接收到乘数');
var result = e.data[0] * e.data[1];
if(isNaN(result)) {
postMessage('请输入两个数字');
}else{
var workerResult = '结果: ' + result;
console.log('Worker: 已计算出结果,并回显');
postMessage(workerResult);
}
}

终止Worker:

worker线程一旦新建成功,就会始终运行,不会被主线程上的活动打断;这样有利于随时响应主线程的通信,但是,这也造成了Worker比较耗费资源的问题,所以不应该过度使用worder,而且一旦使用完毕,就应该终止;

在任何时候,只要调用terminate()方法就可以终止Worker的工作;此时,worker线程中的代码会立即停止执行,后续的所有过程都不会再发生,包括error和message事件也不会再被触发,如:

worker.terminate();

主线程:

等待接收消息...



worker线程:

var count = 0;
self.onmessage = function (event) {
self.postMessage('Worker Start: ');
setInterval(function(){
this.postMessage("我是模拟的数据,不要当真" + count++);
},2000);
};

可以根据主线程发来的数据,worker线程可以调用不同的方法,如:主线程:

等待接收消息...



worker线程:

var count = 0;
self.onmessage = function (event) {
var data = event.data;
switch (data.cmd) {
case 'start':
self.postMessage('Worker Start: ' + data.msg);
setInterval(function(){
this.postMessage("我是模拟的数据,不要当真" + count++);
},2000);
break;
case 'stop':
self.postMessage('Worker Stop: ' + data.msg);
self.close();
break;
default:
self.postMessage(data.msg);
};
};

DedicatedWorkerGlobalScope(Worker线程):

关于Worker,最重要的是要知道它所执行的JavaScript代码完全在另一个作用域(上下文)中,即DedicatedWorkerGlobalScope,其代表了专用worker的上下文,与当前主线程页面中的代码不共享作用域,也就是不同于当前的window;

其继承自WorkerGlobalScope类;该类是DedicatedWorkerGlobalScope、SharedWorkerGlobalScope和ServiceWorkerGlobalScope类的父类;

worker线程:

console.log(self); // DedicatedWorkerGlobalScope

在worker内部中,this、self指向DedicatedWorkerGlobalScope这个上下文,在使用时也可以省略,如:

// 使用this
this.onmessage = function(event){
console.log(event);
}
// 使用self
self.onerror = function(event){
console.log(event);
}
// 省略
postMessage("我是worker线程");

为便于处理数据,Web Workers本身是一个最小化的运行环境:

最小化的navigator对象,包括onLine、appName、appVersion、userAgent和platform属性,在Worker线程中被称为WorkerNavigator对象;

只读的location对象,在Worder线程中是WorkerLocation对象;

还可以使用window对象的部分API ,如:setTimout()、setInterval()、clearTimeout()和clearInterval()方法、XMLHttpRequest,也包括WebSockets、IndexedDB等;

显然,Web Workers的运行环境与页面环境相比,功能是相当有限的;并且在Worker中的代码不能访问DOM,也不能通过任何方式影响页面的外观,只能使用少部分的window的功能;

如果想操作DOM,只能间接地实现,即通过postMessage发送消息给主线程,然后在主线程那里执行DOM操作;

主线程操作DOM:主线程:

worker.onmessage = function(event){
var person = event.data;
var p = document.createElement("p");
p.innerHTML = "姓名:" + person.name + "
" + "性别:" + person.sex + "
" + "年龄:" + person.age; document.body.appendChild(p); }

worker线程:

postMessage({
name: "wangwei",
sex: true,
age: 18
});

在worker线程内部,调用close()方法也可以停止工作,就像在主线程中调用terminate()方法一样,Worker停止工作后就不会再有事件触发了,如:

self.close();

示例:计算斐波那契数,fibonacci.js:

var results = [];
function resultReceiver(event){
results.push(parseInt(event.data));
if(results.length == 2)
postMessage(results[0] + results[1]);
}
function errorReceiver(event){
throw event.data;
}
onmessage = function(event){
var n = parseInt(event.data);
if(n == 0 || n == 1){
postMessage(n);
return;
}
for(var i=1; i<=2; i++){
var worker = new Worker("fibonacci.js");
worker.onmessage = resultReceiver;
worker.onerror = errorReceiver;
worker.postMessage(n - i);
}
};

主页面:


生成subworker:

如果需要的话,Worker也可以生成多个Worker,这就是所谓的subworker; subworker解析URI时会相对于父worker的地址而不是主页面的地址;主线程:

var worker = new Worker("worker.js");
worker.onmessage = function(event){
console.log(event.data);
};

worker.js:

var subWorker = new Worker("subworker.js");
subWorker.onmessage = function(event){
event.data.age = 18;
postMessage(event.data);
};

subworker.js:

postMessage({
name: "wangwei",
message: "我是subworker"
})

subworker的error事件名为onmessageerror;

在多核cpu,且要进行并行计算的情况下,使用subworker特别有用;

示例:使用多个subworker并行计算主线程:

var worker = new Worker('worker.js');
worker.onmessage = function (event) {
document.getElementById('result').textContent = event.data;
};

worker线程:

var num_workers = 10;
var items_per_worker = 100;
var result = 0;
var pending_workers = num_workers;
for(var i = 0; i < num_workers; i++) {
var worker = new Worker('core.js');

worker.postMessage(i * items_per_worker);
worker.postMessage((i + 1) * items_per_worker);
worker.onmessage = messageHandler;
}
function messageHandler(event){
result += event.data;
pending_workers -= 1;
if(pending_workers <= 0)
postMessage(result); // 结束
}

core.js:

var start;
onmessage = getStart;
function getStart(event) {
start = event.data;
console.log("start: " + start);
onmessage = getEnd;
}
var end;
function getEnd(event) {
end = event.data;
console.log("end: " + end);
onmessage = null;
work();
}
function work() {
console.log("finish: start=" + start + ", end=" + end);
var result = 0;
for (var i = start; i < end; i++) {
result += 1;
}
postMessage(result);
close();
}

包含其他脚本(引入脚本和库):

可以调用importScripts(urls)方法,这个方法接收一个或多个指向JavaScript文件的URL,如:

importScripts("file1.js", "file2.js", "file3.js");

每个加载过程都是异步进行的,所有脚本加载并执行之后,importScripts()才会返回,但该方法本身是同步的方案;

执行的时候会按照importScirpts()方法里参数的先后顺序执行;

如果载入脚本的时候抛出了网络错误,或者在执行的时候抛出了错误,那么剩下的脚本就不会载入和运行;

而且,这些脚本是在Worker的全局作用域中执行;

其error事件名为onmessageerror

可以利用importScripts()方法,把一个大的worker线程,拆分成若干个部分,每个部分负责自身的逻辑;

通过importScripts()方法载入的脚本自身还可以调用importScripts()方法载入它需要的文件;

但是,要注意的是,importScripts()方法不会试图去跟踪哪些脚本已经载入了,也不会去防止循环依赖的问题;

Worder执行模型:

Worker线程从上到下同步运行它们的代码(以及所有导入的脚本),然后进入一个异步阶段,来对事件以及计时器做出响应;如果Worker注册了onmessage事件,只要该事件触发了,那么它将永远不会退出;但是,如果Worker没有监听任何消息,那么,一直到所有任务相关的回调函数都调用以及再也没有挂起的任务(比如下载和计时器)之后,它就会退出;但如果在执行完任务后,又有新任务了,它还要继续执行,不会退出;

Worker的数据传输:

结构复制算法:Worker内部的运行机制是,先将消息内容串行化,然后把串行化后的字符串发给Worker,后者再将它还原,如此,传送前和传送后的对象,都是独立的,互不影响,如:

主线程:

var person = {
name: "wangwei",
age: 18
}
worker.postMessage(person);

worker线程:

this.onmessage = function(event){
event.data.name = "jingjing";
console.log(event.data);
}

这种方式被称为结构化复制算法;其特点是:

主线程:

var person = {
name: "wangwei",
age: 18,
// 抛出异常,不能被clone
// smoking: function(){console.log("红南京")},
// 抛出异常,不能被clone
// symbol: Symbol("zeronetwork"),
company: {
name: "零点程序员",
address: "北京东城"
}
};
person.sex = true;
Object.defineProperty(person, "salary", {
value: 8000,
writable: false,
configurable: false,
enumerable: true // 如果为false,就不可复制
});

worker.postMessage(person);
worker.onmessage = function (event) {
console.log(event.data);
};

worker线程:

self.onmessage = function(event) {
// 传过来之后,就可以修改了,因为指定的writable配置被丢弃了
event.data.salary = 20000;
console.log(event.data);
};

主线程与worker线程之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer等类型,如:主线程:

var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

worker线程:

self.onmessage = function(event) {
var uInt8Array = event.data;
postMessage('worker.js: ' + uInt8Array.toString());
postMessage('uInt8Array.byteLength: ' + uInt8Array.byteLength);
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = 0;
}
postMessage('update: ' + uInt8Array.toString());
};

传值(拷贝)方式在发送二进制数据,会造成性能问题;

通过转移所有权(可转移对象)来传递数据(可转移也称为可转让):

为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据;这种转移数据的方法,叫做Transferable Objects可转移对象;可转移对象从一个上下文转移到另一个上下文而不会经过任何拷贝操作,这意味着当传递大数据时会获得极大的性能提升;,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担;

使用postMessage(message[, transfer])方法的第二个参数transfer,其是Transferable对象的数组,用于传递所有权,可转移对象可以是ArrayBuffer,MessagePort或ImageBitmap等实例对象,如: 主线程:

var worker = new Worker('worker.js');
var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array .length; ++i) {
uInt8Array[i] = i;
}
console.log(uInt8Array.length);
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
console.log(uInt8Array.length);
worker.onmessage = function (event) {
console.log(event.data);
};

worker线程:

self.onmessage = function(event) {
var buffer = event.data;
console.log(buffer.byteLength);
postMessage(buffer, [buffer]);
console.log(buffer.byteLength);
};

可转移对象的类型:
ArrayBuffer、MessagePort、ReadableStream、WritableStream、TransformStream、AudioData、ImageBitmap、VideoFrame、OffscreenCanvas、RTCDataChannel。

嵌入式(内联) worker:

可以把worker代码嵌入到主文档中,主要是借助

由于嵌入式worker不需要从远程资源载入,所以这使得worker初始化更快;或者不使用

在实例化Worker时,还可以直接把worker字符串作为参数,如:

var worker = new Worker('data:text/javascript;charset=UTF-8,onmessage = function(event){postMessage("worker: 收到的数据 " + event.data);}');
worker.postMessage("零点网络");
worker.onmessage = function(event){
console.log("Main: " + event.data);
};

Worker与Ajax:

表面上看,Ajax和Web Worker 都是异步执行的,但两者还是有很大的区别;Ajax是异步的,但是它的运行还是单线程的;

Worker是多线程的,其子线程与主线程是分开的,因此不管子线程运行是否阻塞,还是主线程阻塞,它们之间不会相互影响;

有些情况下,Worker还需要与服务器端进行交互,比如页面可能需要处理来自数据库中的信息,此时就需要使用Ajax技术与服务器交互;但XMLHttpRequest的responseXML和channel这两个属性的值为null,如:

var worker = new Worker("worker.js");
var url = "example.json";
worker.postMessage(url);
worker.onmessage = function(event){
console.log(event.data);
}

worker.js:

var xhr = new XMLHttpRequest();
xhr.onload = function(event){
self.postMessage(xhr.response);
};
onmessage = function(event){
var url = event.data;
xhr.open("GET", url);
xhr.send(null);
}

完善,主页面:

var worker = new Worker("worker.js");
worker.onmessage = function(event){
console.log(event.data);
};
// var data = {
// method: "GET",
// url: "example.json"
// }
// worker.postMessage(data);
var data = {
method: "POST",
url: "example.php",
params: "name=wangwei&sex=男&age=18"
};
worker.postMessage(data);

worker.js:

var xhr = new XMLHttpRequest();
xhr.onload = function(event){
self.postMessage(xhr.response);
};
onmessage = function(event){
var data = event.data;
xhr.open(data.method, data.url);
var params = data.params || null;
if(params)
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(params);
}

在Worker中发起同步XMLHttpRequest;主线程:


Worker线程:

onmessage = function(e){
var urls = e.data;
var contents = []; // 输出
for(var i=0; i

示例:在嵌入式worker中使用Ajax:

var asyncEval = (function(){
var aListeners = [];
var worker = new Worker('data:text/javascript;charset=UTF-8,onmessage=function(e){postMessage({"id": e.data.id, "evaluated": eval(e.data.code)});}')
worker.onmessage = function(e){
if(aListeners[e.data.id])
aListeners[e.data.id](e.data.evaluated);
delete aListeners[e.data.id];
};
return function(sCode, fListener){
aListeners.push(fListener || null);
worker.postMessage({
"id": aListeners.length - 1,
"code": sCode
});
};
})();
// 应用
asyncEval("3 + 2", function(sMessage){
alert("3 + 2 = " + sMessage);
});
asyncEval('"大师哥"', function(sHTML){
document.body.appendChild(document.createTextNode(sHTML));
});
asyncEval('(function(){var xhr = new XMLHttpRequest();xhr.open("GET", "http://localhost/demo.php", false);xhr.send(null);return xhr.responseText;})()',
function(message){
console.log(message);
});

示例:在一个Worker线程中执行长时间计算;主线程:


线程smearWorker.js:

onmessage = function(e){
postMessage(smear(e.data));
};
function smear(pixels){
var data = pixels.data,
width = pixels.width,
height = pixels.height;
var n = 10,
m = n - 1;
for(var row = 0; row < height; row++){
var i = row*width*4 + 4;
for(var col=1; col

共享Worker(Shared Worker):
一个Shared Worker可以被多个脚本使用,也就是可以被不同的窗口的多个脚本运行,例如window、Iframes或worker等,可以实现多页面之间的数据共享与通信;共享worker 比专用 worker 稍微复杂一点,它的脚本必须通过活动端口进行通讯;共享worker的上下文是SharedWorkerGlobalScope对象,也是继承自WorkerGlobalScope类;

创建SharedWorker:
使用SharedWorker(URL[, name | options]) 构造函数实例化一个SharedWorker 对象,参数URL指定了要执行URL脚本,所执行的脚本必须遵守同源策略;参数name表示worker范围的标识名称,主要用于调试;参数options为可选,与Worker()参数中的options相同;如:

var sharedWorker = new SharedWorker("worker.js");
console.log(sharedWorker); // SharedWorker

port属性:只读,返回一个MessagePort对象,该对象可以用来进行通信,并能对共享worker进行控制;

console.log(sharedWorker.port); // MessagePort

MessagePort类型:MessagePort 是属于Channel Messaging API当中的一个接口;

MessagePort事件:
onmessage:当该端口收到了一条消息时触发;
onmessageerror:当端口收到了一条无法被反序列化的消息时触发;

sharedWorker.port.onmessage = function(event){
console.log(event); // MessageEvent
console.log(event.data);
};
sharedWorker.port.onmessageerror = function(event){
console.log(event);
};

onmessage事件中的事件对象类型是MessageEvent类型;

MessagePort方法:
启动端口时,可以使用postMessage()方法:从当前端口发送一条消息:

sharedWorker.port.postMessage("Main: 发送的数据");
sharedWorker.port.onmessage = function(event){
console.log("Main: " + event.data);
};

start():在传递消息前,端口连接必须被显式打开,即使用start()方法开始发送该端口中的消息;

sharedWorker.port.start();

当使用onmessage注册事件,默认port就已经打开了;如果使用addEventListener注册message事件,就必须使用start()方法打开port;如:

myWorker.port.start();
myWorker.port.addEventListener("message",function(event){
console.log(event.data);
});

因此,推荐使用DOM1级事件注册机制;

在使用start()方法打开端口连接时,如果主页面和SharedWorker需要双向通信,那么它们都需要调用start()方法,如:

// ...
port.start(); // worker线程中的调用, 假设port变量代表一个端口

每个新的SharedWorker连接都触发一个onconnect事件;

在Worker中使用onconnect处理程序连接到相关端口;可以用SharedWorker内部的onconnect 事件的event对象的ports属性中获取到与该SharedWorker相连接的端口;

使用ports[0]获取当前使用的port,即MessagePort对象,如:

onconnect = function(event){
var port = event.ports[0];
port.postMessage("ports" + event.ports); // ports[object MessagePort]
port.postMessage(event.ports.length); // 1
};

发送数据使用port对象的postMessage()方法,如:

onconnect = function(event){
var port = event.ports[0];
port.postMessage("worker start");
};

使用port对象start()方法来启动端口或直接使用onmessage 处理程序处理来自主线程的消息,并可以向主页面传递消息,如:

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

port.addEventListener("message", function(e) {
port.postMessage("接收到数据为:" + e.data);
});
port.start();
}

但还是建议DOM1级事件注册,如:

onconnect = function(event){
var port = event.ports[0];

port.onmessage = function(event){
port.postMessage("warker: 接收到的消息再返回:" + event.data);
}
};

SharedWorkerGlobalScope:

在共享worker内部,全局作用域是SharedWorkerGlobalScope的一个实例,它继承自WorkerGlobalScope ,因此包括它的所有属性和方法;和专用worker一样,共享worker可以通过self访问全局作用域;

共享数据:index.html和index1.html,两个页面结构一样,如:

点了 -

worker线程:

var a = 1; // 全局变量
onconnect = function (e) {
var port = e.ports[0];
port.onmessage = function () {
port.postMessage(a++)
}
}

示例:点赞,主页面:


一共收获了0个赞

sharedworker.js:

var count = 666;
self.onconnect = function(event){
var port = event.ports[0];
port.postMessage(count);
port.onmessage = function(e){
port.postMessage(++count);
}
}

在SharedWorker中,可以根据获取到不同的值,发送不同的数据,如:主页面:

var sharedWorker = new SharedWorker("sharedworker.js");
sharedWorker.port.onmessage = function(event){
console.log("来自worker的消息:" + event.data);
};

sharedWorker.port.postMessage("get"); // A页面用
sharedWorker.port.postMessage("发送新数据给worker"); // B页面用

worker.js:

var data = 'worker中原始数据';
self.onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
if(e.data === "get"){
port.postMessage(data);
}else{
data = e.data;
port.postMessage("worker: " + data);
// 后续处理
}
}
}

Dedicated Worker与Shared Worker的本质区别:

专用Worker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享,所以在Render进程中创建一个新的线程来运行Worker中的JavaScript程序;

SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用,所以浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中使用相同名字的SharedWorker的多个脚本,只存在一个SharedWorker进程,不管它被创建多少次;

共享worker身份由解析脚本URL、worker名和文档源确定;

new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js');

new SharedWorker('./sharedWorker.js');
new SharedWorker('sharedWorker.js');
new SharedWorker('http://localhost/sharedWorker.js');

即使加载的是同一个脚本,但解析出来的URL不一致,也不能共享,如:

new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js?');

因为可选的worker名是共享worker身份的一部分,使用不同的worker名会让浏览器强制创建多个共享worker,尽管它们的源和脚本URL相同;

new SharedWorker('./sharedWorker.js', {name: 'one'});
new SharedWorker('./sharedWorker.js', {name: 'one'});
new SharedWorker('./sharedWorker.js', {name: 'two'});

共享worker可以在标签、窗口、iframe或其他运行在同一个源的worker之间共享; 如:index.html

SharedWorker也可以使用importScripts()方法;但不能使用共享subworker,在某些浏览器中,可以使用专用subworker,例如Firefox可以使用,如:主页面:

var sharedWorker = new SharedWorker('worker.js');
sharedWorker.port.onmessage = function(event){
console.log("Main: " + event.data);
};

worker.js:

importScripts("w1.js","w2.js");
onconnect = function (e) {
var port = e.ports[0];
port.postMessage("返给主页面,我用到了subworker");
var subworker = new Worker("subworker.js");
subworker.onmessage = function(event){
console.log("worker: " + event.data);
port.postMessage("worker转发给main: " + event.data);
};
subworker.postMessage("worker发送给subworker");
}

subworker.js:

onmessage = function(event){
console.log("subworker: " + event.data);
postMessage("subworker发送给worker");
};

在一个脚本中,可以同时使用专用Worker和共享Worker;如:主页面index.html:

var worker = new Worker('worker.js');
var sharedWorker = new SharedWorker("sharedworker.js");
worker.onmessage = function(event){
console.log(event.data);
// 再扔给sharedWorker
sharedWorker.port.postMessage(event.data);
sharedWorker.port.onmessage = function(e){
console.log(e.data);
};
};
// 给专用worker一个对象
worker.postMessage({name:"wangwei",age:18});

worker线程:

onmessage = function(event){
var person = event.data;
// 进行必要的处理,如:
person.sex = true;
person.company = {
name: "零点程序员",
address: "北京东城"
}
// 加工一下,再送回去
postMessage(person);
};

sharedworker.js:

var obj;
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
if(!obj)
obj = e.data;
port.postMessage(obj);
}
}

close()方法:断开端口连接,它将不再是激活状态;如:

// 主页面:
worker.port.close() // 仅仅关闭连接
// SharedWorker内部:
port.close() // 仅仅关闭连接

不能人为终止共享SharedWorker,在MessagPort对象上调用close()也不会导致共享worker的终止,它仅仅是断开连接,并不会销毁SharedWorker,即使只有一个页面和共享worker连接,其还会持续运行;

另外SharedWorker没有类似terminate()的方法;其运行的状态与相应的MessagePort或MessageChannel的状态无关;一旦建立并连接SharedWorker,连接的管理就由浏览器负责,并且所建立的连接将持续在页面的生命周期中持续,只有全部页面销毁,没有连接的时候,且该SharedWorker内部没有运行中的任务,浏览器才终止该SharedWorker;

Worker(专用和共享)内部中的事件,主线程是没法监听到的,反之亦然;Worker中的异常,主线程也是无法感知的,反之亦然;二者唯一的交互方式就是postMessage和监听message事件;如:主页面:



sharedworker:

var connectList = [];
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
port.postMessage("收到消息: " + e.data);
if(connectList.indexOf(port) === -1)
connectList.push(port);

connectList.forEach(function(v,i){
v.postMessage("现在有连接: " + connectList.length);
})
}
}

清除Shared Worker:
在页面关闭后,workerPool中的port并不会自动清除,会造成内存的占用;通过监听页面关闭或页面生命周期的window.onbeforeunload,通知SharedWorker关闭此连接;如:主页面:

var btnClose = document.getElementById("btnClose");
btnClose.onclick = function(){
sharedWorker.port.postMessage({action: "close"});
sharedWorker.port.close();
console.log("sharedWorker已关闭");
}

sharedworker.js

var connectList = [];
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
if(connectList.indexOf(port) === -1)
connectList.push(port);
if(e.data.action == "close"){
port.close();
connectList = connectList.filter(function(item){
return item != port;
})
}
connectList.forEach(function(v,i){
v.postMessage("现在有连接: " + connectList.length);
});
}
}

当关闭窗口时,包括刷新重新载入,也需要通知SharedWorker,如:主页面添加:

window.onbeforeunload = function(){
sharedWorker.port.postMessage({
action: "close"
})
}

错误的处理:
SharedWorker内部的错误,无论是其内部还是主页面都无法获取,如:主页面:

var sharedWorker = new SharedWorker("sharedworker.js");
console.log(sharedWorker); 
console.log(sharedWorker.port);
sharedWorker.port.postMessage("随便发点啥");
sharedWorker.port.onmessage = function(event){
console.log(event.data);
};
sharedWorker.onerror = function(event){
console.log(event);
};
sharedWorker.port.onerror = function(event){
console.log(event);
};
sharedWorker.port.onmessageerror = function(event){
console.log(event);
};

sharedworker.js:

onconnect = function(event){
var port = event.ports[0];
console.log(port);
port.onmessage = function(e){
throw new Error("有错了");
};
port.onmessageerror = function(event){
console.log(event);
}
}

sharedWorker.onerror,在共享工作进程中发生错误时触发;
port.onmessageerror事件,当MessagePort对象接收到无法反序列化的消息时触发;

示例:计算两个数的积

var first = document.querySelector('#number1');
var second = document.querySelector('#number2');
var result = document.querySelector('.result');
if(!!window.SharedWorker) {
var myWorker = new SharedWorker("sharedworker.js");
first.onchange = function() {
myWorker.port.postMessage([first.value, second.value]);
console.log('向worker中发送第一个乘数');
}
second.onchange = function() {
myWorker.port.postMessage([first.value, second.value]);
console.log('向worker中发送第二个乘数');
}
myWorker.port.onmessage = function(event) {
result.textContent = event.data;
console.log('从worker接收到计算结果:' + event.data);
}
}else{
result.textContent = '浏览器不支持 web workers';
}

sharedworker2.html:






上一次结果: -

sharedworker.js: var result = 0; onconnect = function(event) { var port = event.ports[0]; port.postMessage(result); port.onmessage = function(e) { result = e.data[0] * e.data[1]; port.postMessage(result); } }

在SharedWorker中,利用ports属性向所有连接页面分发消息,如:


worker2.html

sharedworker.js:

var connectList = [];
var textList = [];
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
if(connectList.indexOf(port) === -1)
connectList.push(port);

switch(e.data.status){
case 0:
postMsg(function(item){
if(item != port)
item.postMessage('有新用户加入');
else
item.postMessage("我是新用户");
});
break;
default:
textList.push(e.data.value);
postMsg(textList);
break;
}
}
}
// 分发消息
function postMsg(callback){
var callback = (typeof callback === "function") ? callback : function(item){
item.postMessage(callback);
}
connectList.forEach(callback);
}

示例:页面之间登录状态通知:主页面:



login info...

login.html登录页:



sharedworker.js:

var isLogin = false;
var connectList = [];
var tipsTextArray = [
"You signed in another tab or window. Reload to refresh your session.",
"You signed out in another tab or window. Reload to refresh your session."
];
onconnect = function(event){
var port = event.ports[0];
if(connectList.indexOf(port) === -1)
connectList.push(port);

port.onmessage = function(e){
isLogin = e.data.isLogin;
tipsText = isLogin ? tipsTextArray[0] : tipsTextArray[1];
connectList.forEach(function(item){
item.postMessage({isLogin: isLogin, tipsText: tipsText});
})
};
}

页面更新:2024-03-27

标签:线程   主线   零点   端口   程序员   脚本   对象   消息   事件   页面   方法   数据

1 2 3 4 5

上滑加载更多 ↓
Top