所以脑洞大开,想拿Web Worker尝试一下,看看能否有性能上的提升,所以,我花费将近两天的时间整理出了下面的教程,与大家共勉。文章过长,看不完的可以保存起来慢慢看。
简介
基本工作线程使用
使用思路:可以将复杂的任务,比如下载多个文件这样的任务抽取出来,放到工作线程中。在主线程中创建工作线程。等工作线程的任务执行完成后,再通过线程通信方式将数据发给主线程,再在界面上进行渲染数据。
主线程(main.js)
worker 是使用构造函数,例如 Worker()
创建的对象,该构造函数的参数是个js脚本文件,这个js文件包含了需要在工作线程中运行的代码。主线程使用new Worker()
的方式创建工作线程,如下所示:
// 构造函数
new Worker(aURL)
new Worker(aURL, options)
//示例
const myworker = new Worker("worker.js")
这样就在主线程中就可以使用myworker工作线程了。为了更好的处理错误和向后兼容,一般写法如下:
if (window.Worker) {
//示例
const myworker = new Worker("worker.js")
}
在主线程中,我们可以通过 postMessage()
方法向工作线程中发送消息,并使用 onmessage
监听消息事件从工作线程中接收消息,方向反过来也是如此。
postMessage()
可以传递任意类型的数据,包括二进制数据
//字符串
myWorker.postMessage("你好,我是主线程");
//数组
myWorker.postMessage([1,2,3,4,5,6]);
//json对象或普通object对象
myWorker.postMessage({"name":"zhangsan"});
console.log("消息发送给了工作线程");
myWorker.onmessage = (e) => {
result.textContent = e.data;
console.log("从工作线程接收到了消息,消息内容为"+ result.textContent);
};
子线程(worker.js)
self
,self 代表子线程自身,即子线程的全局对象。self.addEventListener()
指定监听函数,也可以使用self.onmessage
指定。监听函数的参数是一个事件对象,其data
属性包含主线程发来的数据。同样的,我们使用self.postMessage()
方法来向主线程发送消息。self.onmessage = (e) => {
console.log("从主线程接收到消息");
const workerResult = `Result: ${e.data[0] * e.data[1]}`;
console.log("发送消息给主线程");
postMessage(workerResult);
};
或者,像下面这样
self.addEventListener('message', function (e) {
console.log("从主线程接收到消息");
let data = e.data;
switch (data.cmd) {
case 'start':
self.postMessage('WORKER STARTED: ' + data.msg);break;
case 'stop':
self.postMessage('WORKER STOPPED: ' + data.msg);
self.close(); // 在worker线程内部关闭工作线程.
break;
default:
self.postMessage('Unknown command: ' + data.msg);};}, false);
注意:在主线程中,要使用 Worker 对象的
onmessage
和postMessage()
方法,来监听和发送消息,而在 Worker 线程中,可以直接使用 onmessage 和 postMessage() 方法,不需要加上 Worker 对象的前缀,因为 Worker 线程中的 this 或 self 就是指向 Worker 对象的。也就是说在work线程中可以省略掉 self 或 this 关键字。
终止工作线程
//在主线程中终止工作线程可以使用
myWorker.terminate();
//在子线程中终止
self.close();
处理错误
onerror
事件处理程序。它接收名为 error
的事件,该事件实现了ErrorEvent
接口。可以在主线程监听主线程的错误信息// 为 Web Worker 对象添加一个 onerror 事件处理函数,用于捕获并显示错误的信息
myworker.onerror = function(event) {
// 阻止默认的错误处理行为
event.preventDefault();
// 在控制台中打印错误的信息console.log("Error in Web Worker:");
console.log("错误信息: " + event.message);
console.log("文件脚本: " + event.filename);
console.log("行号: " + event.lineno);
};
//控制台运行
Error in Web Worker:
错误信息: Something went wrong!
文件脚本: worker.js
行号: 2
// worker.js
// 在 Web Worker 中故意抛出一个错误
throw new Error("Something went wrong!");
使用web worker的规则及限制
https://example.com/worker.js
,那么子 Worker 的文件路径应该是 https://example.com/subworker.js
,而不是 https://example.com/page/subworker.js
。这样做可以让Worker工作线程更容易地找到它们所需要的文件,而不用担心文件路径的问题。在主线程中导入脚本和库
// 导入一个脚本
importScripts("foo.js");
// 导入多个脚本
importScripts("foo.js", "bar.js");
// 导入绝对路径的脚本
importScripts("https://example.com/foo.js");
// 导入相对路径的脚本
importScripts("../foo.js");
-
当我们在worker工作线程使用一些公共的或复用的功能时,可以导入一些通用的或模块化的脚本,比如一些工具函数、库函数、算法函数等。 -
在工作线程中使用一些动态的或条件的功能时,导入一些根据不同情况选择的脚本,比如一些配置文件、语言文件、主题文件等。 -
在工作线程使用一些异步的或延迟的功能时,可以通过 importScripts() 函数导入一些在特定时间或事件触发的脚本,比如一些更新文件、通知文件、回调文件等。
/** main.js */
// 创建一个 Web Worker 对象,指定 worker.js 作为执行内容
var worker = new Worker("worker.js");
// 为 Web Worker 对象添加一个 onmessage 事件处理函数,用于接收并显示 Web Worker 发送的消息
worker.onmessage = function(e) {
// 在控制台中打印消息
console.log(e.data);
};
/** worker.js */
// 使用 importScripts() 函数导入两个脚本
importScripts("first.js", "second.js");
// 使用 foo.js 中定义的变量 a
var b = a + 1;
// 使用 bar.js 中定义的函数 c
var d = c(b);
// 使用 postMessage() 函数,向主线程发送一个消息
postMessage(d);
/** first.js*/
// 定义一个变量 a
var a = 5;
/** second.js*/
// 定义一个函数 c
function c(x) {
return x * x;
}
共享工作线程
port
对象进行通信, 并打开一个显式端口,脚本可以使用该端口与工作线程进行通信(在基本的工作线程中是隐式完成的)。onmessage
事件处理程序隐式启动端口连接,或者使用 start()
方法显式启动端口连接。仅当 message
事件通过 addEventListener()
方法连接时才需要调用 start()
。start()
方法打开端口连接时,如果需要双向通信,则需要父线程和工作线程都调用该方法。基本用法(差异点)
使用new SharedWorker
创建共享的工作线程:
const myWorker = new SharedWorker("worker.js");
主线程向共享线程发送消息,必须过端口对象调用 postMessage()
方法
//主线程向共享线程发送消息
myWorker.port.postMessage([12, 'hello']);
在共享线程中
onconnect = (e) => {
const port = e.ports[0];
port.onmessage = (e) => {
const workerResult = `Result: 你好,主线程`;
port.postMessage(workerResult);
};
};
onconnect
与主线程通过端口建立连接(即,当父线程中的 onmessage
事件处理程序设置时,或者当 start()
方法在父线程中显式调用时),此时,主线程要建立消息监听。onconnect
的参数是一个事件对象,可以使用此事件对象的 ports
属性来获取端口并将其存储在变量中。onmessage
方法来进行任务处理并将结果返回到主线程去。在工作线程中设置 onmessage
时, 还会隐式打开返回到父线程的端口连接,因此实际上不需要调用 port.start()
。myWorker.port.onmessage = (e) => {
result2.textContent = e.data;
console.log("Message received from worker");
};
数据通信
对此,大多数浏览器都是基于结构化克隆算法实现的。所谓的结构化克隆算法是一种复制复杂的 JavaScript 对象的方法,它可以让你在不同的地方使用同样的对象,比如在不同的线程之间传递数据,或者在数据库中存储数据,或者给其他的功能使用数据。它会把你要复制的对象分解成很多小的部分,然后一一复制这些部分,最后再组合成一个新的对象,这个新的对象和原来的对象一模一样,但是是一个独立的副本,不会影响原来的对象。
传递复杂类型
平时,我们可以使用json做序列化与反序列化,但是JSON不能用来处理循环引用的情况,比如下面是一个循环引用(指一个对象的属性引用了这个对象本身,或者引用了另一个对象的属性,而另一个对象的属性又引用了这个对象,形成了一个循环。)的例子:
// 创建一个对象 a
var a = {};
// 创建一个对象 b
var b = {};
// 让 a 的属性 x 引用 b
a.x = b;
// 让 b 的属性 y 引用 a
b.y = a;
如果你用 JSON 来复制这个循环引用的对象,会出错,因为 JSON 不能处理循环引用,它会认为这是一个无限循环,无法转换成字符串。如果你这样写会报错
// 使用 JSON 复制循环引用的对象
let c = JSON.parse(JSON.stringify(a)); // 报错
但是如果你用结构化克隆算法来复制这个循环引用的对象,就不会出错,因为结构化克隆算法可以处理循环引用,它会记录每个对象的引用,避免无限循环,正确地复制每个对象。
// 用结构化克隆算法复制循环引用的对象
let c = structuredClone(a); // 不报错
对于传递复杂类型:比如对象、数组、函数等。这些类型需要经过结构化克隆算法(Structured Clone Algorithm)进行序列化和反序列化,复杂类型的传递都会有一定的性能影响。
传递可转移对象
postMessage(ArrayBuffer.toString())
直接将数据发送出去,那么就又会出现二进制的拷贝,因为前面我们说了,大多数浏览器都是基于结构化克隆算法实现的数据拷贝,如果文件比较大,那么无疑会造成非常大的性能问题。ArrayBuffer
将被清除并且不再可用。这样做的目的就是为了防止出现多个线程同时修改数据。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker。此方式在发送大型数据集时会带来巨大的性能改进。//使用方式 Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// 例如
const uInt8Array = new Uint8Array(1024 * 1024 * 32).map((v, i) => i);
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
可传输对象支持跨代理传输。传输是有效地重新创建对象,同时共享对底层数据的引用,然后分离正在传输的对象。
注意:转账是不可逆且非幂等的操作。一旦对象被转移,它就不能被再次转移,甚至不能被再次使用。
注意:像
Int32Array
和Uint8Array
这样的类型化数组是可序列化的,但不可传输。然而,它们的底层缓冲区是一个ArrayBuffer
,它是一个可传输的对象。我们可以在数据参数中发送uInt8Array.buffer
,但不在传输数组中发送uInt8Array
。
structuredClone()
创建对象的深层副本时,也可以使用传输。克隆操作之后,传输的资源将被移动而不是复制到克隆对象。const original = new Uint8Array(1024);
const clone = structuredClone(original);
console.log(original.byteLength); // 1024
console.log(clone.byteLength); // 1024
original[0] = 1;
console.log(clone[0]); // 0
// Transferring the Uint8Array 会报异常,因为 它不是一个可传输(转移)对象
const transferred = structuredClone(original, {transfer: [original]});
// We can transfer Uint8Array.buffer.
const transferred = structuredClone(original, { transfer: [original.buffer] });
console.log(transferred.byteLength); // 1024
console.log(transferred[0]); // 1
//转移后 Uint8Array.buffer 不可再用
console.log(original.byteLength); // 0
ImageBitmap
,再传给主线程,主线程使用画布画出来。// 主线程代码
// 获取图片文件
const file = document.getElementById("file").files[0];
// 创建 FileReader 对象
const reader = new FileReader();
// 创建 Worker 对象
const worker = new Worker("worker.js");
// 为 FileReader 对象添加一个 onload 事件监听器,用于在文件读取完成后执行
reader.onload = (e) => {
// 获取 ArrayBuffer 类型的数据
const buffer = e.target.result;
// 将 ArrayBuffer 类型的数据作为可转移对象传递给 Web Worker
worker.postMessage(buffer, [buffer]);
};
// 为 Worker 对象添加一个 onmessage 事件监听器,用于接收 Web Worker 传递的数据
worker.onmessage = (e) => {
// 获取 ImageBitmap 类型的数据
const bitmap = e.data;
// 获取 canvas 元素
const canvas = document.getElementById("canvas");
// 获取 canvas 的绘图上下文
const ctx = canvas.getContext("2d");
// 将 ImageBitmap 类型的数据绘制到 canvas 上
ctx.drawImage(bitmap, 0, 0);
};
// 以 ArrayBuffer 类型的方式读取图片文件
reader.readAsArrayBuffer(file);
子线程代码:
// worker.js
// 为 Worker 添加一个 onmessage 事件处理函数,用于接收主线程传递的数据
onmessage = (e) => {
// 获取 ArrayBuffer 类型的数据
const buffer = e.data;
// 将 ArrayBuffer 类型的数据转换为 ImageBitmap 类型的数据
createImageBitmap(buffer).then((bitmap) => {
// 将 ImageBitmap 类型的数据作为可转移对象传递回主线程
postMessage(bitmap, [bitmap]);
});
};
有哪些可转移对象
-
ArrayBuffer
-
MessagePort
-
ReadableStream
-
WritableStream
-
TransformStream
-
WebTransportReceiveStream
-
WebTransportSendStream
-
AudioData
-
ImageBitmap
-
VideoFrame
-
OffscreenCanvas
-
RTCDataChannel
嵌入式工作线程
<script type="text/js-worker">
的方式来嵌入工作线程的代码块,这段代码块自定义了 type
属性,这个属性浏览器不认识,所以它不能被js引擎解析,这样就可以了。<script type="text/js-worker">
function handleWorkerTask(){
// 处理工作线程的任务
return "任务处理完成";
}
onmessage = (event) => {
let result = handleWorkerTask();
postMessage(result);
};
</script>
text/javascript
,所以可以被JS引擎解析。 <script>
//将上面的代码块脚本生成二进制blob对象
const blob = new Blob(
Array.prototype.map.call(
document.querySelectorAll("script[type='text/js-worker']"),
(script) => script.textContent,
),
{ type: "text/javascript" },
);
// 为blob二进制对象生成URL,并创建工作线程
document.worker = new Worker(window.URL.createObjectURL(blob));
document.worker.onmessage = (event) => {
console.log("从子线程中接收到消息");
};
// 启动工作线程.
window.onload = () => {
document.worker.postMessage("");
};
</script>
如上代码,其工作机制是先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。
工作线程的调试
大多数浏览器都允许您在 JavaScript 调试器中调试 Web Worker,其方式与调试主线程完全相同!例如,Firefox 和 Chrome 都会列出主线程和活动工作线程的 JavaScript 源文件,并且可以打开所有这些文件来设置断点和日志点。
Worker 线程嵌套
指的是在一个工作线程中,可以继续创建新的工作线程。这个使用方式是一样的。
结语
最后,整理一下工作线程可以调用的API和不能调用的API,并结合当前vue框架中的网络请求如何使用工作线程做个简单的说明:
在工作线程中,可以运行任何标准的 JavaScript 代码,不过有一些例外情况。给大家整理了一份工作线程中支持和不支持的 API 表格,供参考:
支持的API | 不支持的API |
---|---|
基本的 JavaScript 语法和内置对象,如 Array, Object, Math, JSON, Promise 等 | DOM API,如 document, window, Element, Event 等 |
Web Worker API,如 Worker, MessagePort, MessageChannel, BroadcastChannel 等 | BOM API,如 navigator, location, history, screen 等 |
Web Storage API,如localStorage, sessionStorage 等 | HTML API,如 Audio, Video, Image, Canvas, FormData 等 |
IndexedDB API,如IDBFactory, IDBRequest, IDBTransaction 等 | Web Audio API,如AudioContext, AudioNode, AudioParam 等 |
Fetch API,如fetch, Request, Response, Headers 等 | WebRTC API,如 RTCPeerConnection, RTCDataChannel, MediaStream 等 |
WebSocket API,如WebSocket, CloseEvent, MessageEvent 等 | Service Worker API,如ServiceWorker, ServiceWorkerRegistration, ServiceWorkerGlobalScope 等 |
File API,如 Blob, File, FileReader, URL 等 | Notification API,如Notification, NotificationEvent 等 |
Stream API,如ReadableStream, WritableStream, TransformStream 等 | Geolocation API,如Geolocation, Position, Coordinates 等 |
Crypto API,如crypto, CryptoKey, SubtleCrypto 等 | Sensor API,如Sensor, Accelerometer, Gyroscope 等 |
Performance API,如 performance,PerformanceEntry, PerformanceObserver 等 | Alert , Confirm 等 |
Console API,如console, Console 等 | |
Timer API,如setTimeout, setInterval, clearTimeout, clearInterval 等 | |
Navigator |
在vue项目中,我们通常使用 axios 作为网络调用组件,它是一个基于 Promise 的 HTTP 库,可以用来发送请求和接收响应,由于它使用了 Fetch API 和 XMLHttpRequest API,所以它在工作线程中是支持的,但是需要注意以下几点:
-
在工作线程中使用 axios 时,需要先使用 importScripts() 函数导入 axios 的脚本文件,例如:
importScripts("https://unpkg.com/axios/dist/axios.min.js");
-
在工作线程中使用 axios 时,不能使用拦截器、取消请求、进度条等功能,因为它们依赖于 window 对象或 DOM API,而这些在工作线程中是不可用的。
-
在工作线程中使用 axios 时,需要注意跨域请求的问题,因为工作线程的 origin 是不同于主线程的,所以可能会遇到 CORS 的错误。为了解决这个问题,可以在请求头中设置 Origin 为主线程的 origin,或者在服务器端配置允许工作线程的 origin 访问。
如果你想判断一个 API 是否在工作线程中支持,你可以使用 typeof 操作符来检查它是否存在,比如:if (typeof document === "undefined")
或 if (typeof fetch === "function")
往期文章推荐
原文始发于微信公众号(小核桃编程):前端多线程web worker详细教程,请保存~
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/216072.html