从单线程到多线程:项目实战web Worker线程使用总结
目录
- 从单线程到多线程:项目实战web Worker线程使用总结
- 前言
- Web Worker 是什么
- Web Worker 作用与用处
- js单线程问题
- 异步处理一个耗时计算
- 创建Web Worker
- 第一步、我们需要创建一个单独的JavaScript文件
- 第二步、在主线程中启动Worker
- web Worker主子线程监听通信
- 主线程发送消息给Worker
- Worker线程接收并处理消息
- Worker发送回主线程
- 主线程接收Worker消息
- web Worker 错误监听信息
- 在主线程中监听Worker错误
- 在Worker内部抛出错误
- 关闭 web Worker
- 在主线程中关闭Worker
- 在Worker内部自我终止
- 主线程和`Worker`线程可传递哪些类型数据
- webworker兼容性问题
- 模块的引入
- 基本使用
- webworker使用注意事项
- webworeker的常见应用
- 案例一、解决canvas滤镜处理卡顿问题
- 案例而、解决十万条数据导出excel表格卡顿问题
从单线程到多线程:项目实战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-inputstyle="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.datafor (let i = 0; i < data.length; i += 4) {// 增加多余循环 6480000*100for (let j = 0; j < 100; j++) {// 每个像素有四个值: R, G, B, Alet avg = (data[i] + data[i + 1] + data[i + 2]) / 3; // 计算灰度平均值data[i] = avg; // Reddata[i + 1] = avg; // Greendata[i + 2] = avg; // Blue}}self.postMessage(data);}
})
使用之前的效果:明显堵塞
使用之后的效果:
案例而、解决十万条数据导出excel表格卡顿问题
当前有10W条数据,需要进行导出excel文件操作,正常点击导出会出现页面堵塞卡顿,无法操作其他DOM,引入webWorker的情况下就不会出现类似此情况。
<template><div><div style="display: flex"><el-inputstyle="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)
})
效果: