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);