Vue中使用WebWorker

  • Post author:
  • Post category:vue




安装loader


npm install worker-loader -D


如果直接把worker.js放到public目录下,则不需要安装loader



vue.config.js

const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
    transpileDependencies: true,
    lintOnSave:false, // 关闭eslint检查

    chainWebpack: config => {
        config.module
            .rule('worker')
            .test(/\.worker\.js$/)            // 文件名必须要xxx.worker.js
            .use('worker')
            .loader('worker-loader')
    }
})



HelloWorld.vue

<template>
  <div class="hello">

    <button @click="useWorker">calculate</button>
    {{ result }}
  </div>
</template>

<script>
import Worker from './demo.worker.js'; // this.worker = new Worker(); 方式才需要引入

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },

  data() {
    return {
      result: 0,
      myworker: null
    }
  },

  methods: {
    useWorker() {
      this.worker.postMessage('帮我计算1+1等于多少')
    }
  },

  mounted() {
    // this.worker = new Worker('worker.js', { name : 'myWorker' });     读取public目录下得js文件,可以配置名字,简单粗暴,不需要worker-loader 
    this.worker = new Worker();                                       // 使用上面import进来的js,名字为 demo.worker.worker.js,不可配置,路径相对比较灵活,需要worker-loader 

    // worker的名字,主要在谷歌浏览器 - 控制台 - sources 中体现

    this.worker.onmessage = event => {
      this.result = event.data;
      console.log('主线程收到回复,即将关闭worker连接');

      // this.worker.terminate();
    }
  },

  beforeDestroy() {
    this.worker?.terminate()
  }
}
</script>



demo.worker.js



// self.addEventListener('message', function (e) {
//     console.log(e.data);
//
//     postMessage('等于2');
//
//     // close();
// }, false)

// this.addEventListener('message', function (e) {
//     console.log(e.data);
//
//     postMessage('等于2');
//
//     close();
// }, false)

onmessage = function(e) {
    console.log(e.data);

    postMessage('等于2');

    // close();
}



应用场景

  • 浏览器的JS线程和GUI渲染线程互斥
  • 在主线程跑吃性能的同步任务,GUI渲染线程会挂起,页面不能及时响应渲染
  • 在worker跑时,GUI渲染线程不会被挂起,页面可以正常响应

优势:

  • 避免页面渲染阻塞。用一个worker处理主线程的任务,两者处理时间差不多,worker的优势在于处理过程中不会影响页面的渲染(e.g. 导出时 Element.message组件弹出提示,但是由于JS处理时间过长,导致页面渲染被阻塞,提示不能及时显示)
  • 减少任务处理时间。worker可以有多个(多线程),用多个worker处理主线程的任务时,总的任务时长会减少(e.g. 压缩100张图片)



处理CPU密集型同步任务

// HelloWorld.vue
<template>
  <div class="hello">
    <p>时间戳:{{ timeStamp }}</p>

    <button @click="useWorker">worker运算</button>
    <button @click="calc">主线程运算</button>

    <div>计算从 1 到给定数值的总和</div>
    <input type="number" :value="1000000000" readonly />

    <p>计算结果为:{{ result || '-' }}</p>
  </div>
</template>

<script>
import Worker from './demo.worker.js';

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },

  data() {
    return {
      result: 0,
      timeStamp: 0,
    }
  },

  methods: {
    useWorker() {
      console.time('worker运算');

      this.worker.postMessage({
        command: 'calc',
      })
    },

    calc() {
      let result = 0

      console.time('主线程运算')

      // 计算求和(模拟复杂计算)
      for (let i = 0; i <= 1000000000; i++) {
        result += i
      }

      // 由于是同步计算,在没计算完成之前下面的代码都无法执行
      this.result = result;

      console.timeEnd('主线程运算')
    }
  },

  mounted() {
    this.worker = new Worker();

    this.worker.onmessage = event => {
      this.result = event.data;
      console.timeEnd('worker运算')

      console.log('主线程收到回复,即将关闭worker连接');
      this.worker.terminate();
    }

    this.timer = setInterval(() => {
      this.timeStamp = Date.now();
    })
  },

  beforeDestroy() {
    this.worker?.terminate()
    clearInterval(this.timer)
  }
}
</script>

// demo.worker.js
onmessage = function(e) {
    console.log(e.data);

    switch (e.data.command) {
        case 'calc':
            calc();
            break;
    }
};

