什么是 Web Worker
众所周知,js是单线程的,那么在进行大量的操作和运算的时候,可能会卡顿,影响性能。
Web Worker 是一项 HTML5 新特性,Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。就像之前你的奶茶店只有你一个人,什么事情都需要你自己亲历亲为,现在你可以请一个帮手了,你可以把那些花时间的杂活比如拖地,制冰,烧水。。。让他做,你自己去做主要的流程的事情。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。这意味着,即使脚本执行了很长时间,Web 应用程序的 UI 仍然可以保持响应。
体验到有帮手的好处了吧?而且这个帮手做事情非常专注,他做事情不会被其他人打扰,而且人还老实,不会偷懒,做完事情也不主动休息,简直是踏实肯干的劳动模范!!!
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
Web Worker 分为两种类型,专用线程(Dedicated Web Worker) 和共享线程(Shared Web Worker)。专用线程仅能被创建它的脚本所使用(一个专用线程对应一个主线程),而共享线程能够在不同的脚本中使用(一个共享线程对应多个主线程
先看专用worker
如何使用
-
新建一个 Worker 线程
判断浏览器受否支持
if (window.Worker) { // ... }使用new命令,调用Worker()构造函数来创建一个线程
let worker = new Worker('work.js');Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。但是 Worker 不能读取本地文件,所以这个脚本必须来自网络。浏览器会创建一个新的后台线程,加载该 URL 指定的脚本,并在该线程中执行。如果下载没有成功(比如404错误),Worker 就会失败。
-
线程通信
Worker 线程和主线程都通过 postMessage() 方法发送消息,通过 onmessage 事件接收消息
-
主线程给 Worker 发消息
使用 worker.postMessage(params) 方法
params就是主线程传给 Worker 的数据。它可以是各种数据类型(除了Error 以及 Function 对象,DOM 节点),包括二进制数据。
worker.postMessage('Hello World'); worker.postMessage({method: 'speak', args: ['你是大帅逼']}); -
主线程如何接收 Worker 发的消息呢?
使用 worker.onmessage 指定监听函数接收子线程发回来的消息
worker.onmessage = function (event) { console.log('Received message ' + event.data); doSomething(); } function doSomething() { // 收到消息 执行任务 worker.postMessage('gogogo!'); } -
Worker 接收主线程发得消息
self.addEventListener('message', function (e) { console.log('收到消息:' + e.data); }, false); -
Worker 给主线程发消息
self.addEventListener('message', function (e) { console.log('收到消息:' + e.data); // do someThing // 将结果发送回主线程 self.postMessage(result); }, false);在 Worker 线程中,self 和 this 都代表子线程的全局对象。
对于监听 message 事件,以下的几种写法是等效的
this.addEventListener('message', function (e) { this.postMessage('给主线程发的消息'); }, false); addEventListener('message', function (e) { postMessage('给主线程发的消息'); }, false); self.addEventListener('message', function (e) { self.postMessage('给主线程发的消息'); }) onmessage = function (e) { postMessage('给主线程发的消息'); } -
这个帮手太老实,所以他做完工作得主动让他下班休息(做完直接让他下班或者给他打电话 让他知道 你做完了就自己下班)
// 两种关闭方式都行 worker.terminate(); // 主线程关闭 self.close() // worker主动关闭 -
worker 错误处理
主线程和worker线程都可以监听 error 时间
// 主线程监听错误 worker.onerror(function (event) { console.log(event); }) // 或者 worker.addEventListener('error', function (event) { console.log(event); }); // 子线程监听错误 onerror = function (event) { console.log(event); }
-
简单得demo
// 主线程js
// 创建worker
var waiterWorker = new Worker('/worker.js', { name: 'waiterWorker' });
// 向 worker 发送信息
waiterWorker.postMessage({ title: 'waiter,收到请回答,把这个数字乘2', params: 22 });
// 当从 Worker 接收到消息时
waiterWorker.onmessage = function (event) {
// 获取 Worker 返回的结果
var result = event.data;
// 在控制台打印结果
console.log('我是店主:我收到了来自waiter的结果==>', result);
};
// 当不再需要 Worker 时,可以终止它 任务完成,回炉重造
// waiterWorker.terminate();
// --------------------------------------
// worker.js
// 当接收到消息时
self.onmessage = function (event) {
// 获取传递的数据
let { data: { title, params } } = event;
console.log('我是waiter:我收到了来自店主得呼唤==>', title);
self.postMessage('我是waiter,我是waiter,我马上开干!');
// 假设我们在这里执行一些计算密集型任务
const result = calculate(params);
// 将结果发送回主线程
self.postMessage(result);
};
// 任务完成,回炉重造
// self.close()
function calculate(data) {
// 这里只是一个简单的示例,你可以替换为实际的计算任务
for (var i = 0; i < 100000000; i++) {
// 执行一些计算
}
return data * 2; // 假设这就是我们的计算结果
}
api 汇总
- 主线程
| 主线程 api | 说明 |
|---|---|
| new Worker(jsUrl, options) | 接受两个参数。第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则会报错。第二个参数是配置对象,该对象可选(可以指定 type、credentials、name 三个属性)。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程 // 主线程 var oneWorker = new Worker('worker.js', { name : 'oneWorker' }) // 子线程 self.name // oneWorker |
| Worker.onerror | error 事件的监听函数 |
| Worker.onmessage | message 事件的监听函数,发送过来的数据在Event.data属性中 |
| Worker.onmessageerror | messageerror 事件的监听函数,发送的数据无法序列化成字符串时,会触发这个事件 |
| Worker.postMessage | 主线程向 Worker 线程发送消息 |
| Worker.terminate | 结束 Worker 线程 |
- 子线程
Web Worker 有自己的全局对象,不是主线程的window,而是一个专门为 Worker 定制的全局对象。因此定义在window上面的对象和方法不是全部都可以使用。
Worker 线程有一些自己的全局属性和方法
| 子线程 api | 说明 |
|---|---|
| self.name | worker 的名字。该属性只读,由构造函数指定 |
| self.onmessage | message事件的监听函数 |
| self.onmessageerror | messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。 |
| self.importScripts | 加载 JS 脚本 |
| self.postMessage | 向产生这个 Worker 线程发送消息 |
| self.close | 关闭 Worker 线程 |
主线程的 terminate() 方法和在 Worker 线程中使用 close() 方法关闭 worker 这两种方法是等效的,但比较推荐的用法是使用 close(),防止意外关闭正在运行的 Worker 线程。Worker 线程一旦关闭 Worker 后 Worker 将不再响应。
worker 加载外部脚本
importScripts('script1.js')
importScripts('script2.js')
// 或者一次性加载两个
importScripts('script1.js', 'script2.js')
共享worker
共享worker可以同时被多个脚本使用,即使这些脚本正在被不同的window、iframe或者worker访问,也就是说可以使用共享worker进行多个浏览器窗口间通信,当然共享worker的通信必须为同源,不能跨域通信。SharedWorker 线程的创建和使用跟 worker 类似,事件和方法也基本一样。只是构造器的名字不同
他们之间一个很大的区别在于:共享worker必须通过一个确切的打开的端口对象供脚本与worker通信,在专用worker中这一部分是隐式进行的。如果父级线程和worker线程需要双向通信,那么它们都需要调用start()方法,对于消息的传递依然使用postMessage但是必须通过调用端口上的postMessage方法来实现消息通信。
也就是说主线程与 SharedWorker 线程是通过MessagePort建立起链接,数据通讯方法都挂载在SharedWorker.port上
共享worker 是被多个页面共同使用,那么除了与各个页面之间的数据通讯是独立的,同一个 共享worker 线程上下文中的其他资源都是共享的。基于这一点,很容易实现不同页面之间的数据通讯
看是否支持 和 专用worker一样
if (window.SharedWorker) {
// ...
}
-
线程创建
共享线程使用 Shared Worker() 方法创建,同样支持两个参数,用法与 Worker() 一致
var sharedWorker = new SharedWorker('worker.js') -
数据通信
在传递消息时,postMessage() 方法和 onmessage 事件必须通过端口对象调用, 这个和 专用worker 是不一样的
需要注意的是:如果采用 addEventListener 来接收 message 事件,那么在主线程初始化SharedWorker() 后,还要调用 SharedWorker.port.start() 方法来手动开启端口。
// main.js(主线程) const myWorker = new SharedWorker('./worker.js'); myWorker.port.start(); // 开启端口 myWorker.port.addEventListener('message', msg => { console.log(msg); })如果采用 onmessage 方法,则默认开启端口,不需要再手动调用SharedWorker.port.start()方法
// main.js(主线程) const myWorker = new SharedWorker('./worker.js'); myWorker.port.onmessage = msg => { console.log(msg); };另外,在 Worker 线程中,需要使用 onconnect 事件监听端口的变化,并使用端口的消息处理函数进行响应。
onconnect = function (e) { let port = e.ports[0] port.onmessage = function (e) { // ... } } -
demo
// 页面1 if (!!window.SharedWorker) { const pageOneTwoWorker = new SharedWorker('/worker.js'); pageOneTwoWorker.port.start(); pageOneTwoWorker.port.postMessage('页面1'); pageOneTwoWorker.port.addEventListener('message', msg => { console.log('msg', msg); }) } // 页面2 if (!!window.SharedWorker) { const pageTwoWorker = new SharedWorker('/worker.js'); pageTwoWorker.port.start(); pageTwoWorker.port.postMessage('页面2'); pageTwoWorker.port.addEventListener('message', msg => { console.log('msg', msg); }) } // worker.js // worker线程 var portArr = []; onconnect = function (e) { var port = e.ports[0]; if (portArr.indexOf(port) === -1) portArr.push(port); port.onmessage = function (e) { portArr.forEach(v => { v.postMessage(e.data); }) } }
Web Worker 需要注意的点
-
Worker 线程运行的脚本文件,必须与主线程的脚本文件同源
-
Worker 无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象
-
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成
-
Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求
-
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
想本地调试的可以在VScode里面通过
调试
- 如果 Worker 线程频繁与主线程进行交互,主线程需要处理交互,仍有可能使页面发生阻塞
- new Worker传入的js是采用的ESModule 模式,可以使用 worker 的第二个可选参数设置为 module 模式初始化 worker 线程!
// main.js(主线程) const worker = new Worker('worker.js', { type: 'module' // 指定 worker.js 的类型 });
那些情况使用
- 懒加载
- 流媒体数据处理
- canvas 图形绘制
- 图像处理
- 计算密集型任务
- ...