目录
从单线程到多线程:项目实战web Worker线程使用总结
前言
在最近的开发过程中,我频繁地遇到了需要处理大量列表数据、大屏展示的数据以及Canvas
数据的任务。在这些情况下,JavaScript
的单线程特性成了一个瓶颈——每当执行复杂的数据处理任务时,网页就会出现堵塞,必须等待数据处理完毕才能继续加载页面。这不仅导致了其他DOM
元素在此期间无法操作,还使得整个网页变得卡顿,严重影响了用户体验。
为了解决这个问题,我决定采用Web Worker
技术来优化数据处理流程。通过使用Web Worker
,可以在后台线程中执行耗时的数据处理任务,从而避免阻塞主线程。这样一来,即使面对长时间运行的数据处理任务,也能确保网页的流畅性和响应性,极大地提升了用户体验。
Web Worker 是什么
Web Worker
是 HTML5
引入的一种技术,它允许在后台运行 JavaScript
代码而不影响页面的性能。JavaScript
历来是单线程执行的,这意味着所有的任务都在同一个线程上顺序执行。如果某个操作需要较长时间完成(比如大数据量处理、复杂计算等),那么整个页面可能会暂时冻结,直到该操作完成。
Web Worker
提供了一种方式让开发者可以在后台创建一个或多个额外的 JavaScript
线程,这些线程可以与主线程并行工作,从而不会阻塞用户界面。通过这种方式,Web Worker
可以帮助提升 Web
应用程序的响应速度和用户体验。
Web Worker 作用与用处
由于web Worker允许在后台执行耗时任务,这使前端UI不会因为复杂的计算出现卡顿,比如在大量的数据分析,图像处理,像素计算时候,可以将这些数据处理逻辑交给web Worker线程来处理。
还可以支持长期运行的任务,那些可能需要很长时间才能完成的操作,比如文件读取,网络请求等都可以使用webWorker线程在后台持续运行,而不会干扰到用户的其他交互行为。
- canvas图像滤镜等处理。
- 数据清洗聚合等。
- 大量排程数据处理。
- 导出10W+数据为Excel表格。
js单线程问题
众所周知,js一直被说不擅长计算,计算是同步的,大规模计算会让js主线程阻塞,主线程阻塞的结果就是界面完全卡死。异步只是把任务发布出去等着,后面还是会拉到主线程执行的,异步不可能在异步列队自己执行,所以一个耗时很高的操作无论你做不做异步,始终会卡死页面。
异步处理一个耗时计算
假设这个耗时任务必须消耗两秒去计算,我们主线程必须消耗两秒去计算,主线程永远不可能躲开这两秒的计算时间,只能通过切片等操作,把这两秒切分成好几个几十毫秒,一点一点计算来解决卡顿的问题。
webworker是真正的多线程,开一条支线,让他计算,然后把结果返回
创建Web Worker
第一步、我们需要创建一个单独的JavaScript文件
我们需要创建一个单独的JavaScript文件,这个文件将在Worker线程中运行。
// worker.js
self.onmessage = function(event) {
console.log("收到消息:" + event.data);
let result = "Worker返回结果: " + event.data.toUpperCase();
postMessage(result);
};
第二步、在主线程中启动Worker
const worker = new Worker('worker.js');
worker.postMessage("Hello Worker");
worker.onmessage = function(event) {
console.log("来自Worker的消息:" + event.data);
};
web Worker主子线程监听通信
Web Worker
使用postMessage()
和 onmessage
进行线程间通信。这种通信方式基于事件模型。
主线程发送消息给Worker
worker.postMessage("Hello from main thread");
Worker线程接收并处理消息
self.onmessage = function(event) {
console.log("Worker收到消息:" + event.data);
// 处理逻辑...
self.postMessage("Worker已处理完成");
};
Worker发送回主线程
self.postMessage("这是Worker的返回值");
主线程接收Worker消息
worker.onmessage = function(event) {
console.log("主线程收到Worker消息:" + event.data);
};
web Worker 错误监听信息
为了确保Worker
的稳定运行,我们可以监听错误信息。
在主线程中监听Worker错误
worker.onerror = function(error) {
console.error("Worker发生错误:" + error.message);
console.error("错误文件:" + error.filename);
console.error("错误行号:" + error.lineno);
};
在Worker内部抛出错误
// worker.js
self.onmessage = function(event) {
try {
// 模拟错误
throw new Error("这是一个测试错误");
} catch (e) {
postMessage("捕获到错误:" + e.message);
}
};
关闭 web Worker
当不再需要Worker时,应该及时关闭它以释放资源。
在主线程中关闭Worker
worker.terminate(); // 立即终止Worker
在Worker内部自我终止
self.close(); // Worker线程内部调用
主线程和Worker
线程可传递哪些类型数据
以下是支持的数据类型:
- 基本类型(String, Number, Boolean)
- 数组(Array)
- 对象(Object)
- Date对象
- Map / Set
- ArrayBuffer
- ImageData
- Transferable对象(如ArrayBuffer)
webworker兼容性问题
模块的引入
非es6
的情况下,必须是网络地址引入,这个网络地址可以跨域
importScripts('http://localhost:9528/process.js');
console.log("a1", a1)
es6
的情况下,在new Worker()
的第二个参数指定module类型
this.worker1 = new Worker("http://localhost:9528/ai-screen/list.js", {type: "module"});
加上后可以写成,worker.js
可写成
import { a1 } from 'http://localhost:9528/process.js'
console.log("a1", a1)
基本使用
主线程监听worker1
发过来的消息,子线程监听主线程发过来的消息,主线程发送一个消息给子线程,子线程打印"收到"并将返回结果给主线程,主线程接收到结果后打印"辛苦你了worker1
:子线程返回的结果结果"
webworker使用注意事项
通过上面的基本使用,需要注意以下四个问题:
- webworker不能使用本地文件,必须是网络上的同源文件。
- webworker不能使用window上的dom操作,也不能获取dom对象,dom相关的东西只有主线程有,只能做一些计算相关的操作。
- 有的东西是无法通过主线程传递给子线程的,比如方法,dom节点,一些对象里的特殊设置(freeze,getter,setter这些),所以vue的响应式对象不能传递的
- 模块的引入
webworeker的常见应用
因为webworker
的限制,就别想多线程渲染dom
了,因为它根本无法创建dom
,所以vue
和react
框架没有考虑webworker
,webworker
的常见主要是耗时的计算,随着webgl,canvas的能力加入,web前端越来越多的可视化操作,比如在线滤镜,在线绘图,web游戏等,这些都是非常消耗计算的,一些后台管理也会涉及到一些,最常见的就是一些电子表单,大量的数据大量的计算,比如10W条数据导出为excel表格。
案例一、解决canvas滤镜处理卡顿问题
案例一、使用canvas进行图片过滤,图片上放有个input标签,图片过滤需要处理很多像素点数据,由于js单线程机制,会导致在图片过滤的过程中进行堵塞,网页就会卡顿,直到每个像素点都完全过滤好为止,接下来通过webWorker进行处理,进行过滤的同时其他dom元素不会被卡顿。
<template>
<div>
<div style="display: flex">
<el-input
style="width: 170px"
placeholder="请输入"
v-model="inputValue"
></el-input>
<el-button @click="imghandler">过滤</el-button>
</div>
<canvas id="imgCanvas" width="1800" height="900"> </canvas>
</div>
</template>
<script>
import { Input } from "element-ui";
export default {
data() {
return {
inputValue: "",
worker1: null,
canvas: null,
myContext: null,
img: null,
imageData: null,
};
},
created() {
this.$nextTick(() => {
let img = new Image();
img.src = require("../../src/assets/daskBg.jpg");
img.onload = () => {
// 使用箭头函数保持外部的 `this` 不变
this.canvas = document.getElementById("imgCanvas");
this.myContext = this.canvas.getContext("2d");
this.myContext.drawImage(img, 0, 0, 1800, 900);
this.imageData = this.myContext.getImageData(0, 0, 1800, 900);
};
});
},
methods: {
// 滤镜函数 - 灰色滤镜
imghandler() {
if (!this.imageData) {
console.error("Image data is not available.");
return;
}
let imageData = this.imageData;
let data = imageData.data;
let worker1 = new Worker("http://localhost:9528/imageProcess.js");
worker1.postMessage(data);
worker1.addEventListener("message", (event) => {
const processedImageData = new ImageData(
new Uint8ClampedArray(event.data),
imageData.width,
imageData.height
);
this.myContext.putImageData(processedImageData, 0, 0);
});
// 将修改后的图像数据放回画布上
},
},
};
</script>
<style scoped></style>
imageProcess.js
过滤处理文件:
self.addEventListener("message", function (e) {
if (e.data.length > 0) {
let data = e.data
for (let i = 0; i < data.length; i += 4) {
// 增加多余循环 6480000*100
for (let j = 0; j < 100; j++) {
// 每个像素有四个值: R, G, B, A
let avg = (data[i] + data[i + 1] + data[i + 2]) / 3; // 计算灰度平均值
data[i] = avg; // Red
data[i + 1] = avg; // Green
data[i + 2] = avg; // Blue
}
}
self.postMessage(data);
}
})
使用之前的效果:明显堵塞
使用之后的效果:
案例而、解决十万条数据导出excel表格卡顿问题
当前有10W条数据,需要进行导出excel文件操作,正常点击导出会出现页面堵塞卡顿,无法操作其他DOM,引入webWorker的情况下就不会出现类似此情况。
<template>
<div>
<div style="display: flex">
<el-input
style="width: 170px"
placeholder="请输入"
v-model="inputValue"
></el-input>
<el-button @click="importClick">导出</el-button>
</div>
</div>
</template>
<script>
import { Input } from "element-ui";
import { writeFile, utils } from "xlsx";
export default {
data() {
return {
inputValue: "",
worker1: null,
arr: [],
};
},
created() {
this.$nextTick(() => {
this.worker1 = new Worker("http://localhost:9528/work.js");
this.worker1.onmessage = (e) => {
writeFile(e.data, "test.xlsx");
};
this.arr = [];
});
},
methods: {
importClick() {
this.worker1.postMessage("");
},
},
};
</script>
<style scoped></style>
work.js
importScripts("http://localhost:9528/xlsx.js")
let arr = []
for (let i = 0; i < 100000; i++) {
arr.push({
id: i,
name: `name${i}`,
age: i,
sex: i % 2 === 0 ? "男" : "女",
a: i * 2,
b: i / 2,
c: 1 + 2,
d: 11,
e: 123,
f: 2323,
});
}
self.addEventListener("message", function (e) {
const sheet = XLSX.utils.json_to_sheet(arr);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, sheet, "sheet1");
this.self.postMessage(workbook)
})
效果: