Web Worker
是 HTML5 标准的一部分,这一规范定义了一套 API。
Web Worker 允许我们在js主线程之外运行一个独立的线程(Worker),也就是运行在后台的一个独立的js脚本。
因为Worker线程是独立的线程,与js主线程能够同时允许,互不阻塞。
所以如果有大量运算任务时,可以把任务交给Worker线程去处理,当Worker线程计算完成,再把结果返回给js主线程。这样,js主线程只用专注处理业务逻辑,不用耗费过多时间去处理大量复杂计算,从而减少了阻塞时间,也提高了运行效率,页面流畅度和用户体验自然而然也提高了。
能解决的问题是:
1 解决页面卡死问题;
2 发挥多核CPU的优势,提高JS性能
2.1 创建 worker
创建 worker
只需要通过 new
调用 Worker()
构造函数即可,它接收两个参数
const worker = new Worker(path, options);
参数 | 说明 |
---|---|
path | 有效的js脚本的地址,必须遵守同源策略。无效的js地址或者违反同源策略,会抛出SECURITY_ERR 类型错误 |
options.type | 可选,用以指定 worker 类型。该值可以是 classic 或 module 。 如未指定,将使用默认值 classic |
options.credentials | 可选,用以指定 worker 凭证。该值可以是 omit , same-origin ,或 include 。如果未指定,或者 type 是 classic ,将使用默认值 omit (不要求凭证) |
options.name | 可选,在 DedicatedWorkerGlobalScope 的情况下,用来表示 worker 的 scope 的一个 DOMString 值,主要用于调试目的。 |
2.2 主线程与 worker 线程数据传递
发送消息:postMessage()。postMessage() 方法接收的参数可以是字符串、对象、数组等
接受消息:监听 message 事件
2.3 监听错误
web worker 提供两个事件监听错误,error
和 messageerror
。
这两个事件的区别是:
事件 | 描述 |
---|---|
error |
当worker内部出现错误时触发 |
messageerror |
当 message 事件接收到无法被反序列化的参数时触发 |
2.4 关闭 worker 线程
worker 线程的关闭在主线程和 worker 线程都能进行操作,但对 worker 线程的影响略有不同。
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.terminate(); // 关闭worker
// worker.js(worker线程)
self.close(); // 直接执行close方法就ok了
无论是在主线程关闭 worker,还是在 worker 线程内部关闭 worker,worker 线程当前的 Event Loop 中的任务会继续执行。至于 worker 线程下一个 Event Loop 中的任务,则会被直接忽略,不会继续执行。
区别是,在主线程手动关闭 worker,主线程与 worker 线程之间的连接都会被立刻停止,即使 worker 线程当前的 Event Loop 中仍有待执行的任务继续调用 postMessage()
方法,但主线程不会再接收到消息。
在 worker 线程内部关闭 worker,不会直接断开与主线程的连接,而是等 worker 线程当前的 Event Loop 所有任务执行完,再关闭。也就是说,在当前 Event Loop 中继续调用 postMessage()
方法,主线程还是能通过监听message
事件收到消息的。
2.5 Worker 线程引用其他js文件
总有一些场景,需要放到 worker 进程去处理的任务很复杂,需要大量的处理逻辑,我们当然不想把所有代码都塞到 worker.js
里,那样就太糟糕了。不出意料,web worker 为我们提供了解决方案,我们可以在 worker 线程中利用 importScripts()
方法加载我们需要的js文件,而且,通过此方法加载的js文件不受同源策略约束!
// utils.js
const add = (a, b) => a + b;
// worker.js(worker线程)
// 使用方法:importScripts(path1, path2, ...);
importScripts('./utils.js'); // 加载需要的js文件
console.log(add(1, 2)); // log 3
2.6 ESModule 模式
还有一些场景,当你开启一个新项目,正高兴的用 importScripts()
导入js文件时发现, importScripts()
方法执行失败。仔细一看,原来是新项目的 js 文件都用的是 ESModule 模式。难道要把引用到的文件都改一遍吗?当然不用,还记得上文提到初始化 worker 时的第二个可选参数吗,我们可以直接使用 module 模式初始化 worker 线程!
// main.js(主线程)
const worker = new Worker('/worker.js', {
type: 'module' // 指定 worker.js 的类型
});
// utils.js
export default add = (a, b) => a + b;
// worker.js(worker线程)
import add from './utils.js'; // 导入外部js
self.addEventListener('message', e => {
postMessage(e.data);
});
add(1, 2); // log 3
export default self; // 只需把顶级对象self暴露出去即可
2.7 主线程和 worker 线程可传递哪些类型数据
很多场景,在调用某些方法时,我们将一些自定义方法当作参数传入。但是,当你使用 postMessage()
方法时这么做,将会导致 DATA_CLONE_ERR
错误。
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
const fun = () => {};
myWorker.postMessage(fun); // Error:Failed to execute 'postMessage' on 'Worker': ()=>{} could not be cloned.
那么,使用 postMessage()
方法传递消息,可以传递哪些数据?
postMessage()
传递的数据可以是由结构化克隆算法处理的任何值或 JavaScript 对象,包括循环引用。
结构化克隆算法不能处理的数据:
Error
以及 Function
对象;RegExp
对象的 lastIndex
字段不会被保留结构化克隆算法支持的数据类型:
类型 | 说明 |
---|---|
所有的原始类型 | symbols 除外 |
Boolean 对象 | |
String 对象 | |
Date | |
RegExp | lastIndex 字段不会被保留。 |
Blob |
|
File |
|
FileList |
|
ArrayBuffer | |
ArrayBufferView | 这基本上意味着所有的 类型化数组 ,如 Int32Array 等。 |
ImageData |
|
Array | |
Object | 仅包括普通对象(如对象字面量) |
Map | |
Set |
主线程文件 webworker.html
DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Web Workerstitle>
head>
<body>
<button onclick="startWorker()">开启 worker 线程button>
<button onclick="stopWorker()">关闭 worker 线程button>
<script>
var myWorker;
function startWorker() {
// 1 创建 worker
myWorker = new Worker("./workers.js");
// 2 向 worker 线程发送消息
myWorker.postMessage("主线程向 worker 线程发送消息");
// 3 接受 worker 线程发送的消息
myWorker.addEventListener("message", (e) => {
console.log("主线程收到的 worker 线程发来的数据:", e.data);
});
// 这种写法也可以 接收消息
// myWorker.onmessage = e => {
// console.log(e.data);
// };
// 4 监听错误
myWorker.addEventListener("error", (err) => {
console.log(err.message); // 当worker内部出现错误时触发
});
myWorker.addEventListener("messageerror", (err) => {
console.log(err.message); // 当 message 事件接收到无法被反序列化的参数时触发
});
}
// 5 在主线程中关闭 worker 线程
function stopWorker() {
myWorker.terminate();
console.log("关闭 worker 线程");
myWorker = undefined;
}
script>
body>
html>
worker 线程文件 worker.js
(
function () {
// 接受与发送消息
self.addEventListener('message', e => { // 接收到消息
console.log('worker 线程收到的消息:', e.data);
self.postMessage('向主线程发送消息'); // 向主线程发送消息
});
// 监听错误
self.addEventListener('error', err => {
console.log(err.message);
});
self.addEventListener('messageerror', err => {
console.log(err.message);
});
// 关闭 worker 线程
// self.close(); // 直接执行close方法就ok了
}
)()
SharedWorker 是一种特殊类型的 Worker,可以被多个浏览上下文访问,比如多个 windows,iframes 和 workers,但这些浏览上下文必须同源。它们实现于一个不同于普通 worker 的接口,具有不同的全局作用域:SharedWorkerGlobalScope
,但是继承自WorkerGlobalScope
SharedWorker
线程的创建和使用跟 worker
类似,事件和方法也基本一样。 不同点在于,主线程与 SharedWorker
线程是通过MessagePort
建立起链接,数据通讯方法都挂载在SharedWorker.port
上。
值得注意的是,如果你采用 addEventListener
来接收 message
事件,那么在主线程初始化SharedWorker()
后,还要调用 SharedWorker.port.start()
方法来手动开启端口。
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.start(); // 开启端口
myWorker.port.addEventListener('message', msg => {
console.log(msg.data);
})
但是,如果采用 onmessage
方法,则默认开启端口,不需要再手动调用SharedWorker.port.start()
方法
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.onmessage = msg => {
console.log(msg.data);
};
以上两种方式效果是一样的,具体信息请参考MessagePort。
由于 SharedWorker
是被多个页面共同使用,那么除了与各个页面之间的数据通讯是独立的,同一个SharedWorker
线程上下文中的其他资源都是共享的。基于这一点,很容易实现不同页面之间的数据通讯。
一个利用SharedWorker
实现多页面数据共享的例子
// index.html
DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>index pagetitle>
head>
<body>
<p>index page: p>
count: <span id="container">0span>
<button id="add">addbutton>
<br>
// 利用iframe加载
<iframe src="./iframe.html">iframe>
body>
<script type="text/javascript">
if (!!window.SharedWorker) {
const container = document.getElementById('container');
const add = document.getElementById('add');
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.start();
myWorker.port.addEventListener('message', msg => {
container.innerText = msg.data;
});
add.addEventListener('click', () => {
myWorker.port.postMessage('add');
});
}
script>
html>
// iframe.html
DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>iframe pagetitle>
head>
<body>
<p>iframe page: p>
count: <span id="container">0span>
<button id="reduce">reducebutton>
body>
<script type="text/javascript">
if (!!window.SharedWorker) {
const container = document.getElementById('container');
const reduce = document.getElementById('reduce');
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.start();
myWorker.port.addEventListener('message', msg => {
container.innerText = msg.data;
})
reduce.addEventListener('click', () => {
myWorker.port.postMessage('reduce');
});
}
script>
html>
// sharedWorker.js
let num = 0;
const workerList = [];
self.addEventListener('connect', e => {
const port = e.ports[0];
port.addEventListener('message', e => {
num += e.data === 'add' ? 1 : -1;
workerList.forEach(port => { // 遍历所有已连接的part,发送消息
port.postMessage(num);
})
});
port.start();
workerList.push(port); // 存储已连接的part
port.postMessage(num); // 初始化
});
结果可以发现,index 页面和 iframe 页面的 count 始终保持一致,实现了多个页面数据同步。