function calc() {
    let result = 0

    // 计算求和(模拟复杂计算)
    for (let i = 0; i <= 1000000000; i++) {
        result += i
    }

    // 由于是同步计算,在没计算完成之前下面的代码都无法执行
    postMessage(result);
}
// 控制台输出如下
主线程运算: 1063.4140625 ms
worker运算: 1079.2041015625 ms



导出10万条数据的表格

<template>
  <div class="hello">
    {{ timeStamp }}

    <button @click="getBlob">主线程导出表格</button>
    <button @click="useWorker">worker导出表格</button>
  </div>
</template>

<script>
import Worker from './demo.worker.js';
import { saveAs } from "file-saver";
import Excel from "exceljs";


export default {
  name: 'ExportExcel',
  props: {
    msg: String
  },

  data() {
    return {
      timeStamp: 0,
      timer: null,

      blobData: null
    }
  },

  methods: {
    useWorker() {
      console.time('worker导出')

      this.worker.postMessage({
        // 不能传方法
        // fn: () => this.exportExcel(true)
        // fn: () => {}
        command: 'getBlob',
      })
    },

    getBlob() {
      console.time('主线程导出')

      const workbook = new Excel.Workbook();
      const worksheet = workbook.addWorksheet('My Sheet');

      worksheet.columns = [
        { header: '序号', key: 'index', width: 10 },
        { header: '姓名', key: 'name', width: 32 },
        { header: '性别.', key: 'sex', width: 10, outlineLevel: 1 }
      ];

      for (let i = 1; i <= 100000; i++) {
        worksheet.addRow({index: i, name: '明明', sex: i % 2 > 0 ? '男' : '女'});
      }

      workbook.xlsx.writeBuffer().then(buffer => {
        this.blobData = new Blob([buffer]);
        this.export()
      });
    },

    // 导出
    export() {
      saveAs(this.blobData, `${Date.now()}_feedback.xlsx`);
      console.timeEnd('主线程导出')
      console.timeEnd('worker导出')
    },

  },

  mounted() {
    this.worker = new Worker();

    this.worker.onmessage = event => {
      this.blobData = event.data;
      this.export();

      console.log('主线程收到回复,即将关闭worker连接');
      this.worker.terminate();
    }

    this.timer = setInterval(() => {
      this.timeStamp = Date.now();
    })
  },

  beforeDestroy() {
    this.worker?.terminate()
    clearInterval(this.timer)
  }
}
</script>

import Excel from "exceljs";

onmessage = function(e) {
    console.log(e.data);

    switch (e.data.command) {
        case 'getBlob':
            getBlob()
    }
};

function getBlob() {
    const workbook = new Excel.Workbook();
    const worksheet = workbook.addWorksheet('My Sheet');

    worksheet.columns = [
        { header: '序号', key: 'index', width: 10 },
        { header: '姓名', key: 'name', width: 32 },
        { header: '性别.', key: 'sex', width: 10, outlineLevel: 1 }
    ];

    for (let i = 1; i <= 100000; i++) {
        worksheet.addRow({index: i, name: '明明', sex: i % 2 > 0 ? '男' : '女'});
    }

    workbook.xlsx.writeBuffer().then(buffer => {
        postMessage(new Blob([buffer]));
    });
}

主线程导出: 13290.680908203125 ms
worker导出: 13147.514892578125 ms



压缩100张图片

<template>
  <div class="hello">
    {{ timeStamp }}

    <button @click="compress">主线程压缩</button>
    <button @click="useWorker">worker压缩</button>
    <button @click="useWorkers">多线程worker压缩</button>
  </div>
</template>

<script>
import Worker from './demo.worker.js';

