web workers簡介(三)subworker
大家好,今天在這裡簡單介紹一下如何實現與subworker相似的功能。
subworker
在web workers的標準中,還有一個概念叫subworker,這使得你可以在web workers中建立web workers。但事實上如果你在例如chrome
或safari
瀏覽器中這麼做,會得到類似Worker is not defined
這樣的報錯資訊,即無法在web worker內建立Worker
。即使這是一個自2010年即被報告的bug,但仍始終未被修復,因此如果你需要使用subworker,只能藉助其他手段來實現類似的效果。
這裡我們需要將web workers的建立(以及web workers之間的通訊)通過主執行緒進行代理。
假設我們建立一個web worker(稱為parent
),併為p
建立一個subworkerchild
,加上主執行緒main
,如果我們需要實現從主執行緒到worker再到subworker並返回的通訊歷程,即:
- m2p
- p2c
- c2p
- p2m
其中比較需要關注的是2、3即parent和child之間的通訊,這裡實際都是傳送資訊給main之後讓main來轉發資訊的。
首先,我們判斷當前程式碼在worker還是主執行緒中執行。因為web worker的種種限制,判斷方式有多種,例如嘗試呼叫document
,或是嘗試呼叫Worker
:
(function () {
let inWorker = false;
try {
document;
} catch (_) {
inWorker = true;
}
const getId = function getId() {
return (+new Date()).toString(32);
};
if (inWorker) {
...
} else {
...
}
}
複製程式碼
主執行緒中的程式碼如下:
// main.js
const parent = new SubWorker('parent.js');
parent.postMessage('start');
parent.onmessage = (ev) => {
if (ev.data === 'success'){
console.log('p 2 m');
}
};
複製程式碼
在主執行緒的環境下,我們的SubWorker
只是對真正的worker做一層簡單的代理,例如postMessage
和terminate
都是直接呼叫真正的worker來執行操作。同時,我們把所有worker都通過id
儲存下引用:
const workers = {};
class SubWorker {
constructor (f, id, parentId) {
this.id = id || getId();
this.worker = new Worker(f);
this.worker.onmessage = this.handleMessage.bind(this);
this.parentId = parentId;
workers[this.id] = this;
}
handleMessage(ev) {
...
}
postMessage(data) {
this.worker.postMessage(data);
}
terminate() {
this.worker.terminate();
}
}
self.SubWorker = SubWorker;
複製程式碼
這裡m2p
的訊息傳送已經實現了。
在建立worker時我們指定了onmessage。如果我們收到的是帶_subWorker
標誌的訊息,則代表我們需要處理worker內為child worker代理的事件,包括新建worker、傳送訊息和終止worker等等。在新建child worker時,parent會指定id併傳送過來,這樣在main和parent中就是用同一個id標記實際的worker和它的代理,這樣我們就能讓main為parent代理child的事件傳送(data.type === 'msg'
)。同時,接收到指令的parent worker和由此建立的child worker的父子關係也被需要記錄下來:
handleMessage(ev) {
const data = ev.data;
if (!data._subWorker) {
...
}
if (data.type === 'create') {
const subWorker = new SubWorker(data.file, data.id, this.id);
} else if (data.type === 'msg') {
workers[data.id].postMessage(data.data);
} else if (data.type === 'terminate') {
workers[data.id].terminate();
}
}
複製程式碼
接下來我們再來看一下在worker中的SubWorker
,p2m
訊息使用的是原生的postMessage
來傳送的。parent worker接受訊息使用了onMessage
而避開了原生了onmessage
,這裡的原因之後會解釋的:
// parent.js
importScripts('./subworkers.js');
const subWorker = new SubWorker('child.js');
subWorker.onmessage = (ev) => {
if (ev.data === 'pong') {
console.log('c 2 p');
postMessage('success');
}
};
onMessage = (ev) => {
if (ev.data === 'start'){
console.log('m 2 p');
subWorker.postMessage('ping');
}
};
複製程式碼
在worker中的SubWorker
只需要實現一些簡單的代理,傳送帶有_subWorker
標誌位的訊息給主執行緒:
if (!self.SubWorker) {
const workers = {};
class SubWorker {
constructor (f) {
this.id = getId();
workers[this.id] = this;
self.postMessage({
_subWorker: true,
type: 'create',
id: this.id,
file: f,
});
}
postMessage(data) {
self.postMessage({
_subWorker: true,
type: 'msg',
id: this.id,
data: data,
});
}
terminate() {
self.postMessage({
_subWorker: true,
type: 'terminate',
id: this.id,
});
}
}
self.SubWorker = SubWorker;
}
複製程式碼
parent中SubWorker
的postMessage
發出的訊息被主程式轉發給child worker,child worker用原生的onmessage
和postMessage
來收發訊息,這樣p2c
的訊息傳送也完成了,而c2p
的訊息傳送需要進一步的處理。
c2p
的訊息會在主執行緒中接收到,但這條訊息是傳送給parent而非main的,因此這裡需要轉發一下。當main
中的SubWorker
收到訊息時,如果當前的SubWorker
沒有父級,那訊息就是發給自己的,否則,實際的訊息接受人應該是自己的父級worker,因此需要轉發一下訊息。於是我們需要修改主執行緒下SubWorker
的handleMessage
方法:
handleMessage(ev) {
const data = ev.data;
if (!data._subWorker) {
if (this.parentId) {
workers[this.parentId].postMessage({
_subWorker: true,
type: 'msg',
id: this.id,
data: data,
});
} else {
this.onmessage(ev);
}
return;
}
...
}
複製程式碼
最後的部分是,處理m2p
和c2p
兩種訊息的接收。這兩個訊息都是在parent中接收的,一個是self.onMessage
,一個是subWorker.onmessage
。但接收訊息的渠道只有self.onmessage
(或者通過self.addEventListener('message', cb)
),因此需要設定self.onmessage
,並將訊息交給自己或subWorker
處理:
if (inWorker) {
if (!self.SubWorker) {
const workers = {};
self.onmessage = function onmessage(ev) {
const data = ev.data;
if (!data._subWorker) {
self.onMessage(ev);
return;
}
workers[data.id].onmessage(new MessageEvent('worker', {
data: data.data,
}));
};
...
}
}
複製程式碼
如此,整個訊息的接收、傳送流程就走通了。
小結
自己動手實現一下SubWorker
,對於實踐和鞏固代理模式的知識會是非常好的場景。
總結
web workers是一個非常酷的東西,包括從未被實現的subworker、因為安全漏洞被關閉的SharedArrayBuffer等等特性。相比它的好哥們兒Service Workers的C位出道,隨著wasm越來越完善,也許web workers還沒火就要過氣了 (:
程式碼
(function () {
let inWorker = false;
try {
document;
} catch (_) {
inWorker = true;
}
const getId = function getId() {
return (+new Date()).toString(32);
};
if (inWorker) {
if (!self.SubWorker) {
const workers = {};
self.onmessage = function onmessage(ev) {
const data = ev.data;
if (!data._subWorker) {
self.onMessage(ev);
return;
}
workers[data.id].onmessage(new MessageEvent('worker', {
data: data.data,
}));
};
class SubWorker {
constructor (f) {
this.id = getId();
workers[this.id] = this;
self.postMessage({
_subWorker: true,
type: 'create',
id: this.id,
file: f,
});
}
postMessage(data) {
self.postMessage({
_subWorker: true,
type: 'msg',
id: this.id,
data: data,
});
}
terminate() {
self.postMessage({
_subWorker: true,
type: 'terminate',
id: this.id,
});
}
}
self.SubWorker = SubWorker;
}
} else {
const workers = {};
class SubWorker {
constructor (f, id, parentId) {
this.id = id || getId();
this.worker = new Worker(f);
this.worker.onmessage = this.handleMessage.bind(this);
this.parentId = parentId;
workers[this.id] = this;
}
handleMessage(ev) {
const data = ev.data;
if (!data._subWorker) {
if (this.parentId) {
workers[this.parentId].postMessage({
_subWorker: true,
type: 'msg',
id: this.id,
data: data,
});
} else {
this.onmessage(ev);
}
return;
}
if (data.type === 'create') {
const subWorker = new SubWorker(data.file, data.id, this.id);
} else if (data.type === 'msg') {
workers[data.id].postMessage(data.data);
} else if (data.type === 'terminate') {
workers[data.id].terminate();
}
}
postMessage(data) {
this.worker.postMessage(data);
}
terminate() {
this.worker.terminate();
}
}
self.SubWorker = SubWorker;
}
})();
複製程式碼