Web Worker使用

作者: 绝尘kinoko | 来源:发表于2022-05-13 16:42 被阅读0次

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

    相关文章

      网友评论

        本文标题:Web Worker使用

        本文链接:https://www.haomeiwen.com/subject/trfrurtx.html