4

教你快速上手Web Worker

 3 years ago
source link: https://zhuanlan.zhihu.com/p/346160096
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

之前作业要求写一个逻辑回归正则化,由于梯度下降算法会耗费很长的计算时间,总计算时长在一分半左右,在此期间页面会处于无法响应的状态。因此想到了用 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 实现与主线程的通信

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

    <script>
    //此处使用内联脚本构造,内联脚本可以避免同源策略的限制。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()
    }
    </script>

运行结果如下:

YNjaamQ.jpg!mobile

三、执行顺序

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

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

    <script>
        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)
        }

    </script>

执行结果如下图:

m2eYZfI.jpg!mobile

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

四、控制线程运行

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

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

因此最好的办法是等待上次任务执行完毕,再触发下一个任务。对代码做一下修改:创建 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)

结果如下:

Bv6bAbM.jpg!mobile

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

五、应用

1、耗时任务处理

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

MbiymqR.jpg!mobile

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

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

eeIbQ3.jpg!mobile

最终结果如下图:

iURJfiZ.gif!mobile

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

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 )


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK