参考
链接:https://juejin.cn/post/7139718200177983524
来源:稀土掘金

介绍

Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,允许我们在 js 主线程之外开辟新的 Worker 线程,并将一段 js 脚本运行其中,它赋予了开发者利用 js 操作多线程的能力。

因为是独立的线程,Worker 线程与 js 主线程能够同时运行,互不阻塞。所以,在我们有大量运算任务时,可以把运算任务交给 Worker 线程去处理,当 Worker 线程计算完成,再把结果返回给 js 主线程。这样,js 主线程只用专注处理业务逻辑,不用耗费过多时间去处理大量复杂计算,从而减少了阻塞时间,也提高了运行效率,页面流畅度和用户体验自然而然也提高了。

虽然 Worker 线程是在浏览器环境中被唤起,但是它与当前页面窗口运行在不同的全局上下文中,我们常用的顶层对象 window,以及 parent 对象在 Worker 线程上下文中是不可用的。另外,在 Worker 线程上下文中,操作 DOM 的行为也是不可行的,document对象也不存在。但是,locationnavigator对象可以以可读方式访问。除此之外,绝大多数 Window 对象上的方法和属性,都被共享到 Worker 上下文全局对象 WorkerGlobalScope 中。同样,Worker 线程上下文也存在一个顶级对象 self

使用

创建woker

const worker = new Worker(path,options);

/*
example const worker = new Worker("worker.js")
*/

主线程和worker 线程数据传递

主线程和worker线程都通过postMessage方法来发送消息,监听message事件来接收消息

示例:

script.js

const worker = new Worker("worker.js")
worker.addEventListener('message',(e)=>{
    console.log(e.data)
})
//向worker发送消息
worker.postMessage("hello,i am main thread")

------------


worker.js
self.addEventListener('message',(e)=>{
    //接收到来自主线程的消息后,向主线程发送消息
    console.log(e.data)
    self.postMessage("hello,i am worker")
})

---

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="script.js"></script>
</body>
</html>

postMessage()方法可以接收的参数可以是字符串,对象,数组等

主线程与worker线程之间的数据传递是传值而不是传地址,

所以当你发送一个对象的时候,到了另一方就已经不是同一个对象了

传送数据支持的类型

向worker线程传送大文件 IO密集型操作

Transferable Objects 与大文件传输:深入探讨
Transferable Objects 是一种非常高效的机制,可以将大块二进制数据(例如 ArrayBuffer)从一个上下文(如主线程)转移到另一个上下文(如 Worker 线程),而无需复制数据。这对于处理大文件非常有益,因为它可以显著提高性能并减少内存占用。
为什么 Transferable Objects 适合大文件传输?
避免数据复制: 当你将一个 ArrayBuffer 标记为 Transferable 并传递给 Worker 线程时,这个 ArrayBuffer 的所有权实际上被转移了。主线程不再拥有对这个数据的访问权,而 Worker 线程获得了对它的独占访问权。这避免了在两个线程之间复制一份数据,节省了内存。
提高性能: 由于避免了数据复制,Transferable Objects 可以显著提高大文件传输的性能。尤其是对于 I/O 密集型操作,这种性能提升更为明显。
Transferable Objects 的局限性
所有权转移: 虽然 Transferable Objects 的主要优势在于所有权转移,但这也意味着主线程在传递数据后将无法再访问该数据。如果主线程还需要保留一份副本,则需要在传递之前进行复制。
数据类型限制: Transferable Objects 主要用于传输 ArrayBuffer。对于其他类型的数据,需要先将其转换为 ArrayBuffer,然后才能使用 Transferable Objects。
浏览器兼容性: 虽然大多数现代浏览器都支持 Transferable Objects,但在一些较老的浏览器中可能存在兼容性问题。
你的问题:拥有权是否转移?
是的,Transferable Objects 的核心特性就是所有权转移。 当你将一个 ArrayBuffer 标记为 Transferable 并传递给 Worker 线程时,这个 ArrayBuffer 的所有权就从主线程转移到了 Worker 线程。主线程将无法再访问这个数据。
在指使用 postMessage 方法,将一个 ArrayBuffer 对象作为参数传递给 Worker 线程,并且在 postMessage 的第二个参数中再次传递这个 ArrayBuffer,以实现数据所有权的转移。
示例代码:

// 主线程
const worker = new Worker('worker.js');
const arrayBuffer = new ArrayBuffer(1024 * 1024); // 创建一个 1MB 的 ArrayBuffer
worker.postMessage(arrayBuffer, [arrayBuffer]); // 将 ArrayBuffer 标记为 Transferable 并发送
// Worker 线程
self.onmessage = (event) => {
  const receivedBuffer = event.data;
  // 在 Worker 线程中处理 receivedBuffer
};

监听错误消息

监听方式和接收消息是一致的

关闭worker线程

// 主线程
const worker = new Worker("worker.js");
worker.terminate();

// worker线程
self.close()

