JavaScript技术

Web Worker使用

2022-05-13  本文已影响0人  绝尘kinoko

最近对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)。然后就进行了一系列的试错。

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

上一篇下一篇

猜你喜欢

热点阅读