web workers簡介(二)動態建立worker
大家好,今天在這裡簡單介紹一下如何動態的建立內聯的web workers。
動態建立worker
在介紹worker-loader
時,我們已經瞭解到可以通過Blob
和createObjectURL
來建立內聯的web worker,這使得web worker的使用變得更加靈活。
接下來,讓我們一起來對此進行嘗試。新建workify.js
,並且編寫我們的頁面程式碼:
// index.html
<script type="text/javascript" src="./workify.js"></script>
<script type="text/javascript">
function add(a, b) {
return a + b;
}
async function initFunc() {
const workerAdd = workify(add);
console.log('workerAdd', await workerAdd(23, 16));
}
initFunc();
</script>
複製程式碼
我們希望通過workify
方法建立一個內聯的web worker的代理,並且可以用async/await
的形式來呼叫這個方法。
接著我們將編寫我們的workify
方法。首先新增一些工具方法。
// workify.js
(function (g) {
const toString = function toString(t) {
return Function.prototype.toString.call(t);
};
const getId = function getId() {
return (+new Date()).toString(32);
};
...
const workify = function workify(target) {
...
}
g.workify = workify;
})(window);
複製程式碼
接下來,我們將建立內聯程式碼的web worker:
const code = `(${toString(proxy)})(${toString(target)})`;
const blob = new Blob([code]);
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
複製程式碼
這裡我們拼接的程式碼,將會把目標函式作為引數傳給我們的proxy
函式,proxy
函式會負責處理web worker呼叫目標函式並與主執行緒進行通訊(通過呼叫postMessage
和設定onmessage
)。
接下來,在workify
中我們將設定worker
的onmessage
方法。同時我們將為worker
新增一個send
方法,這個方法會使用postMessage
發出訊息,並返回一個Promise
。
最後workify
會返回一個方法,這個方法會通過worker.send
傳送訊息並返回它的Promise
:
worker.onmessage = function (ev) {
//
};
worker.send = function ({ type, data }) {
//
}
const rtn = function rtn(...args) {
return worker.send({
type: 'exec',
data: args,
});
};
return rtn;
複製程式碼
因為我們需要在worker
完成任務時知道需要去resolve
哪個Promise
,因此我們將在postMessage
中傳送一個id
,並由worker
再返回來:
worker._cbs = {};
worker.onmessage = function (ev) {
const { type, id, data } = ev.data;
if (type === 'exec') {
worker._cbs[id](data);
}
};
worker.send = function ({ type, data }) {
return new Promise((res) => {
const id = getId();
worker._cbs[id] = (data) => {
res(data);
};
worker.postMessage({
id,
type,
data,
});
});
}
複製程式碼
之後我們再來實現proxy
方法,既web worker端的邏輯:
const proxy = function proxy(target) {
self.onmessage = function (ev) {
const { type, data, id } = ev.data;
let rtn = null;
if (type === 'exec') {
rtn = target.apply(null, data);
}
self.postMessage({
id,
type,
data: rtn,
});
};
};
複製程式碼
我們使用接收到的引數來呼叫目標函式,並將結果和id
傳送回去。
如果需要通過importScripts
載入程式碼,我們可以在目標函式中直接使用importScripts
,或將需要載入的程式碼陣列作為另一個引數傳入proxy:
const proxy = function proxy(target, scripts) {
if (scripts && scripts.length) importScripts.apply(self, scripts);
...
}
複製程式碼
如上,我們已經可以將函式內聯為web worker。接下來,我還希望能將Class
同樣內聯為web worker。
class Adder {
constructor(initial) {
this.count = initial;
}
add(a) {
this.count += a;
return this.count;
}
}
async function initClass() {
let WAdder = workify(Adder);
let instance = await new WAdder(5);
console.log('apply add', await instance.add(7));
console.log('get count', await instance.count);
}
initClass();
複製程式碼
首先,我們改變rtn
的程式碼,以判斷其是否通過new
呼叫:
const rtn = function rtn(...args) {
if (this instanceof rtn) {
return worker.send({
type: 'create',
data: args,
});
} else {
return worker.send({
type: 'exec',
data: args,
});
}
};
複製程式碼
接下來我們修改work.onmessage
,根據事件型別做出不同處理(在此處不同的僅create
事件):
worker.onmessage = function (ev) {
const { type, id, data } = ev.data;
if (type === 'create') {
worker._cbs[id](_proxy(worker));
} else {
worker._cbs[id](data);
}
};
複製程式碼
我們將先支援以下4類事件:
- exec:呼叫函式
- create:建立例項
- apply:呼叫例項方法
- get:獲取例項屬性
相應的proxy
函式中定義的onmessage
也要修改:
self.onmessage = function (ev) {
const { type, data, id } = ev.data;
let rtn = null;
if (type === 'exec') {
rtn = target.apply(null, data);
} else if (type === 'create') {
instance = new target(...data);
} else if (type === 'get') {
rtn = instance;
for (let p of data) {
rtn = rtn[p];
}
} else if (type === 'apply') {
rtn = instance;
for (let p of data.path) {
rtn = rtn[p];
}
rtn = rtn.apply(instance, data.data);
}
...
};
複製程式碼
對應的邏輯分別為生成示例、獲取屬性與呼叫方法。
在worker.onmessage
中,我們通過_proxy(worker)
來返回一個代理,這是比較tricky的一段程式碼。
我們希望我們返回的代理物件,可以獲得任意獲取屬性、任意呼叫程式碼,並將呼叫web worker相應的行為。
因此這裡我們使用了Proxy
,並且其目標是一個函式,這樣我們就能代理期get
(獲取屬性)和apply
(呼叫)兩種行為。在get
中,我們通過遞迴的使用_proxy
來實現深度代理。我們通過path
來記錄當前路徑,當獲取的屬性為then
時,例如await instance.count
中path
為['count']
,我們將使用worker.send
來獲取相應的屬性並返回其then
;而若當前path
為空,我們可以直接返回null
,表示當前物件非thenable
並中斷Promise
鏈。
const _proxy = function _proxy(worker, path) {
path = path || [];
return new Proxy(function(){}, {
get: (_, prop, receiver) => {
if (prop === 'then') {
if (path.length === 0) return null;
const p = worker.send({
type: 'get',
data: path,
});
return p.then.bind(p);
}
return _proxy(worker, path.concat(prop));
},
apply: (_0, _1, args) => {
return worker.send({
type: 'apply',
data: {
path,
data: args,
},
});
},
});
};
複製程式碼
小結
今天介紹瞭如何通過Blob
來建立內聯的web workers。接下來將要介紹一下如何實現與subworker相似的功能。
程式碼
(function (g) {
const toString = function toString(t) {
return Function.prototype.toString.call(t);
};
const getId = function getId() {
return (+new Date()).toString(32);
};
const proxy = function proxy(target, scripts) {
if (scripts && scripts.length) importScripts.apply(self, scripts);
let instance;
self.onmessage = function (ev) {
const { type, data, id } = ev.data;
let rtn = null;
if (type === 'exec') {
rtn = target.apply(null, data);
} else if (type === 'create') {
instance = new target(...data);
} else if (type === 'get') {
rtn = instance;
for (let p of data) {
rtn = rtn[p];
}
} else if (type === 'apply') {
rtn = instance;
for (let p of data.path) {
rtn = rtn[p];
}
rtn = rtn.apply(instance, data.data);
}
self.postMessage({
id,
type,
data: rtn,
});
};
};
const _proxy = function _proxy(worker, path) {
path = path || [];
return new Proxy(function(){}, {
get: (_, prop, receiver) => {
if (prop === 'then') {
if (path.length === 0) return null;
const p = worker.send({
type: 'get',
data: path,
});
return p.then.bind(p);
}
return _proxy(worker, path.concat(prop));
},
apply: (_0, _1, args) => {
return worker.send({
type: 'apply',
data: {
path,
data: args,
},
});
},
});
};
const workify = function workify(target, scripts) {
const code = `(${toString(proxy)})(${toString(target)}, ${JSON.stringify(scripts)})`;
const blob = new Blob([code]);
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker._cbs = {};
worker.onmessage = function (ev) {
const { type, id, data } = ev.data;
if (type === 'exec') {
worker._cbs[id](data);
} else if (type === 'create') {
worker._cbs[id](_proxy(worker));
} else if (type === 'apply') {
worker._cbs[id](data);
} else if (type === 'get') {
worker._cbs[id](data);
}
};
worker.send = function ({ type, data }) {
return new Promise((res) => {
const id = getId();
worker._cbs[id] = (data) => {
res(data);
};
worker.postMessage({
id,
type,
data,
});
});
}
const rtn = function rtn(...args) {
if (this instanceof rtn) {
return worker.send({
type: 'create',
data: args,
});
} else {
return worker.send({
type: 'exec',
data: args,
});
}
};
return rtn;
};
g.workify = workify;
})(window);
複製程式碼