Web Worker使用
最近对Web Worker进行了系统学习,主要看了阮大的教程和MDN。
详细信息不做介绍,worker的作用是为js提供多线程能力,但是比较耗费资源,所以应当用完即销毁。
基本使用
主要是主线程和worker线程的通信
主线程
var worker = new Worker('work1.js');
worker.postMessage('Hello World');
worker.postMessage({ method: 'echo', args: ['Work'] });
worker.onmessage = function (event) {
console.log('Received message ' + event.data);
doSomething();
};
function doSomething() {
worker.terminate();
}
work1.js
self.addEventListener(
'message',
function (e) {
let msg = typeof e.data === 'string' ? e.data : e.data.method + '--' + e.data.args;
self.postMessage('You said: ' + msg);
},
false
);
self.close();
主线程通知worker:worker.postMessage
worker接收消息:self.addEventListener('message', cb)
消息体为回调参数的data值
worker通知主线程:self.postMessage
主线程接收消息:worker.onmessage
消息体同上
所有通信机制基本都是一样的,本质上都是EventEmitter。
PS:如果使用vscode的插件打开index.html(主线程)会报错,因为worker有同源限制,需要用http-server或者别的方式启个本地服务,别的方式路径要改一下。
同页面的Worker,使用BlobUrl作为Worker构造入参
同页面可能不太重要,现在都是SPA,使用BlobUrl作为构造体还是有用的
<script id="worker" type="app/worker">
addEventListener('message', function () {
postMessage('some message');
}, false);
</script>
<script>
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
worker.onmessage = function (e) {
console.log(e.data);
};
worker.postMessage('conn');
</script>
上面的是worker,下面的是主线程。
worker的type不能被浏览器识别,可以当作只需要函数的字符串格式。
主线程的最后一行在阮大的博文中没有,看了半天没log,才发现是主线程没发通知。
worker轮询
本身只是教程中的一个例子,不过从中学到了一些别的东西
原例子
function createWorker(f) {
var blob = new Blob(['(' + f.toString() +')()']);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
return worker;
}
var pollingWorker = createWorker(function (e) {
var cache;
function compare(new, old) { ... };
setInterval(function () {
fetch('/my-api-endpoint').then(function (res) {
var data = res.json();
if (!compare(data, cache)) {
cache = data;
self.postMessage(data);
}
})
}, 1000)
});
pollingWorker.onmessage = function () {
// render data
}
pollingWorker.postMessage('init');
一开始没看明白worker和主线程在哪,总想着要用js文件初始化Worker。其实这段代码比较像上面的例子,拿到createWorker的函数体转换为BlobUrl构造Worker。
另一个问题是fetch,这个不太熟,简单补下:
fetch(url, option?).then(res => res.json() || res.text()).then(data => ...)
上例这种不加http前缀的,baseUrl默认是Location.origin
返回结果要用json或text方法转换,再在下个then中使用。
为了fetch接口,首先要启个本地服务。起初启了个express服务,route代码如下
app.get('/rolling', (req, res) => {
res.send('rolling...')
});
暂时不考虑轮询一段时间后改变返回值,在请求时发生跨域问题。
之前一直是客户端,基本设置个header就可以了,这回也去找fetch的option配置,MDN说是里面有个mode,设置为cors即可跨域,设置完了并没有生效(默认为cors)。然后就进行了一系列的试错。
- 用http-server启动主线程,端口和express服务端口一致,想着这样就不跨域了。结果证明我想多了,启同端口,后面启的会覆盖前面的,所以fetch并没有通,然后当我停掉主线程服务后,fetch就通了(此时是express服务生效),误打误撞,倒也能验证worker的功能。
- 将location.origin传进worker中,避免将BlobUrl作为baseUrl。此法行不通,一是这种行为跟fetch(baseUrl+'/rolling')没差,二是不解决跨域问题
- 最后是反应过来,需要在服务端进行跨域设置,主要代码为
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'content-type');
res.header('Access-Control-Allow-Methods', 'GET');
本来是写在rolling路由的中间件里,后来觉得这种允许跨域的请求类似白名单,可以单独写个中间件,其中用白名单过滤请求路径允许跨域,再全局应用该中间件,后面如果有跨域需求,在白名单里加即可,泛用性要好一些。
const WHITELIST = ['/rolling'];
let whiteList = function (req, res, next) {
if (WHITELIST.includes(req._parsedUrl.pathname)) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'content-type');
res.header('Access-Control-Allow-Methods', 'GET');
}
next();
};
module.exports = whiteList;
或者用cors插件,未验证。
解决完跨域问题后,需要mock轮询的数据变化,想法是在服务端定义一个计数器,超过阈值后改变返回值。
但在服务端做这个有点小问题,用nodemon热更新是不会重置全局变量的,只有重启才行,所以应该在客户端mock
var pollingWorker = createWorker(function (e) {
var cache;
var count = 0;
function compare(cur, old) {
return cur == old;
}
setInterval(function () {
count++;
fetch('http://127.0.0.1:4000/rolling?count=' + count)
.then(function (res) {
return res.text();
})
.then((data) => {
if (!compare(data, cache)) {
cache && self.postMessage(data);
cache = data;
}
});
}, 1000);
});
pollingWorker.onmessage = function () {
console.log('diff');
};
app.get('/rolling', (req, res) => {
let count = req.query.count;
if (count < 5) {
res.send('rolling1...');
} else {
res.send('rolling2...');
}
});
result
worker新建worker
主线程
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
document.getElementById('result').innerHTML = event.data;
};
worker.js
var num_workers = 10;
var items_per_worker = 10;
var result = '';
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
var worker = new Worker('core.js');
worker.postMessage(i * items_per_worker);
worker.postMessage((i + 1) * items_per_worker);
worker.onmessage = storeResult;
}
function storeResult(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;
onmessage = getEnd;
}
var end;
function getEnd(event) {
end = event.data;
onmessage = null;
work();
}
function work() {
let res = '';
for (var i = start; i < end; i += 1) {
// 具体工作
res += `cur: ${i}<br />`;
}
postMessage(res);
close();
}
整个流程就是主线程新建worker.js的worker,worker.js又根据core.js新建worker,数量在worker.js前两行定义了——10个worker.js,每个worker.js含10个core.js。
core.js接收范围的临界值,在work函数的循环里进行具体操作。在命名时,我将这组记作递归,现在看看更像是回溯。整体比较简单,core中onmessage的替换挺有意思,另一个有意思的点是输出结果:
result
每次刷新得到的结果都不一样,每个worker都是独立的,即便都是同样的操作,也有可能先创建,后完成。
总结
总体来说,worker的使用比较有局限性,必须挂到服务端,不然就只能用同页面的worker函数体,SPA的话可能只能写到index.html里。好处是能处理一些计算密集型或高延迟的任务,目前还没遇到过,有场景可以试试。