和你的爱的人和爱你的人一起去见证美好
大家好,我是柒八九 。一个专注于前端开发技术/Rust
及AI
应用知识分享 的Coder
。
前言
作为一个前端开发,如果你还停留在每天CRUD
,还停留在切图/画图,还停留在和后端同学对某个API
设计的是否合理而大打出手
时,是时候停下来了。我们要变强,我们需要对我们经手的项目进行一番改造和优化。这才是我们能够变强的方式。而不是,沉浸在无休止的争吵和埋怨中。
众所周知,Javascript
是一种单线程语言。因此,如果我们执行任何耗时任务,它将阻塞UI交互。用户需要等待任务完成才能执行其他操作,这会给用户体验带来不好的影响。
其实,针对此类问题,我们有很多解决方案,
- 例如将耗时任务分割成多个短任务,并让其在多个渲染帧内执行,给UI交互(也就是UI渲染)留有时间,
- 也可以通过回调的方式,在UI交互触发后,在进行耗时任务的操作。
- 亦或者我们可以指定一个优先队列 ,当高优先级任务被执行时,低优先级任务(耗时任务)被降级处理(冷处理),直到高优先级任务被执行后再执行剩余低优先级任务。(这其实就是
React
并发的核心要点) - ...等等
上述列举了很多解决方式,他们都有一个共同特点 - 由于JS
单线程属性,它们只是将一些耗时任务从一个渲染帧分割或者延后到多个渲染帧内。本质上还是单线程的处理方式。
而,今天我们就介绍一种利用多线程(Web Worker
)处理React
中的耗时操作 。我们之前也在前面讲过Web Worker
的相关内容。
今天我们就详细的介绍如何在前端项目中使用Web Worker
用于处理耗时任务,然后将长任务利用多线程的分割出主线程,然后给主线程留足时间去回应更紧急的用户操作,优化用户操作。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- Web Workers
- React 的并发模式
- React 中使用Web Worker
- useWorker
- Web Worker的注意点
1. Web Workers
虽然,在之前的文章中介绍过Web Worker
,但是为了最大限度的兼容大家的学习情况,还是打算简单介绍一些。
如上图所示,JS
中存在三中Worker
,按照实现可以分为三类。
Web Worker
Shared Web Worker
Service Worker
而我们今天的主角-Web Worker
是我们最常见的。
Web Worker
是在后台运行的脚本,不会影响用户界面,因为它在单独的线程中运行,而不是在主线程中。
因此,它不会导致任何阻塞用户交互。Web Worker
主要用于在Web浏览器中执行耗时任务,如对大量数据进行排序、CSV导出、图像处理等。
从上图中,如果耗时任务在主线程中执行会阻塞UI渲染,当用Web Worker
代理耗时任务后,主线程并不会发生阻塞,也就是说它强任它强,老子Web Worker
2. React 的并发模式
讲到这里,可能有些心细的小伙伴就会产生疑问。既然都是处理耗时任务。那么,React 18
的并发渲染也可以达到此种目的。也就是使用React.useTransition()
将耗时任务设定为过渡任务,通过对某些操作标记为低优先级 ,在页面渲染过程中给高优先级的任务让步。
之前我们在
中,对React 并发
有过介绍。(想了解更多可以翻阅上述文章)。这里我们就简单阐述一下为什么React 并发
只是锦上添花
,缺不能药到病除
。
如果,你仔细看过上面的文章,你就会有有一个清晰的认知:
React并发模式
并不会并行运行任务。它会将非紧急任务
移动到过渡状态,并立即执行紧急任务。它使用相同的主线程来处理它。
下面是之前的一个示例。
使用useTransition
只是告知React
,有一些操作是不紧急的,如果遇到更高级的任务,不紧急的任务可以不立马显示,而是在处理完高优先级任务后才进行低优先级任务的渲染。
例如,如果一个表格正在渲染一个大型数据集,而用户尝试搜索某些内容,React
会将任务切换到用户搜索并首先处理它。
正如我们在图片中看到的那样,
紧急任务是通过上下文切换来处理的
React
的并发模式,只是让我们的项目拥有了辨别优先级的能力 ,并且在一定限制条件下 能够快速响应用户操作。但是,但是,但是,如果一个单个任务已经超过了浏览器一帧的渲染时间 ,那虽然设置了startTransition
,但是也无能为力 。如果存在这种情况,那就只能人为的将单个任务继续拆分或者利用Web Worker
进行多线程处理了。
当使用Web Worker
进行相同任务时,表格渲染会在一个独立的线程中并行运行。
3. React 中使用Web Worker
由于我们在项目开发时,使用不同的打包工具(vite/webpack
)。幸运的是,最新版的vite/webpack
都支持Web Worker
了。
我们可以通过
-
new URL()
的方式 --vite/webpack
都支持jsnew Worker( new URL( './worker.js', import.meta.url ) );
-
import
方式 只有vite
支持jsimport MyWorker from './worker?worker' const worker = new MyWorker()
更详细的处理可以参考它们的官网
当然,我们在项目代码中如何实例化Worker
对象也有很多方式。下面就介绍两种。
通过引入文件路径
index.js
js
// 创建一个新的Worker对象,
// 指定要在Worker线程中执行的脚本文件路径
const myWorker = new Worker(
new URL('./worker.js', import.meta.url)
);
// 向Worker发送消息
myWorker.postMessage(789789);
// 监听来自Worker的消息
myWorker.onmessage = function(event) {
console.log("来自worker的消息: ", event.data);
};
worker.js
js
// 在Worker脚本中接收并处理消息
self.onmessage = function(event) {
console.log("来自主线程的消息: ", event.data);
// 执行一些计算密集型的任务
let result = doSomeHeavyTask(event.data);
// 将结果发送回主线程
self.postMessage(result);
};
const doSomeHeavyTask = (num) => {
// 模拟一些计算密集型的操作
let result = 0;
for (let i = 0; i < num; i++) {
result += i;
}
return result;
};
Blob 方式
index.js
js
// 定义要在Worker中执行的脚本内容
const workerScript = `
self.onmessage = function(e) {
console.log('来自主线程的消息: ' + e.data);
self.postMessage('向主线程发送消息: ' + 'Hello, ' + e.data);
};
`;
// 创建一个Blob对象,指定脚本内容和类型
const blob = new Blob(
[workerScript],
{ type: 'application/javascript' }
);
// 使用URL.createObjectURL()方法创建一个URL,用于生成Worker
const blobURL = URL.createObjectURL(blob);
// 生成一个新的Worker
const worker = new Worker(blobURL);
// 监听来自Worker的消息
worker.onmessage = function(e) {
console.log('来自worker的消息: ' + e.data);
};
// 向Worker发送消息
worker.postMessage('Front789');
使用Blob
构建方式生成Web Worker
有以下几个优势:
优势 | 描述 |
---|---|
动态生成 | 可以动态地生成Worker 脚本,无需保存为单独文件,根据需要生成不同的Worker 实例。 |
内联脚本 | 将Worker 脚本嵌入到Blob 对象中,直接在JavaScript 代码中定义Worker 的逻辑,无需外部脚本文件。 |
便捷性 | 更方便地创建和管理Worker 实例,无需依赖外部文件。 |
安全性 | Blob 对象在内存中生成,不需要保存为实际文件,提高安全性,避免了对实际文件的依赖和管理。 |
总的来说,使用
Blob
构建方式生成Web Worker
可以提供更灵活、便捷和安全的方式来管理和使用Worker
实例。
4. useWorker
上面一节中,我们介绍了如何在前端项目中使用Web Worker
。无论是使用文件导入的方式还是Blob
的方式。都需要写一些模板代码。虽然能解决我们的问题,但是使用方式还是不够优雅。
功能介绍
下面,我们就介绍一种更优雅的方式- 使用useWorker
库。
useWorker是一个库,它使用React Hooks
在简单的配置中使用Web Worker API。它支持在不阻塞UI的情况下执行耗时任务,支持使用Promise
而不是事件监听器。
我们可以从官网看到相关的介绍信息。
其中,WORKER_STATUS
用于返回Web Worker
的状态信息。
我们可以通过向useWorker
中传递一个回调函数,然后该函数就会在对应的Web Worker
中执行。
js
const sortNumbers = numbers => ([...numbers].sort())
const [
sortWorker,
{
status: sortStatus,
kill: killSortWorker
}
] = useWorker(sortNumbers);
大家可以对比之前的用原生构建Web Worker
实例。我们可以抛弃冗余代码,并且返回的函数(sortWorker
)还支持Promise
。
也就意味着我们使用xx.then()
或者 await xx()
以同步的写法获取异步结果。
jsx
import React from "react";
import { useWorker } from "@koale/useworker";
const numbers = [...Array(5000000)].map(
e => ~~(Math.random() * 1000000)
);
const sortNumbers = nums => nums.sort();
const Example = () => {
const [sortWorker] = useWorker(sortNumbers);
const runSort = async () => {
const result = await sortWorker(numbers);
};
return (
<button type="button" onClick={runSort}>
运行耗时任务
</button>
);
};
并且,useWorker
是一个大小为3KB
的库,我们还不需要有太多的资源负担。既然,有这么多强势的功能,那我们就来看看它到底是何方神圣。
安装依赖
用我们御用脚手架f_cli,来构建一个前端项目(npx f_cli_f craete worker_demo
)。
要将useWorker()
添加到React项目中,请使用以下命令:
bash
npm install @koale/useworker --force
由于useworker
源码中使用了peerDependencies
指定了React
版本为^16.8.0
。如果大家在17/18
版本的React
环境下,会发生错误。所以我们可以使用--force
忽略版本限制。(这里大家可以放心使用,它内部的只是用到简单的hook
)
安装完包后,导入useWorker()
。
javascript
import {
useWorker,
WORKER_STATUS
} from "@koale/useworker";
我们从库中导入useWorker
和WORKER_STATUS
。useWorker()
钩子返回workerFn
和controller
。
workerFn
是一个允许在Web Worker
中运行函数的函数。controller
包含status
和kill
参数。-
status
参数返回Worker
的状态 -
kill
函数用于终止当前运行的Worker
-
案例展示
让我们通过一个示例来看看useWorker()
。
使用useWorker()
和主线程对大数组进行排序
SortingArray
首先,创建一个SortingArray
组件,并添加以下代码:
工具代码
js
// 模拟耗时任务
const bubleSort = (arr: number[]): number[] =>{
const len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
const numbers = [...Array(50000)].map(() =>
Math.floor(Math.random() * 1000000)
);
主要逻辑
jsx
import React,{ useState } from "react";
import {
useWorker,
WORKER_STATUS
} from "@koale/useworker";
function SortingArray() {
const [sortStatus, setSortStatus] = useState(false);
const [
sortWorker,
{
status: sortWorkerStatus
}
] = useWorker(bubleSort);
console.log("WebWorker status:", sortWorkerStatus);
const onSortClick = () => {
setSortStatus(true);
const result = bubleSort(numbers);
setSortStatus(false);
alert('耗时任务结束!')
console.log("处理结果", result);
};
const onWorkerSortClick = () => {
sortWorker(numbers).then((result) => {
console.log("使用WebWorker的处理结果", result);
alert('耗时任务结束!')
});
};
return (
<div>
<section >
<button
type="button"
disabled={sortStatus}
onClick={() => onSortClick()}
>
{sortStatus ?
`正在处理耗时任务...` :
`主线程触发耗时任务`
}
</button>
<button
type="button"
disabled={sortWorkerStatus === WORKER_STATUS.RUNNING}
onClick={() => onWorkerSortClick()}
>
{sortWorkerStatus === WORKER_STATUS.RUNNING
? `正在处理耗时任务...`
: `使用WebWorker处理耗时任务`
}
</button>
</section>
<section>
<span style={{ color: "white" }}>
打开控制台查验状态信息
</span>
</section>
</div>
);
}
export default SortingArray;
我们在SortingArray
配置了两个操作
onSortClick
中按照常规处理,也就是在主线程中执行耗时操作onWorkerSortClick
中执行useWorker
相关逻辑,并传递了bubleSort
函数以使用Worker
执行耗时的排序操作。
App.js
我们App.js
中引入SortingArray
组件,并且为了能让UI
阻塞看的更明显,我们用JS
来操作logo
文件,让其不停的转动,每100毫秒旋转一次。
- 如果是一个阻塞主线程的任务,那么
logo
将会停止 - 如果主线程不阻塞,那
logo
会一直转动
jsx
import React from "react";
import SortingArray from "./SortingArray";
import logo from './assets/react.svg'
import "./App.css";
let turn = 0;
function infiniteLoop() {
const lgoo = document.querySelector(".logo");
turn += 8;
lgoo.style.transform = `rotate(${turn % 360}deg)`;
}
export default function App() {
React.useEffect(() => {
const loopInterval = setInterval(infiniteLoop, 100);
return () => clearInterval(loopInterval);
}, []);
return (
<>
<div >
<h1 >useWorker Demo</h1>
<header>
<img src={logo} className="logo" />
</header>
<hr />
</div>
<div>
<SortingArray />
</div>
</>
);
}
我们来看看分别点击对应按钮会发生啥?
上图是耗时任务在主线程中执行的效果。在执行期间,动画效果是阻塞的,也就意味着在多个帧的时间内,浏览器是无法执行额外的操作的。
我们用Chrome-performance
来探查一下性能消耗。
我们可以看到事件:点击任务花费了7.85
秒来完成整个过程,并且它阻塞了主线程7.85
秒。
而这个图,我们使用了Web Worker
,在执行耗时任务的时候,动画还是执行原来的操作。也就是操作不会阻塞。因为useWorker
在后台执行排序而不阻塞UI。这使得用户体验非常流畅。
和上面的分析方式一样,打开Performance
tab,让我们看看这种方法的性能分析结果。
我们截取主线程的部分数据,发现有任意时间段内,Scripting
所占总时间的比例都很少,更大部分都是Idle
也就是主线程处于空闲阶段,可以随时响应用户操作。
而在对应的worker
中确是一直在执行计算任务,丝毫没有片刻休息。
5. Web Worker的注意点
何时用Worker
我们之前的文章讲过,JS
自从引入V8后,在代码执行和内存处理上有了更高的优化。例如使用JIT,引入WebAssembly,热代码优先编译等。
但是呢,针对一些特殊的场景,上述的方式只能提供简单的优化,这样我们就需要另外的解决方案来处理这些棘手的问题。
当我们遇到如下情景,并有严重的性能问题,那就需要借助Web Worker
一臂之力了
- 图像处理
- 对大型数据集进行排序或处理
- 带有大量数据的CSV或Excel导出
- 画布绘制
- 任何CPU密集型任务
Worker的限制
这个在之前介绍Web Worker
的文章就介绍过,我们就直接拿来主义了。
Web Worker
无法访问window
对象和document
。- 当
Worker
正在运行时,我们无法再次调用它,直到它完成或被终止。为了解决这个问题,我们可以创建两个或更多useWorker()
钩子的实例。 Web Worker
无法返回函数,因为响应是序列化的。Web Worker
受到终端用户机器可用CPU核心和内存的限制。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。