Web Worker基础概念 & 图片滤镜处理实际应用 -- Vue3
1.摘要
本文主要阐述一些Web Worker的基本概念,并且结合Web Worker在Vue3项目中实现简易的图片滤镜处理Demo,用于加深了解Web Worker在实际生产开发过程中的运用
2.基础概念
- 定义:运行在后台的 JavaScript,独立于其他脚本,用于处理需要大量计算的耗时任务,避免阻塞主线程导致页面卡顿
- 判断是否使用标准:运算超过16ms(一帧的时间);用户操作后出现明显卡顿或延迟
- 适用情境
- 大数据处理:处理大型数组、矩阵运算、大数据集分析
- 图像/视频处理:像素级操作、滤镜应用、图像识别
- 复杂算法:加密解密、压缩解压、机器学习推理
- 长时间运行任务:日志分析、数据排序、复杂计算
- 实时数据处理:WebSocket 数据处理、传感器数据分析
- 注意事项
- 数据传输:Worker 和主线程之间通过消息传递数据,数据会被结构化克隆算法处理(类似深拷贝),大数据传输会有性能开销
- 启动成本:Worker 初始化需要50-200ms
- 内存占用:每个 Worker 都有独立的内存空间,创建过多 Worker 会增加内存消耗
- 生命周期:Worker 需要显式终止
worker.terminate()
/self.close()
,否则会持续存在,造成内存泄漏 - 调试:Worker 脚本有独立的调试上下文,在开发者工具中可以单独查看
- 兼容性:现代浏览器都支持 WebWorker,但在某些特殊环境(如某些移动浏览器)可能需要降级处理
3.与主线程区别
特性 | 主线程 JS 代码 | WebWorker JS 代码 |
---|---|---|
DOM 访问 | 可以访问 DOM | 不能访问 DOM |
全局对象 | window | self 或 this |
UI 操作 | 可以直接操作 UI | 不能直接操作 UI |
阻塞影响 | 阻塞会导致页面卡顿 | 不会阻塞主线程 |
通信方式 | 直接调用函数 | 通过 postMessage 通信 |
引入脚本 | 直接 <script> 标签 |
使用 importScripts() |
错误处理 | 常规 try-catch | 需要通过 onerror 监听 |
内存 | 共享主线程内存 | 独立内存空间 |
Web API 支持 | 支持全部 API | 有限支持 (如不支持 localStorage) |
不可用的常用API:document、Element、alert、confirm、localStorage、sessionStorage、WebSocket worker与主线程对比:worker能够并行处理数据,多worker并行时需注意 worker 数量 ≤ CPU 核心数
4.相关API
worker.postMessage({ type: 'render', canvas: offscreen }, [offscreen])
- 作用:向worker传递信息
- 第一个参数:发送给 Worker 的消息内容(普通对象,会被结构化克隆)
- 第二个参数:数组,声明哪些对象需要转移所有权(ArrayBuffer 、offscreen等均需声明)
- 所有权转移:转移不可逆,即使进程销毁,如
<canvas>
永久失去所有权
workerFilter.onmessage = (e) => {}
:e.data
为从worker中接收到的信息内容workerDrawer.terminate()
:用于终止worker,避免内存泄漏;worker对象仍可访问,但后续其他调用无效createImageBitmap(file)
- 作用:将图像源复制转化为 ImageBitmap ,以便在worker交互过程中可直接转移所有权,避免结构化克隆消耗
- 参数:图像源支持类型包括
<img>
元素、<canvas>
元素、<video>
元素(当前帧)、Blob/File 对象(如用户上传的图片)、ImageData 对象、其他 ImageBitmap 对象 - 注意: ImageBitmap 需要调用
.close()
显式关闭,否则可能内存泄漏
new Worker(new URL('../../utils/worker/filterWorker.js', import.meta.url));
- 作用:根据worker文件生成对应的worker线程
- 注意:worker文件的路径需是网站上的实际路径,因此需要结合
new URL
与import.meta.url
指向
canvas.transferControlToOffscreen()
- 作用:主线程将canvas转化为离屏canvas后,将其控制权权转移给worker进行处理,同时避免结构化克隆消耗
- 注意:转移控制权前主线程不得调用
canvas.getContext("2d")
;转移控制权后主线程不得再次对同一canvas调用该函数
new OffscreenCanvas(width, height)
:在worker中创建离屏canvas用于图像处理offscreenCanvas.transferToImageBitmap
:直接转移 OffscreenCanvas 的底层数据到 ImageBitmap ,转移后 OffscreenCanvas 清空,但相较于createImageBitmap
性能更高
5.离屏canvas渲染
- 核心逻辑:主线程通过
transferControlToOffscreen()
将可见的<canvas>
控制权转移给 Worker;Worker 直接操作离屏 Canvas,绘制结果自动同步到页面对应的<canvas>
- 优势:零拷贝显示(直接由浏览器合成器 GPU 处理,性能最优),无需手动回传数据
- 适用情境:实时可视化(如动态图表、粒子效果)、游戏渲染(每一帧都需要更新屏幕)、任何需要低延迟显示的图形
javascript
/* 主线程 */
const canvas = ref(null); // 绑定canvas元素
let workerDrawer = null
const handleWorkerDrawFilter = () => {
let message = {
type: 'drawFilter',
filterType: 'sepia',
algorithm: algorithm.value,
}
const transferArray = []
if(!workerDrawer) {
workerDrawer = new Worker(new URL('../../utils/worker/filterWorker.js', import.meta.url))
const mainCanvas = canvas.value.transferControlToOffscreen()
message.mainCanvas = mainCanvas
transferArray.push(mainCanvas)
// 统一处理消息
workerDrawer.onmessage = (e) => {
if (e.data.type === 'drawResult') {
console.log('worker图片绘制完成')
}
if (e.data.type === 'drawFilterResult') {
console.log('worker滤镜绘制完成')
}
};
}
workerDrawer.postMessage(message, transferArray)
}
/* Worker 线程 */
let mainCanvas = null // 存储传入的canvas用于后续操作
onmessage = async (e) => {
const data = e.data
if (data.type === 'drawFilter') {
if(data.mainCanvas) mainCanvas = data.mainCanvas
drawFilter(data.filterType, data.algorithm);
postMessage(
{ type: 'drawFilterResult' },
);
}
};
const drawFilter = (filterType, algorithm) => {
const ctx = mainCanvas.getContext("2d")
const imageData = ctx.getImageData(0, 0, mainCanvas.width, mainCanvas.height);
const data = imageData.data;
if(algorithm == 'simple') simpleFilterCreate(data, filterType)
else difficultFilterCreate(mainCanvas, data, filterType)
ctx.putImageData(imageData, 0, 0)
}
6.处理canvas渲染数据
- 核心逻辑:Worker 内部创建独立的 OffscreenCanvas,处理图形数据(如滤镜、图像分析);通过
transferToImageBitmap()
将结果转换为轻量级位图,按需传回主线程 - 优势:完全脱离主线程,适合计算密集型任务,灵活控制数据传输时机(非实时场景)
- 适用情境:图像处理(如压缩、滤镜)、物理模拟/数据计算生成中间结果、不需要实时显示的预处理任务
javascript
/* 主线程 */
const canvas = ref(null); // 绑定canvas元素
let workerFilter = null;
const handleWorkerFilter = async () => {
const file = fileList.value[0]?.originFileObj;
const bitmap = await createImageBitmap(file)
if(!workerFilter) {
workerFilter = new Worker(new URL('../../utils/worker/filterWorker.js', import.meta.url));
// 统一处理消息
workerFilter.onmessage = (e) => {
if (e.data.type === 'result') {
const bitmap = e.data.bitmap;
const ctx = canvas.value.getContext('2d')
ctx.drawImage(bitmap, 0, 0);
bitmap.close()
console.log('worker滤镜处理完成');
}
};
}
workerFilter.postMessage({
type: 'process',
bitmap: bitmap,
filterType: 'sepia',
maxWidth: maxWidth.value,
algorithm: algorithm.value,
}, [bitmap]);
};
/* Worker 线程 */
onmessage = async (e) => {
const data = e.data
if (data.type === 'process') {
const bitmap = await applyFilter(data.bitmap, data.filterType, data.maxWidth, data.algorithm);
postMessage(
{ type: 'result', bitmap },
[bitmap]
);
}
};
// 处理滤镜
const applyFilter = async(bitmap, filterType, maxWidth, algorithm) => {
const scaleFactor = maxWidth / bitmap.width;
const scaledHeight = bitmap.height * scaleFactor;
const offscreen = new OffscreenCanvas(maxWidth, scaledHeight)
const ctx = offscreen.getContext("2d")
ctx.drawImage(bitmap, 0, 0, maxWidth, scaledHeight);
bitmap.close()
const imageData = ctx.getImageData(0, 0, offscreen.width, offscreen.height);
const data = imageData.data;
if(algorithm == 'simple') simpleFilterCreate(data, filterType)
else difficultFilterCreate(offscreen, data, filterType)
ctx.putImageData(imageData, 0, 0)
return offscreen.transferToImageBitmap()
}
7.完整代码
1.主线程代码
html
<template>
<div class="worker">
<a-upload
v-model:file-list="fileList"
:customRequest="customUpload"
name="file"
>
<a-button>
<a-icon name="UploadOutlined"></a-icon>
上传
</a-button>
</a-upload>
<div class="operation">
<a-button @click="handleChangeAlgorithm">切换算法:{{ algorithm == 'simple' ? '简单' : '复杂' }}</a-button>
<a-button @click="handleDraw">主线程绘制图片</a-button>
<a-button @click="handleMainFilter">主线程处理滤镜</a-button>
<a-button @click="handleWorkerFilter">worker处理滤镜</a-button>
<a-button @click="handleWorkerDraw">worker绘制图片</a-button>
<a-button @click="handleWorkerDrawFilter">worker绘制滤镜</a-button>
</div>
<div class="canvas-container">
<canvas id="canvas" ref="canvas"></canvas>
</div>
</div>
</template>
<script>
import { defineComponent, ref, onUnmounted } from "vue";
import { simpleFilterCreate, difficultFilterCreate } from '@/utils/worker/filterWorker'
export default defineComponent({
name: 'Worker',
setup() {
const canvas = ref(null);
const fileList = ref([]);
const maxWidth = ref('')
const algorithm = ref('simple')
const customUpload = ({ onSuccess }) =>{
setTimeout(() => {
onSuccess();
}, 500);
}
const handleChangeAlgorithm = () => {
algorithm.value = algorithm.value == 'simple' ? 'difficult' : 'simple'
}
// 主线程绘制图片
const handleDraw = async () => {
const file = fileList.value[0]?.originFileObj;
const bitmap = await createImageBitmap(file)
const container = document.querySelector('.canvas-container');
const ctx = canvas.value.getContext("2d");
// 设置canvas的宽高
const clientWidth = container.clientWidth;
const scaleFactor = clientWidth / bitmap.width;
const scaledHeight = bitmap.height * scaleFactor;
canvas.value.width = clientWidth;
canvas.value.height = scaledHeight;
maxWidth.value = clientWidth
ctx.drawImage(bitmap, 0, 0, clientWidth, scaledHeight);
bitmap.close()
}
// 主线程绘制滤镜
const handleMainFilter = async () => {
const ctx = canvas.value.getContext('2d')
const imageData = ctx.getImageData(0, 0, canvas.value.width, canvas.value.height);
const data = imageData.data;
if(algorithm.value == 'simple') simpleFilterCreate(data, 'sepia')
else difficultFilterCreate(canvas.value, data, 'sepia')
const processedBitmap = await createImageBitmap(imageData)
ctx.drawImage(processedBitmap, 0, 0);
processedBitmap.close()
console.log('主线程滤镜处理完成');
}
// worker处理滤镜数据
let workerFilter = null;
const handleWorkerFilter = async () => {
const file = fileList.value[0]?.originFileObj;
const bitmap = await createImageBitmap(file)
if(!workerFilter) {
workerFilter = new Worker(new URL('../../utils/worker/filterWorker.js', import.meta.url));
// 统一处理消息
workerFilter.onmessage = (e) => {
if (e.data.type === 'result') {
const bitmap = e.data.bitmap;
const ctx = canvas.value.getContext('2d')
ctx.drawImage(bitmap, 0, 0);
bitmap.close()
console.log('worker滤镜处理完成');
}
};
}
workerFilter.postMessage({
type: 'process',
bitmap: bitmap,
filterType: 'sepia',
maxWidth: maxWidth.value,
algorithm: algorithm.value,
}, [bitmap]);
};
const createWorkerDrawer = (message, transferArray) => {
workerDrawer = new Worker(new URL('../../utils/worker/filterWorker.js', import.meta.url))
const mainCanvas = canvas.value.transferControlToOffscreen()
message.mainCanvas = mainCanvas
transferArray.push(mainCanvas)
// 统一处理消息
workerDrawer.onmessage = (e) => {
if (e.data.type === 'drawResult') {
console.log('worker图片绘制完成')
}
if (e.data.type === 'drawFilterResult') {
console.log('worker滤镜绘制完成')
}
};
}
let workerDrawer = null
// worker绘制图片
const handleWorkerDraw = async () => {
const file = fileList.value[0]?.originFileObj;
const bitmap = await createImageBitmap(file)
const container = document.querySelector('.canvas-container');
const clientWidth = container.clientWidth;
const transferArray = [bitmap]
let message = {
type: 'draw',
bitmap,
clientWidth
}
if(!workerDrawer) createWorkerDrawer(message, transferArray)
workerDrawer.postMessage(message, transferArray)
}
// worker绘制滤镜
const handleWorkerDrawFilter = () => {
let message = {
type: 'drawFilter',
filterType: 'sepia',
algorithm: algorithm.value,
}
const transferArray = []
if(!workerDrawer) createWorkerDrawer(message, transferArray)
workerDrawer.postMessage(message, transferArray)
}
onUnmounted(() => {
if(workerFilter) {
workerFilter.terminate()
workerFilter = null
}
if(workerDrawer) {
workerDrawer.terminate()
workerDrawer = null
}
})
return {
algorithm,
fileList,
canvas,
customUpload,
handleChangeAlgorithm,
handleMainFilter,
handleWorkerDrawFilter,
handleWorkerFilter,
handleDraw,
handleWorkerDraw
}
},
})
</script>
<style lang="less" scoped>
.worker {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.operation {
margin: 20px 0;
display: flex;
gap: 10px;
}
.canvas-container {
width: 600px;
border: 1px solid #ccc;
#canvas {
width: 100%;
height: auto;
}
}
}
</style>
2.worker代码
javascript
let mainCanvas = null
onmessage = async (e) => {
const data = e.data
if (data.type === 'process') {
const bitmap = await applyFilter(data.bitmap, data.filterType, data.maxWidth, data.algorithm);
postMessage(
{ type: 'result', bitmap },
[bitmap]
);
}
if (data.type === 'drawFilter') {
if(data.mainCanvas) mainCanvas = data.mainCanvas
drawFilter(data.filterType, data.algorithm);
postMessage(
{ type: 'drawFilterResult' },
);
}
if (data.type === 'draw') {
if(data.mainCanvas) mainCanvas = data.mainCanvas
draw(data.bitmap, data.clientWidth)
postMessage(
{ type: 'drawResult' },
);
}
};
// 绘制图片
const draw = (bitmap, clientWidth) => {
const ctx = mainCanvas.getContext("2d")
const scaleFactor = clientWidth / bitmap.width;
const scaledHeight = bitmap.height * scaleFactor;
mainCanvas.width = clientWidth;
mainCanvas.height = scaledHeight;
ctx.drawImage(bitmap, 0, 0, clientWidth, scaledHeight);
bitmap.close()
}
// 绘制滤镜
const drawFilter = (filterType, algorithm) => {
const ctx = mainCanvas.getContext("2d")
const imageData = ctx.getImageData(0, 0, mainCanvas.width, mainCanvas.height);
const data = imageData.data;
if(algorithm == 'simple') simpleFilterCreate(data, filterType)
else difficultFilterCreate(mainCanvas, data, filterType)
ctx.putImageData(imageData, 0, 0)
}
// 处理滤镜
const applyFilter = async(bitmap, filterType, maxWidth, algorithm) => {
const scaleFactor = maxWidth / bitmap.width;
const scaledHeight = bitmap.height * scaleFactor;
const offscreen = new OffscreenCanvas(maxWidth, scaledHeight)
const ctx = offscreen.getContext("2d")
ctx.drawImage(bitmap, 0, 0, maxWidth, scaledHeight);
bitmap.close()
const imageData = ctx.getImageData(0, 0, offscreen.width, offscreen.height);
const data = imageData.data;
if(algorithm == 'simple') simpleFilterCreate(data, filterType)
else difficultFilterCreate(offscreen, data, filterType)
ctx.putImageData(imageData, 0, 0)
return offscreen.transferToImageBitmap()
}
export const simpleFilterCreate = (data, filterType) => {
switch(filterType) {
case 'grayscale':
// 灰度滤镜
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
break;
case 'sepia':
// 复古滤镜
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
data[i] = Math.min(255, (r * 0.393) + (g * 0.769) + (b * 0.189));
data[i + 1] = Math.min(255, (r * 0.349) + (g * 0.686) + (b * 0.168));
data[i + 2] = Math.min(255, (r * 0.272) + (g * 0.534) + (b * 0.131));
}
break;
case 'invert':
// 反色滤镜
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // R
data[i + 1] = 255 - data[i + 1]; // G
data[i + 2] = 255 - data[i + 2]; // B
}
break;
}
}
export const difficultFilterCreate = (canvas, data, filterType) => {
/* 一种复杂的图像滤镜处理方法,主要用于体现主线程阻塞效果,可自行AI生成,此处不赘述 */
}