问题描述

最近在做vue3的项目中,遇到了计算量庞大导致页面响应缓慢的问题,正好每个计算任务的结果不需要汇总,所以想到了以多线程的形式去执行每个计算任务。但是通过一轮google、baidu都没有找到vue3的案例,还好在外网的一篇案例中嗅到了一丝可能。


Web Worker的作用

大佬可以跳过, 对于Web Worker多线程可能有很多小伙伴还不知道是干嘛的,所以我先举一个列子让大家知道它大概是用来做什么的。

提示:用到的console如果看不懂的,可以到这篇文章学习:传送门

①洗完澡后再洗衣服: 在没有洗衣机(Worker)前,通常都是先洗完澡后再手洗衣服(这里我除外,我都是踩着衣服一起洗澡的),这样我们每天都要花费不少时间在洗澡和洗衣服上,花费时长为:洗澡所用时长+洗衣服所用时长。相当于下面这段代码:

console.time('一共花了多少时长')
console.time('洗澡所用时长')
let length_1 = 300000000;
let sum1 = 0
for (let i = 0; i <= length_1; i++) {
    sum1 += i
}
console.log('%c循环1执行完:' + sum1, 'color:green')
console.timeEnd('洗澡所用时长')
console.time('洗衣服所用时长')
let length_2 = 200000000;
let sum2 = 0
for (let i = 0; i <= length_2; i++) {
    sum2 += i
}
console.log('%c循环2执行完:' + sum2, 'color:green')
console.timeEnd('洗衣服所用时长')
console.timeEnd('一共花了多少时长')
// 循环1执行完:45000000067108860
// 洗澡所用时长: 2712.139892578125 ms
// 循环2执行完:20000000067108864
// 洗衣服所用时长: 1751.049072265625 ms
// 一共花了多少时长: 4463.2490234375 ms

GIF输出效果:(从结果可以看到大概第2.7秒循环1执行完了,接着大概到了第4.5秒后循环2才执行完,所以从中可以看出循环1阻塞了循环2,所以此时花费时长为:循环1+循环2)
在这里插入图片描述

②把衣服放到洗衣机后洗澡: 当我们拥有洗衣机(Worker)后,就可以把衣服放到洗衣机后愉快的洗澡了,此时为异步操作,那么这时花费的总时长取决于谁更后洗完了,花费时长为:Math.max(洗澡所用时长,洗衣服所用时长)。相当于下面的代码:

本地工作目录:

文件夹
	|-index.html
	|-worker.js

index.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Worker</title>
</head>

<body>

</body>
<script>
    console.time('洗衣机洗完衣服了,所用时长')
    let worker = new Worker("./worker.js"); // 开启副线程
    let length_1 = 300000000;
    let sum1 = 0
    worker.postMessage(length_1); // 发送消息给副线程
    worker.onmessage = e => { // 监听副线程返回的消息
        sum1 = e.data
        console.log('%c循环1执行完了:' + e.data, 'color:green')
        console.timeEnd('洗衣机洗完衣服了,所用时长')
        worker.terminate() // 关闭线程
    }
    console.time('洗完澡了,所用时长')
    let length_2 = 200000000;
    let sum2 = 0
    for (let i = 0; i <= length_2; i++) {
        sum2 += i
    }
    console.log('%c循环2执行完了:' + sum2, 'color:green')
    console.timeEnd('洗完澡了,所用时长')
</script>

</html>

worker.js:

self.onmessage = function (e) { //监听主线程发过来的消息
    let length_1 = e.data
    let sum = 0
    for (let i = 0; i <= length_1; i++) {
        sum += i
    }
    self.postMessage(sum); // 将信息发送到主线程上
}

GIF输出效果:(从结果可以看到大概第1.9秒循环2执行完了,紧接着接着大概第2.4秒后循环1也执行完了,当我们把循环1放到线程上执行时并没有阻塞后续的循环2,因为循环2循环时长短所以先打印出来了,因此此时花费总时长为:循环1)
在这里插入图片描述
虽然折腾这么久后只省了几秒钟,但是当项目计算量越来越大,多线程的优势就会变得特变明显了。


使用插件(仅适用于Webpack项目) ✘已不推荐使用

在vue项目中,如果直接使用Web Worker,会遇到worker文件路径与打包解析的问题,在做vue2.x项目时可以使用vue-worker插件去优雅的使用多线程,但我当我尝试在vue3中使用时,它依赖的simple-web-worker库会报Object.defineProperty called on non-object的错误,应该是vue3把Vue全局对象给模块化了导致它拿不到对象,所以我放弃它了。经过一番查找,我发现还有另外一个方案,就是worker-loader插件,因为它是一个解析器所以我初步判断是可以在vue3中使用的,但是看过很多相关的博文,都没有能够在vue3中正常使用它(╯°Д°)╯︵┻━┻,最后终于在这篇外网博文中找到了可用代码,成功在vue3中跑起来了(T ^ T)

❀安装worker-loader

好了说了这么多,开始进入正题,我们先运行npm安装worker-loader

npm install worker-loader

安装成功后在package.json文件的dependencies中可以看到
在这里插入图片描述

❀配置webpack

vue.config.js文件的defineConfig里加上配置参数

  chainWebpack: config => {
    config.module
      .rule('worker-loader')
      .test(/\.worker\.js$/)
      .use({
        loader: 'worker-loader',
        options: {
          inline: true
        }
      })
      .loader('worker-loader')
      .end()
  }

