使用 useWorker 的多线程 React App
使用 useWorker 在单独的线程中处理昂贵且阻塞 UI 的任务。
众所周知,JavaScript 是一门单线程语言。因此,如果我们执行任何耗时的任务,它都会阻塞 UI 交互。用户需要等到它完成后才能执行其他操作,这会给用户体验带来很差。
为了克服和执行此类任务,Javascript 有一个名为Web Workers 的解决方案,它允许在 Web 浏览器中执行昂贵的任务而不会阻塞用户界面并使 UX 非常流畅。
首先让我们看看什么是 Web Worker。
Web Workers
Web Worker 是一个在后台运行的脚本,它运行在单独的线程中,而不是主线程中,不会影响用户界面。因此,它不会阻塞用户交互。Web Worker 主要用于在 Web 浏览器中执行耗时的任务,例如对大量数据进行排序、导出 CSV 文件、处理图像等。
参考上图,我们可以看到一个昂贵的任务由一个工作线程并行执行,而不会阻塞主线程。当我们在主线程中执行相同的操作时,它会导致 UI 阻塞。
React 中的并发模式怎么样?
React并发模式不会并行运行任务。它会将非紧急任务转移到紧急任务上,并立即执行紧急任务。它使用同一个主线程来处理紧急任务。
如下图所示,紧急任务会使用上下文切换来处理。例如,如果某个表正在渲染一个大型数据集,而用户尝试搜索某些内容,React 会将任务切换为用户搜索,并优先处理该任务,如下所示。
当使用 Worker 执行同一任务时,表格渲染会在单独的线程中并行运行。请查看下图。
使用Worker
useWorker是一个使用 React Hooks 以简单配置使用 Web Worker API 的库。它支持在不阻塞 UI 的情况下执行高开销任务,支持使用 Promise 替代事件监听器,其他值得关注的功能包括:
- 超时选项可终止工作进程
- 远程依赖项
- 可转让
- 工人状态
useWorker 是一个 3KB 的库
为什么 JavaScript 没有内置 Web Worker?
使用 JavaScript Web Worker 时,我们需要添加一些复杂的配置,用多行代码来设置 Worker。使用 useWorker 可以简化 Worker 的设置。让我们在下面的代码块中看看两者的区别。
在 React App 中使用 JS Web Worker 时
// webworker.js
self.addEventListener("message", function(event) {
var numbers = [...event.data]
postMessage(numbers.sort())
});
//index.js
var webworker = new Worker("./webworker.js");
webworker.postMessage([3,2,1]);
webworker.addEventListener("message", function(event) {
console.log("Message from worker:", event.data); // [1,2,3]
});
在 React App 中使用 useWorker 时
// index.js
const sortNumbers = numbers => ([...numbers].sort())
const [sortWorker] = useWorker(sortNumbers);
const result = await sortWorker([1,2,3])
正如我之前所说,useWorker()
与普通的 javascript 工作者相比,简化了配置。
让我们与 React App 集成并执行高 CPU 密集型任务来查看useWorker()
实际运行情况。
快速入门
要添加useWorker()
到 React 项目,请使用以下命令
yarn add @koale/useworker
安装包后,导入 useWorker()。
import { useWorker, WORKER_STATUS } from "@koale/useworker";
我们从库中导入 useWorker 和 WORKER_STATUS。useWorker ()钩子返回 workerFn 和控制器。
workerFn
是允许使用 Web 工作程序运行函数的函数。- 控制器由status和kill参数组成,status参数返回worker的状态,kill函数用于终止当前正在运行的worker。
让我们通过一个例子来看一下useWorker() 。
使用useWorker()和主线程对大型数组进行排序
首先,创建一个 SortingArray 组件并添加以下代码
//Sorting.js
import React from "react";
import { useWorker, WORKER_STATUS } from "@koale/useworker";
import { useToasts } from "react-toast-notifications";
import bubleSort from "./algorithms/bublesort";
const numbers = [...Array(50000)].map(() =>
Math.floor(Math.random() * 1000000)
);
function SortingArray() {
const { addToast } = useToasts();
const [sortStatus, setSortStatus] = React.useState(false);
const [sortWorker, { status: sortWorkerStatus }] = useWorker(bubleSort);
console.log("WORKER:", sortWorkerStatus);
const onSortClick = () => {
setSortStatus(true);
const result = bubleSort(numbers);
setSortStatus(false);
addToast("Finished: Sort", { appearance: "success" });
console.log("Buble Sort", result);
};
const onWorkerSortClick = () => {
sortWorker(numbers).then((result) => {
console.log("Buble Sort useWorker()", result);
addToast("Finished: Sort using useWorker.", { appearance: "success" });
});
};
return (
<div>
<section className="App-section">
<button
type="button"
disabled={sortStatus}
className="App-button"
onClick={() => onSortClick()}
>
{sortStatus ? `Loading...` : `Buble Sort`}
</button>
<button
type="button"
disabled={sortWorkerStatus === WORKER_STATUS.RUNNING}
className="App-button"
onClick={() => onWorkerSortClick()}
>
{sortWorkerStatus === WORKER_STATUS.RUNNING
? `Loading...`
: `Buble Sort useWorker()`}
</button>
</section>
<section className="App-section">
<span style={{ color: "white" }}>
Open DevTools console to see the results.
</span>
</section>
</div>
);
}
export default SortingArray;
这里我们配置了 useWorker,并传递了 bubleSort 函数来使用 worker 执行昂贵的排序。
接下来,向组件添加以下代码App.js
并导入SortingArray组件。
//App.js
import React from "react";
import { ToastProvider } from "react-toast-notifications";
import SortingArray from "./pages/SortingArray";
import logo from "./react.png";
import "./style.css";
let turn = 0;
function infiniteLoop() {
const lgoo = document.querySelector(".App-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 (
<ToastProvider>
<div className="App">
<h1 className="App-Title">useWorker</h1>
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<ul>
<li>Sorting Demo</li>
</ul>
</header>
<hr />
</div>
<div>
<SortingArray />
</div>
</ToastProvider>
);
}
我们在App.js组件中添加了 React 徽标,该徽标每100 毫秒旋转一次,以直观地表示执行昂贵任务时的阻塞和非阻塞 UI。
运行上述代码时,我们可以看到两个按钮 Normal Sort 和 Sort using useWorker()。
接下来,点击“普通排序”按钮,在主线程中对数组进行排序。我们可以看到 React 徽标停止旋转了几秒钟。由于排序任务阻塞了 UI 渲染,因此排序完成后,徽标会恢复旋转。这是因为两个任务都在主线程中处理。请查看以下 gif 动图。

