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里。好处是能处理一些计算密集型或高延迟的任务,目前还没遇到过,有场景可以试试。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容