详细截图:
在这里插入图片描述

❀使用

先在src目录下新建workers文件夹,接着在里面新建worker.js,在js文件里添加下面的测试代码:

addEventListener('message', e => {
    const { data } = e
    console.log(data)
    setTimeout(() => {
        return postMessage('线程完成')
    }, 1000)
})
export default {}

详细截图:
在这里插入图片描述
之后就可以新建一个vue文件,加入下面代码进行测试:

引入js文件时需要使用worker-loader!@并且路径是从src目录下开始数层级的,并非你执行的vue文件开始,这就是我一直不能正常跑起来的原因。

<template>
  <div>
    <h1>vue3-Worker</h1>
    <button @click="openWorker">开启线程</button>
    <p>F12打开浏览器控制台查看效果</p>
  </div>
</template>

<script setup>
import Worker from 'worker-loader!@/workers/worker'
const openWorker = () => {
  const worker = new Worker()
  worker.postMessage('开启线程')
  worker.onmessage = e => {
    console.log(e.data)
    setTimeout(() => {
      worker.postMessage('线程关闭')
      worker.terminate()
    }, 1000)
  }
}
</script>

为了方便,我就直接用官方模板的HelloWorld.vue进行测试了:
在这里插入图片描述
GIF运行效果:(在控制台Console中可以看到线程的打印状态,源代码Sources的线程Threads中可以看到所有已开启的线程,手速快的按shift+esc也能看到)

在这里插入图片描述

ts项目注意事项

当你把代码写成typeScript时(因为线程跑完后我返回了字符串,所以event里的data需要声明为string),你会发现引入的ts文件路径会报波浪线:
在这里插入图片描述
这时我们需要在shims-vue.d.ts文件中声明一下文件路径,直接为所有.ts文件声明也行:
在这里插入图片描述

❀获取项目Demo

有积分的交一下公粮,没有的话到Gitee下载就好

CSDN:

vue3-web-worker(js原味):传送门
vue3-ts-web-worker(ts风味):传送门

Gitee:

vue3-web-worker(js原味):传送门
vue3-ts-web-worker(ts风味):传送门


直接使用URL(适用于Vite项目和Webpack项目)✔推荐使用

当我在看vite官方文档时,原来可以直接用URL()的形式去解决导入文件的问题,试了一下vite项目和webpack项目都能正常使用,vite官方地址:传送门。具体使用案例可以参考一下下面的例子。

本地工作目录:

src
	|-components
		|-UseWorker.vue
	|-workers
		|-worker.ts

worker.ts:

addEventListener('message', e => {
    const { data } = e
    console.log(data)
    setTimeout(() => {
        return postMessage('线程完成')
    }, 1000)
})

UseWorker.vue:

<template>
    <div>
        <h1>vue3-Worker</h1>
        <button @click="openWorker">开启线程</button>
        <p>F12打开浏览器控制台查看效果</p>
    </div>
</template>
  
<script lang="ts" setup>
const openWorker = () => {
    const worker = new Worker(new URL('../workers/worker.ts', import.meta.url))
    worker.postMessage('开启线程')
    worker.onmessage = e => {
        console.log(e.data)
        setTimeout(() => {
            worker.postMessage('线程关闭')
            worker.terminate()
        }, 1000)
    }
}
</script>

需要注意的是,在vite项目中我们想在worker.ts里使用ES Module导出一些东西

export default {}

那么在我们new实例时需要加上 type: 'module',否则会报错。

const worker = new Worker(new URL('../workers/worker.ts', import.meta.url), {
    type: 'module',
})

另外对于vite项目我们还可以在vite.config.ts中配置Worker 选项,具体看官方文档:传送门

▲异步任务处理

有些小伙伴可能会遇到一种情况,就是有多个计算任务,但是最后结果需要有一个汇总,这时候我们就需要使用Promise.all来处理了,具体参考下面例子:

Promise.all会等待所有异步事件执行完毕了才会把结果回调给then,不懂的小伙伴自个去补习一下:传送门

本地工作目录:

文件夹
	|-cumsumWorker.js
	|-index.html

cumsumWorker.js:(简单累加任务)

self.onmessage = function (e) {
    let length = e.data
    let sum = 0
    for (let i = 0; i <= length; i++) {
        sum += i
    }
    self.postMessage(sum);
}

index.html:我们给每个计算任务都单独开一个子线程去计算各自的累加任务,之后在主线程去等待全部子线程计算完后再处理结果

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Worker</title>
</head>

<body>

</body>
<script>
    // 计算任务1
    let calculate1 = new Promise((resolve, reject) => {
        let worker = new Worker("./cumsumWorker.js");
        let length = 1000000000;
        worker.postMessage(length);
        worker.onmessage = e => {
            worker.terminate()
            resolve(e.data)
        }
    })
    // 计算任务2
    let calculate2 = new Promise((resolve, reject) => {
        let worker = new Worker("./cumsumWorker.js");
        let length = 2000000000;
        worker.postMessage(length);
        worker.onmessage = e => {
            worker.terminate()
            resolve(e.data)
        }
    })
    // 任务汇总
    Promise.all([calculate1, calculate2]).then((res) => {
        console.log(res)
    })
</script>

</html>

GIF输出效果:

在这里插入图片描述


over~~

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