让我们使用chrome 性能记录来检查它的性能分析。
我们可以看到Event:click任务花费了3.86秒来完成该过程,并且它阻塞了主线程3.86秒。
接下来,我们尝试使用useWorker()选项进行排序。点击它时,我们可以看到 React 徽标仍在旋转,没有中断。这是因为 useWorker 在后台执行排序,不会阻塞 UI。这使得用户体验非常流畅。请查看以下 gif

您还可以使用 来在控制台中查看工作者状态为RUNNING、SUCCESSsortWorkerStatus
。
让我们看看这种方法的性能分析结果
我们可以看到,第一幅图表示主线程中没有长时间运行的进程。第二幅图则显示排序任务由单独的工作线程处理。因此,主线程中没有阻塞任务。
您可以在以下沙箱中试用整个示例。
何时使用 worker
- 图像处理
- 对大型数据集进行排序或处理。
- 包含大量数据的 CSV 或 Excel 导出。
- 画布绘画
- 任何 CPU 密集型任务。
使用限制Worker
- Web 工作者无权访问窗口对象和文档。
- 当 Worker 运行时,我们无法再次调用它,直到它完成或被终止。为了解决这个问题,我们可以创建两个或更多个
useWorker()
hook 实例。 - 由于响应已被序列化,因此 Web Worker 无法返回该函数。
- Web Worker 受到最终用户机器可用 CPU 核心和内存的限制。
结论
Web Worker 允许在 React 应用中使用多线程执行高开销任务,而不会阻塞 UI。而 useWorker 允许在 React 应用中以简化的 hooks 方式使用 Web Worker API。Worker 不应过度使用,我们应该仅在必要时使用,否则会增加管理 Worker 的复杂性。
想要了解更多?欢迎在Twitter上与我们交流:)
你可以给我买杯咖啡来支持我 ☕
电子书
使用 ChatGPT 调试 ReactJS 问题:50 个基本技巧和示例
Twitter 实时关注者数量
更多博客
- 使用 Vite 代替 CRA 开发 React 应用
- 使用 Next.js、NextAuth 和 TailwindCSS 的 Twitter 关注者追踪器
- 不要优化你的 React 应用,而是使用 Preact
- 如何将 React 应用加载时间缩短 70%
- 使用 Next.js、Tailwind 和 Vercel 构建支持暗黑模式的投资组合
- React 中不再导入 ../../../
- 10 个 React 包,包含 1K 个 UI 组件
- 5 个软件包可在开发过程中优化和加速您的 React 应用
- 如何在 React 中以优化和可扩展的方式使用 Axios
- 15 个自定义 Hooks 让你的 React 组件更轻量
- 免费托管 React 应用的 10 种方法
- 如何在单页应用程序中保护 JWT