教你快速上手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:// 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