无论在主线程中关闭worker,还是在worker线程内部关闭worker,worker线程中当前event loop中的任务还会继续执行,而worker线程中下一个event loop中的任务,会被直接忽略,不会继续执行

区别是,在主线程手动关闭 worker,主线程与 worker 线程之间的连接都会被立刻停止,即使 worker 线程当前的 Event Loop 中仍有待执行的任务继续调用 postMessage() 方法,但主线程不会再接收到消息。

在 worker 线程内部关闭 worker,不会直接断开与主线程的连接,而是等 worker 线程当前的 Event Loop 所有任务执行完,再关闭。也就是说,在当前 Event Loop 中继续调用 postMessage() 方法,主线程还是能通过监听message事件收到消息的。

我们来验证一下:

我们让主线程接收到一条来自worker的消息后就直接terminate,在worker线程中,我们使用先用postMessage发送一条消息,然后setInterval每隔一秒发送一个消息

script.js
const worker = new Worker("worker.js");

worker.addEventListener('message',(e)=> {
    console.log(e.data);
    worker.terminate();
})

worker.postMessage("Hello from main thread");

------

worker.js
self.addEventListener('message', () => {

    postMessage("hello from worker")

    setInterval(() => {
        const message = `Message from worker at ${new Date().toISOString()}`;
        postMessage(message);
    }, 1000);


})

我们来浏览器看看什么效果

能看到主线程接收到第一条消息以后就不再接收来自worker线程的消息了

那如果在worker线程中关闭呢?

self.close() 是在Worker线程内部调用的,用于主动终止当前Worker的执行。
当调用 self.close() 后,Worker会立即停止执行后续的代码,包括当前Worker中的所有计时器(如 setIntervalsetTimeout)、回调、事件监听等。任何正在进行的操作都会中断。

script.js

// 创建Worker
const worker = new Worker('worker.js');

// 监听消息
worker.onmessage = (event) => {
    console.log("Message from Worker:", event.data);
};

// 监听错误
worker.onerror = (event) => {
    console.error("Error from Worker:", event.message);
    console.error("File:", event.filename);
    console.error("Line:", event.lineno, "Column:", event.colno);
};

// 发送启动消息
worker.postMessage("start");


---


worker.js
self.addEventListener('message', () => {
    postMessage("hello from worker");
    setTimeout(() => {
        postMessage("Final message before closing.");
        self.close(); // 延迟关闭
    }, 5000); // 确保有时间发送消息
    setInterval(() => {
        const message = `Message from worker at ${new Date().toISOString()}`;
        postMessage(message);
    }, 1000);

});

worker线程引用其它js文件

importScripts('filepath')

我们来写一个例子

util.js,其中包括一个计算函数,我们用主线程向worker线程中发送一个对象,其中包括a,b,然后让worker线程拿到这个对象,取出其中的值,然后计算出来发送给主线程

utils.js 
const add = (a,b)=>{
    return a + b
}
script.js
// 创建Worker
const worker = new Worker('worker.js');

// 监听消息
worker.onmessage = (event) => {
    console.log("Message from Worker:", event.data);
};

// 监听错误
worker.onerror = (event) => {
    console.error("Error from Worker:", event.message);
    console.error("File:", event.filename);
    console.error("Line:", event.lineno, "Column:", event.colno);
};

// 发送启动消息
worker.postMessage({
    a: 1,
    b: 2
});

worker.js
importScripts('util.js')
self.addEventListener('message', (e) => {
    let res = add(e.data.a, e.data.b)
    postMessage("calculate result is " + res)
});

ESMODULE模式

script.js
// 创建Worker
const worker = new Worker('worker.js',{
    type: 'module'
});

// 监听消息
worker.onmessage = (event) => {
    console.log("Message from Worker:", event.data);
};

// 监听错误
worker.onerror = (event) => {
    console.error("Error from Worker:", event.message);
    console.error("File:", event.filename);
    console.error("Line:", event.lineno, "Column:", event.colno);
};

// 发送启动消息
worker.postMessage({
    a: 1,
    b: 2
});

----

worker.js

import {add} from './util.js'
self.addEventListener('message', (e) => {
    let res = add(e.data.a, e.data.b)
    let res2 = add(e.data.a,e.data.b)
    postMessage("calculate a + b is " + res)
    postMessage("calculate a - b is " + res2)
});
export default self


---

util.js
const add = (a,b)=>{
    return a + b;
}
const minus = (a,b)=>{
    return a - b;
}
export {add,minus}


主线程和worker线程之间哪些类型不能传递?

postMessage() 传递的数据可以是由结构化克隆算法处理的任何值或 JavaScript 对象,包括循环引用。

结构化克隆算法不能处理的数据:

  • Error 以及 Function 对象;

  • DOM 节点

  • 对象的某些特定参数不会被保留

    • RegExp 对象的 lastIndex 字段不会被保留
    • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write
    • 原形链上的属性也不会被追踪以及复制。