hello~亲爱的看官老爷们大家好~最近入职新公司后,接手可视化平台的开发工作。经过收集意见后,发现使用者普遍吐槽渲染时太卡。查看接口后发现,后端直接返回查询数据库后的原始结果,需要页端傻乎乎地循环处理各种数据。当数据量大的时候,用户就只能傻看着浏览器卡死。
优化方案相信大家都有想法,在无法改后端数据体的情况下,最低成本的当然是引入 Web Worker
,将处理逻辑丢给它就好。但由于各种原因,建一个 Web Worker
文件再求后端爸爸部署,暂时是不太可行的。那么,脑洞大开想一想,能不能页端动态创建一个呢?
于是,提出了如下问题:
- 是否能动态创建
Web Worker
? - 如能创建的话,希望
API
调用是面向对象风格,而且支持Pormise
调用。 - 为节省资源起见,这个动态创建的
Web Worker
希望是可复用的。
带着这些问题,开始瞎折腾之旅吧!
动态创建 Web Worker
Web Worker
是什么,相信大家都了然于胸,一般的引用方法是请求一个 JS
文件,即:
const worker = new Wokrer(worker's url);
因而,问题可以转化为,我们如何将一个函数,转换成浏览器可识别并能重新解析为该函数的 url 呢?之前看 iframe和HTML5 blob实现JS,CSS,HTML直接当前页预览这篇文章时,接触到 URL.createObjectURL()
,文中对此的描述是:“使用 URL.createObjectURL()
方法将 Blob
对象转换为 URL
对象并赋予我们创建的 iframe
元素的src属性。”那么,是否能将其用于创建 Web Worker
的 url
呢?查阅 MDN
文档后发现了这么一句:
Note: 此特性在 Web Worker 中可用。
喜大普奔,计划通!该 API
接受 File
对象或者 Blob
对象作为参数,此处我们使用 Blob
对象作为参数并指定类型为 text/javascript
,测试 demo
如下:
function demo() {
setTimeout(() => {
postMessage('success!');
}, 1000)
}
const blob = new Blob([demo.toString() + ' demo()'], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.addEventListener('message', function(res) {
console.log(res);
})
打开 Chrome
,在控制台中输入上面的代码,隔一秒之后便能打印出:MessageEvent {isTrusted: true, data: "success!", origin: "", lastEventId: "", source: null, …}
。实验通过,可以确认 Web Worker
能被动态创建。
封装 API
既然能动态创建,作为程序员,肯定是不希望每次都手动创建,因而封装一下以便于使用。由于可能会多处使用,因而封装成一个“类”,有助于代码复用与节省内存。构造函数接受一个函数作为参数即可,这个“类”应有一个 send
方法让我们再想发送数据时调用。那么基础的架子应该是:
class DynamicWorker {
constructor(cb) {
//根据 cb 创建 Web Worker
}
send(data) {
//传入参数后发送数据给 Web Worker,Web Worker处理后返回
}
}
鉴于创造的 Web Worker
不是马上就处理数据,而是在 DynamicWorker
实例调用 send
方法时才开始干活,因而不能像上面的 demo
一样直接写死 postMessage
,而应该多构建一个 onmessage
函数,因而 constructor
函数构建如下:
constructor(cb) {
const _fn = `const _fn = ${cb.toString()};`;
const _handle = ` onmessage = function ({ data }) {
postMessage(_fn(data));
}`;
const blob = new Blob([_fn + _handle], { type: 'text/javascript' });
this.worker = new Worker(URL.createObjectURL(blob));
//释放被引用的 url 对象,经过测试,就算释放了也能重复访问创建的 Web Worker的
URL.revokeObjectURL(blob);
}
稍微需要解释下的是为何需要在 Web Worker
中创造一个 _fn
变量,这是由于生成 Blob
对象时,接受的参数是字符串数组,如果只是 cb.toString()
的话,是拿不到函数名的,那么在 Web Worker
中执行该函数更是无从谈起,因而赋值一个变量,在 message
事件触发后使用 postMessage
返回函数处理后的结果。
之后就是 send
方法了,该方法返回一个 Promise
,当接收到 Web Worker
传回的数据后,改变 Promise
的状态。根据这个思路,有这样的设计:
send(data) {
const worker = this.worker;
let resolve = null;
function _handleResult({ data }) {
resolve(data);
}
worker.addEventListener('message', _handleResult);
worker.postMessage(data);
return new Promise((res) => {
resolve = res;
})
}
根据思路看代码,其实是很好理解的。复制 DynamicWorker
的代码丢去控制台,执行下面的代码:
const test = new DynamicWorker(function(data) {
return data;
})
test.send(123).then(res => console.log(res));
就能愉快地看到浏览器打印出 123
了。
复用与优化
上述的代码基本能达到我们的预期,但存在若干问题,如果短时间内调用多次 send
方法,那么后调用的方法会得出前面前面的结果。这是由于 worker.addEventListener('message', _handleResult)
区分不出每次调用,因而收到消息后一律执行 resolve
,大家应该都知道 Promise
的状态决议后就无法再修改的,因而导致调用错误。此处可以通过添加一个标志位解决。另一个问题是潜藏的,为 worker
添加太多的事件监听器了,其实这大可不必,一个 DynamicWorker
实例一个事件监听器就足够了。结合这两点,修改一下对应的代码,首先是 constructor
:
...
const _handleResult = ({ data: { data, flag } }) => {
const _res = this._map[flag];
if (_res) {
_res(data);
this._map[flag] = null;
}
}
this._map = {};
this.worker.addEventListener('message', _handleResult);
...
为其添上这几行代码即可,通过 _map
缓存不同的标志,而根据不同的标志可以区分出不同的 Promise
,根据标志位对应的值调用即可。同理,send
方法也需要作出修改,为其添加标志的生成,完整的 DynamicWorker
代码如下:
class DynamicWorker {
constructor(cb) {
const _fn = `const _fn = ${cb.toString()};`;
const _handle = ` onmessage = function ({ data: { data, flag } }) {
postMessage({
data: _fn(data),
flag
});
}`;
const _handleResult = ({ data: { data, flag } }) => {
const _res = this._map[flag];
if (_res) {
_res(data);
this._map[flag] = null;
}
}
const blob = new Blob([_fn + _handle], { type: 'text/javascript' });
this.worker = new Worker(URL.createObjectURL(blob));
this._map = {};
this.worker.addEventListener('message', _handleResult);
URL.revokeObjectURL(blob);
}
send(data) {
const worker = this.worker;
const flag = Math.random();
worker.postMessage({
data,
flag,
});
return new Promise((res) => {
this._map[flag] = res;
})
}
}
还是挺简单易懂的对不对?调用 API
是没有变化的,优化的只是内部的逻辑,有兴趣的同学可以复制进去控制台,随意写个需要运算很久的函数丢进去,尝试下浏览器是否不会卡住哦~
小结
这个 DynamicWorker
还只是未成品,缺少错误处理,异步处理等功能。而且应该有同学可能会怀疑这样的东西到底有何用途,兼容性上也有不少问题,当时我写完代码后也有这个疑问(因为下个月就会接入 node.js
,可以自己部署文件)。思考后认为,对比正常使用 Web Worker
,动态创建最大的优点是在于动态,不依赖于服务器部署文件,相对灵活易用。对于某些需要依赖运算大量数据,但又无法控制服务器的场景来说,不失为一个可行的解决思路。
最后,有个问题想请教一下,在需要处理大量数据的场景下,node.js
也不是一个好的选择,只是因为页端性能差与只是内部系统的关系,将处理逻辑上移到 node.js
,那么正确的处理方式或者架构是怎样的呢?还望有相关经验的同学不吝赐教。
感谢各位看官大人看到这里,知易行难,希望本文对你有所帮助~谢谢!