vue3中使用Web Worker多线程
文章目录
问题描述
最近在做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~~
更多推荐
所有评论(0)