Web Worker基础概念 & 图片滤镜处理实际应用 -- Vue3

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 URLimport.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生成,此处不赘述 */
}
相关推荐
paopaokaka_luck23 分钟前
婚纱摄影管理系统(发送邮箱、腾讯地图API、物流API、webSocket实时聊天、协同过滤算法、Echarts图形化分析)
vue.js·spring boot·后端·websocket·算法·echarts
一只小风华~25 分钟前
JavaScript 函数
开发语言·前端·javascript·ecmascript·web
仰望星空的凡人2 小时前
【JS逆向基础】数据库之MongoDB
javascript·数据库·python·mongodb
若梦plus3 小时前
Nuxt.js基础与进阶
前端·vue.js
樱花开了几轉3 小时前
React中为甚么强调props的不可变性
前端·javascript·react.js
若梦plus3 小时前
Vue服务端渲染
前端·vue.js
Mr...Gan4 小时前
VUE3(四)、组件通信
前端·javascript·vue.js
给力学长4 小时前
自习室预约小程序的设计与实现
java·数据库·vue.js·elementui·小程序·uni-app·node.js
楚轩努力变强6 小时前
前端工程化常见问题总结
开发语言·前端·javascript·vue.js·visual studio code
前端开发爱好者6 小时前
只有 7 KB!前端圈疯传的 Vue3 转场动效神库!效果炸裂!
前端·javascript·vue.js