export default {
  name: 'CompressImage',
  props: {
    msg: String
  },

  data() {
    return {
      timeStamp: 0,
      timer: null,

      img: './1.png', // 6.92MB的图片
    }
  },

  methods: {
    // 单线程worker
    async useWorker() {
      console.time('worker压缩');

      // Dom隔离,img元素传不过worker,用fetch读取图片,转成blob,传blob给worker
      // Dom隔离,canvas元素传不不过worker
      // worker没有document,不能创建传统的canvas,可以创建OffscreenCanvas。或者在主线程把传统canvas转成OffscreenCanvas,传给worker
      // 传图像不传图片路径,避免相对路径的图片在worker中读取不到

      const canvas = document.createElement('canvas');
      const offscreenCanvas = canvas.transferControlToOffscreen();

      let blobImgs = [];
      for (let i = 0; i < 100; i++) {
        blobImgs.push(await fetch(this.img).then(res => res.blob()));
      }

      this.worker.postMessage({
        command: 'compress',
        blobImgs,
        canvas: offscreenCanvas
      }, [offscreenCanvas])
    },

    // 多线程worker
    async useWorkers() {
      console.time(`多线程worker压缩`);

      let promiseList = [];

      for (let i = 1; i <= 5; i++) {

        let blobImgs = [];
        for (let i = 0; i < 20; i++) {
          blobImgs.push(await fetch(this.img).then(res => res.blob()));
        }

        const promise = new Promise(resolve => {
          const canvas = document.createElement('canvas');
          const offscreenCanvas = canvas.transferControlToOffscreen();

          const worker = new Worker();
          worker.onmessage = event => {
            resolve(event.data);

            worker.terminate(); // 卸载worker
          }

          worker.postMessage({
            command: 'compress',
            blobImgs,
            canvas: offscreenCanvas
          }, [offscreenCanvas])
        })

        promiseList.push(promise);
      }

      await Promise.all(promiseList);
      console.timeEnd(`多线程worker压缩`);
    },

    // 图片用Image请求
    async compress() {
      console.time('主线程压缩');

      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');

      const img = new Image();
      img.setAttribute('crossOrigin', 'Anonymous');

      let imgs = [];
      for (let i = 0; i < 100; i++) {
        img.src = this.img;

        const blobImg = await new Promise(resolve => {
          img.onload = function() {
            canvas.width = this.width;
            canvas.height = this.height;

            context.drawImage(img, 0, 0, this.width, this.height);

            canvas.toBlob((blob) => {
              resolve(blob)
            }, 'image/jpeg', 0.75)
          };
        })

        imgs.push(blobImg);
      }

      console.timeEnd('主线程压缩');
    },

    // 与worker线程一样,图片用fetch请求
    async compress2() {
      console.time('主线程压缩');

      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');

      let blobImgs = [];
      for (let i = 0; i < 100; i++) {
        blobImgs.push(await fetch(this.img).then(res => res.blob()));
      }

      await Promise.all(blobImgs.map(async blobImg => {
        const img = await createImageBitmap(blobImg);

        canvas.width = img.width;
        canvas.height = img.height;

        context.drawImage(img, 0, 0, img.width, img.height);

        return new Promise(resolve => {
          canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.75)
        })
      }))

      console.timeEnd('主线程压缩');
    },
  },

  mounted() {
    this.worker = new Worker();

    this.worker.onmessage = event => {
      console.timeEnd('worker压缩')

      // this.worker.terminate();
    }

    this.timer = setInterval(() => {
      this.timeStamp = Date.now();
    })
  },

  beforeDestroy() {
    this.worker?.terminate()
    clearInterval(this.timer)
  }
}
</script>

onmessage = function(e) {
    console.log(e.data);

    switch (e.data.command) {
        case 'compress':
            compress(e.data);
            break;
    }
};

 async function compress(data) {
    const { blobImgs, canvas: offscreenCanvas } = data;
    const context = offscreenCanvas.getContext('2d');

     const compressedImgs = await Promise.all(blobImgs.map(async blobImg => {
         const img = await createImageBitmap(blobImg);

         offscreenCanvas.width = img.width;
         offscreenCanvas.height = img.height;

         context.drawImage(img, 0, 0, img.width, img.height);

         // 没有toDataURL方法,用convertToBlob压缩,再用FileReader转成base64
         return await offscreenCanvas.convertToBlob({ type: 'image/jpeg', quality: 0.75 });
     }))

     postMessage(compressedImgs);
}
每次压缩都刷新浏览器,避免浏览器缓存造成的误差

主线程压缩(Image):      46263.44287109375 ms
主线程压缩(fetch):       24482.360107421875 ms
worker压缩(fetch):      24908.89599609375 ms
多线程worker压缩(fetch): 19629.847900390625 ms   ()



补充

  • Navigator.hardwareConcurrency可以看电脑的CPU是多少核的
  • 6核CPU开10个worker也行,两者好像没什么联系
  • worker会占据浏览器内存,可以在F12 – Memory中看到,用完了需要手动卸载,避免占用内存
  • 不正确使用会造成浏览器内存不足(

    SBOX_FATAL_MEMORY_EXCEEDED

    )奔溃
一张blob图片:7267193字节

用postMessage传输给worker

1个worker,500图片,崩
5个worker,每个100图片,崩
5个worker,每个100图片,每个worker处理完后,卸载worker,崩
10个worker,每个50图片,每个worker处理完后,卸载worker,正常
10个worker,每个50图片,崩

  • postmessage

    应该少量多次,用完的worker及时卸载,避免一直占用内存,浏览器内存不够就奔溃



版权声明:本文为weixin_44240581原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。