教你快速上手Web Worker

前言

之前作业要求写一个逻辑回归正则化,由于梯度下降算法会耗费很长的计算时间,总计算时长在一分半左右,在此期间页面会处于无法响应的状态。因此想到了用 Web Worker 处理,保证主页面的流畅性。

一、Web Worker 基础介绍

Web Worker 用来创造多线程,来帮助分担比执行较耗时的任务,从而不影响主线程的流畅度,相比于其他异步解决方法,Web Worker 是真正意义上的不会阻塞线程。Web Worker 所属的全局对象与主线程不一样,不能直接操作 dom,window。但是可以使用 window 中的许多功能,例如 websocket,IndexDB 等功能。

Web Worker 有三种类型:

  • Dedicated Worker(专用 Worker):由主线程实例化且只能与它通信,不能访问别的环境。
  • Shared Worker(共享 Worker):所有 Shared Worker 实例共享一个全局环境,多个页面可以用 Shared Worker 互相通信,可以处理多个连接。
  • Service worker(服务 Worker):具有 Shared Worker 的特点、通过事件驱动,可以随时关闭再重启、不需要任何页面也能工作,只支持 https 和 localhost。多用于离线场景

平常使用最多,最常见的就是 Dedicated Worker,并且 Dedicated Worker 兼容性较好,因此本文主要介绍 Dedicated Worker,用 worker 代指 Dedicated Worker。

二、基本用法

1、基本使用方法

主线程方法:

var worker = new Worker('work.js') //用来新建一个 worker,参数为脚本文件的 url 地址,url会受到同源策略的限制

worker.postMessage(message) //向子线程传递消息,参数可以为任意类型的值,传递的是数据的拷贝,而非引用。传递大量数据建议使用转移

worker.onmessage = function (event) {
    console.log(event.data);
    ...
} //接收子线程返回的消息,数据储存在 event.data 里面

worker.terminate() //终止线程方法。worker建立后不会自动销毁,需要手动控制销毁,需要注意这是一个同步方法,一定要根据情况放在回调里面使用,否则会立即终止子线程。

子线程方法:

this.addEventListener('message', function (e) {
    this.postMessage(e.data);
}, false)

//this.addEventListener 用于监听主线程传递过来的消息,this.postMessage 用于向主线程发送消息。this 代表子线程自身。

以上方法能够满足大多数场景的使用,更多更完整的说明方法可以查阅官方文档和下方链接。

2、新建一个 worker 实现与主线程的通信

        //监听收到的消息
        this.addEventListener('message',function(e){
            console.log(e.data)
            //向主线程返回消息
            this.postMessage('我脑海里全都是你')
        },false)
    

    
    //此处使用内联脚本构造,内联脚本可以避免同源策略的限制。Blob方法把对象转为二进制,URL.createObjectURL为Blob对象生成网络url地址。
    var blob = new Blob([document.querySelector('#worker').textContent]);
    var url = window.URL.createObjectURL(blob);
    var worker = new Worker(url);

    //传递消息
    worker.postMessage('oh honey');

    //监听消息
    worker.onmessage=function(e){
        console.log(e.data)
        //终止线程
        worker.terminate()
    }
    

运行结果如下:

三、执行顺序

知道子线程的执行顺序是非常有必要的,子线程有自己的任务队列,遵从任务队列的特性,与主线程互不干扰。子线程可以触发多次,返回数据的顺序为子线程任务队列的执行顺序。 具体见下图,触发了 3 个任务,且三个任务都是异步任务。

        //子线程任务为异步任务
        this.addEventListener('message',function(e){
            setTimeout(() => {
                this.postMessage(`子线程耗时${e.data}`)
            }, e.data)
        },false)
    

    
        var blob = new Blob([document.querySelector('#worker').textContent]);
        var url = window.URL.createObjectURL(blob);
        var worker = new Worker(url);

        //触发三次任务
        const time=[10000,5000,2000];
        for(var i=0;i<3;i++)
            {
                worker.postMessage(time[i]);
            }

        //输出结果
        worker.onmessage=function(e){
            console.log(e.data)
        }

    

执行结果如下图:

解析:三个异步任务被触发后形成任务队列,子线程按照执行顺序执行。

四、控制线程运行

上一段中有一点值得注意,在多次触发子线程任务时,触发的任务复杂度情况不一样,并且触发的时间也不一样,可能子线程正在执行任务,又添加了一个新任务。结果就是这些任务会不可预知的合并在一起,给数据返回带来混乱。

例如在上述例子中添加一个同步任务,同步任务又会先于异步任务执行。

因此最好的办法是等待上次任务执行完毕,再触发下一个任务。对代码做一下修改:创建 worker 类:

class myWorker{
            constructor(url){
                this.queue=[];
                this.worker=new Worker(url);
                this.worker.onmessage=(e)=>this.queue.shift().resolve(e.data);
                this.worker.onerror=(e)=>this.queue.shift().reject(e.data);
            }

            post(params){
                return new Promise((resolve,reject)=>{
                    this.queue.push({resolve,reject});
                    this.worker.postMessage(params)
                })
            }
        }

使用 async 函数控制执行顺序

var worker = new myWorker()
        const time=[10000,5000,2000]
        async function dispatch(time){
            while(time.length)
            {
                var res=await worker.post(time.shift())
                console.log(res)
            }
        }

        dispatch(time)

结果如下:

控制好子线程,那么子线程的销毁也就好控制了。

五、应用

1、耗时任务处理

这个是最常见的用法,在处理耗时任务时非常有用。作业里的子线程代码大致像这样:

在里面定义好处理函数和一些初始化数据,等待主线程的传入数据就可以开始。

主线程此时只负责传入数据,等待子线程完成进行数据渲染处理就好了,非常像异步回调函数的形式。主线程大致如下:

最终结果如下图:

可以看出运行非常顺畅,鼠标可以随意滑动,页面没有卡死。其他耗时任务包括高频的用户交互,例如根据用户的输入习惯、历史记录以及缓存等信息来协助用户完成输入的纠错、校正功能等。

2、Worker 线程完成轮询

子线程核心代码就是定时向服务器请求数据,比较后返回

setInterval(function () {
    fetch('url').then(function (res) {
      var data = res.json();
      if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
      }
    })
  }, 1000)
});

3、渐进式网络应用。

使用 IndexDB 等功能将数据请求储存至本地,在网络不稳定的时候也能快速加载,为了不阻塞 UI 线程的渲染,这项工作必须由 Web Workers 来执行。这里需要使用 Service Worker,详情参考以下链接( https:// developer.mozilla.org/z h-CN/docs/Web/Progressive_web_apps/Introduction )

参考链接:

https:// blog.sessionstack.com/h ow-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a

https:// developer.mozilla.org/e n-US/docs/Web/API/Web_Workers_API/Using_web_workers

https://www. ibm.com/developerworks/ cn/web/1112_sunch_webworker/index.html

https:// developer.mozilla.org/e n-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers )