从单线程到多线程:项目实战web Worker线程使用总结

0 阅读7分钟

image.png

从单线程到多线程:项目实战web Worker线程使用总结

前言

        在最近的开发过程中,我频繁地遇到了需要处理大量列表数据、大屏展示的数据以及Canvas数据的任务。在这些情况下,JavaScript的单线程特性成了一个瓶颈——每当执行复杂的数据处理任务时,网页就会出现堵塞,必须等待数据处理完毕才能继续加载页面。这不仅导致了其他DOM元素在此期间无法操作,还使得整个网页变得卡顿,严重影响了用户体验。         为了解决这个问题,我决定采用Web Worker技术来优化数据处理流程。通过使用Web Worker,可以在后台线程中执行耗时的数据处理任务,从而避免阻塞主线程。这样一来,即使面对长时间运行的数据处理任务,也能确保网页的流畅性和响应性,极大地提升了用户体验。

Web Worker 是什么

Web WorkerHTML5 引入的一种技术,它允许在后台运行 JavaScript 代码而不影响页面的性能。JavaScript 历来是单线程执行的,这意味着所有的任务都在同一个线程上顺序执行。如果某个操作需要较长时间完成(比如大数据量处理、复杂计算等),那么整个页面可能会暂时冻结,直到该操作完成。 Web Worker 提供了一种方式让开发者可以在后台创建一个或多个额外的 JavaScript线程,这些线程可以与主线程并行工作,从而不会阻塞用户界面。通过这种方式,Web Worker 可以帮助提升 Web 应用程序的响应速度和用户体验。

Web Worker 作用与用处

由于web Worker允许在后台执行耗时任务,这使前端UI不会因为复杂的计算出现卡顿,比如在大量的数据分析,图像处理,像素计算时候,可以将这些数据处理逻辑交给web Worker线程来处理。 还可以支持长期运行的任务,那些可能需要很长时间才能完成的操作,比如文件读取,网络请求等都可以使用webWorker线程在后台持续运行,而不会干扰到用户的其他交互行为。

  1. canvas图像滤镜等处理。
  2. 数据清洗聚合等。
  3. 大量排程数据处理。
  4. 导出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使用注意事项

通过上面的基本使用,需要注意以下四个问题:

  1. webworker不能使用本地文件,必须是网络上的同源文件。
  2. webworker不能使用window上的dom操作,也不能获取dom对象,dom相关的东西只有主线程有,只能做一些计算相关的操作。
  3. 有的东西是无法通过主线程传递给子线程的,比如方法,dom节点,一些对象里的特殊设置(freeze,getter,setter这些),所以vue的响应式对象不能传递的
  4. 模块的引入

webworeker的常见应用

因为webworker的限制,就别想多线程渲染dom了,因为它根本无法创建dom,所以vuereact框架没有考虑webworkerwebworker的常见主要是耗时的计算,随着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)
})

效果: 在这里插入图片描述