React 应用程序利用主线程来处理用户界面(UI) 渲染和 JavaScript 执行。当组件运行长时间任务时,此设置可能会导致性能问题。我们可以使用 Web Workers 将繁重的操作卸载到后台线程,以防止 UI 无响应。
在本指南中,我们将了解什么是 Web Workers 以及如何在 React 中使用它们。
先决条件
为了充分利用本文,您需要熟悉以下内容:
-
JavaScript 基础知识
-
对 React 有基本了解
什么是 Web Worker?
Web Worker 是一项浏览器功能,可让您在后台线程中运行脚本。工作线程可以执行繁重的任务而不会干扰 UI,从而确保响应迅速的用户体验。
Web Worker 有三种类型:
-
专用 Worker:这些 Worker 只能由创建它们的脚本访问。这意味着它们一次只能由一个脚本使用,即实例化它们的脚本。
-
共享 Worker:它们可以被多个脚本访问。这意味着它们可以同时在多个浏览器环境中使用。
-
Service Worker:它们充当位于 Web 应用程序、浏览器和网络之间的代理服务器。它们用于渐进式 Web 应用程序(PWA) 中以启用离线功能。
在本指南中,我们将重点介绍专用工作者以及如何在 React 应用程序中使用它们。
专职员工 www.cqzlsb.com
要创建专用工作者,您需要实例化该类Worker
并传入要运行的脚本文件的路径。
const myWorker = new Worker("<worker-file.js>");
这将创建一个工作线程来运行代码<worker-file.js>
。
Web Worker 虽然很方便,但它们也有一定的局限性:
-
它们不能直接访问 DOM
-
Web Worker 的作用范围
self
是window
由于这些限制,工作线程和主线程需要特殊的方法/事件来相互通信。
使用 worker 的方法在 worker 和其调用脚本之间共享消息postMessage
。此方法接受需要在 worker 和主线程之间共享的任何数据。例如:
// UI script
const handleCalculate = (number) => {
myWorker.postMessage(number);
console.log("Message posted to worker");
}
上面的代码是一个进行一些计算的函数。该函数将其接受的数字作为参数发送给 worker。然后,worker 使用该数字执行一些计算。
事件处理程序onmessage
允许工作线程和主线程监听消息。只要在它们之间发送消息,就会触发此事件。
// worker-file.js
self.onmessage = function (event) {
console.log("Message received from main script");
const number = event.data;
const result = factorial(number);
console.log("Posting message back to main script");
self.postMessage(result);
};
上述代码演示了 worker 如何处理传入的消息。它从消息事件的属性访问数据event.data
。worker 计算收到的数字的阶乘并将结果发送回调用脚本。
我们现在可以在主线程中响应从工作线程发送的消息:
// UI script
myWorker.onmessage = (event) => {
result.textContent = event.data;
console.log("Message received from worker");
};
要停止正在运行的工作线程,我们可以调用工作线程的terminate
方法。这将立即终止工作线程:
myWorker.terminate();
这涵盖了 Web Workers 的基础知识。现在,让我们探索如何在 React 中使用它们!
在 React 中使用 Web Workers
首先,让我们使用 Vite 创建一个 React App。运行以下命令:
npm create vite@latest react-worker -- --template react
按照安装后显示的说明在 IDE 中打开项目。
导航到该src
文件夹并components
在其中创建一个名为的文件夹。创建一个名为的文件Products.jsx
。
在这个文件中,我们将创建一个显示和过滤大量产品卡的组件。首先,让我们写出 UI 代码:
import { useState, useRef, useEffect } from "react";
const CATEGORIES = ["Electronics", "Clothing", "Toys"];
export default function Products() {
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
const [status, setStatus] = useState("loading");
const [filter, setFilter] = useState("");
// worker code goes here...
return (
<div>
<div className="filter-container">
<select value={filter} onChange={(e) => setFilter(e.target.value)}
className="product-dropdown"
disabled={status !== "idle"}
>
<option value="">All Products</option>
{CATEGORIES.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
<div className="product-btns">
<button onClick={handleFilterProduct} disabled={status !== "idle" || !filter} className="filter-btn" >
Filter Products
</button>
<button onClick={handleReset} disabled={status !== "idle"}>
Reset
</button>
</div>
</div>
{status === "loading" && <p>Loading...</p>}
{status === "filtering" && <p>Filtering...</p>}
{status === "resetting" && <p>Resetting...</p>}
{filteredProducts.length > 0 ? (
<ul className="product-list">
{filteredProducts.map((product) => (
<li key={product.id} className="product-card">
<div className="product-name">{product.name}</div>
<div className="product-price">${product.price}</div>
<div className="product-category">({product.category})</div>
</li>
))}
</ul>
) : (
status === "idle" && <p>No products found matching the filters.</p>
)}
</div>
);
}
上述代码包括一个用于选择要过滤的类别的下拉菜单、一个用于触发过滤的按钮以及一个基于当前过滤器显示的产品卡列表。它还具有一个重置按钮。
对于样式,请导航到App.css
文件并添加以下样式:
#root {
margin: 0 auto;
padding: 2rem 0;
min-width: 400px;
}
body {
display: flex;
min-height: 100vh;
font-family: "Courier New", Courier, monospace;
}
.filter-container {
display: flex;
justify-content: space-between;
}
.product-btns {
display: flex;
gap: 8px;
> button {
border-radius: 0.5rem;
color: white;
background-color: #7e3af2;
padding: 10px 20px;
font-size: 14px;
border: none;
cursor: pointer;
&:hover {
background: #6c2bd9;
}
&:disabled {
background: #c1b0de;
cursor: not-allowed;
}
}
}
.product-list {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: 20px;
}
.product-card {
border: 1px solid #e1e1e1;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #fff;
transition: transform 0.2s ease-in-out;
&:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
}
.product-name {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.product-price {
font-size: 16px;
color: #333333;
}
.product-category {
font-size: 14px;
color: #666;
margin-top: 5px;
}
p {
text-align: center;
}
.product-dropdown {
padding: 10px 25px 10px 10px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 0.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-size: 16px;
color: #333;
appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 4 5"><path fill="%23333" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
background-repeat: no-repeat;
background-position: right 10px center;
cursor: pointer;
&:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
}
要初始化 worker,我们将使用useRef
和useEffect
钩子。我们首先为 worker 定义一个引用,如下所示:
const workerRef = useRef(null);
在中useEffect
,我们创建了工作者并提供其脚本文件的路径(我们还没有创建)。
useEffect(() => {
workerRef.current = new Worker(
new URL("../workers/data-worker.js", import.meta.url)
);
const worker = workerRef.current;
const handleMessage = (event) => {
const { type, products: newProducts } = event.data;
if (type === "generated") {
setProducts(newProducts);
setFilteredProducts(newProducts);
setStatus("idle");
} else if (type === "filtered") {
setFilteredProducts(newProducts);
setStatus("idle");
}
};
worker.addEventListener("message", handleMessage);
worker.postMessage({ type: "generate" });
return () => {
worker.removeEventListener("message", handleMessage);
worker.terminate();
};
}, []);
以下是代码中发生的情况:
-
我们初始化 worker 并将其设置为新的 worker 实例。我们使用对象来避免打包过程中出现问题,
workerRef.current
而不是为脚本路径提供字符串。URL
-
我们向工人发送一条消息,指示它生产产品并听取它的回应。
-
该
handleMessage
函数处理来自工作人员的消息。它根据消息类型更新产品列表和状态。 -
最后,我们确保通过删除事件监听器并在组件卸载时终止工作程序来进行清理。
我们现在可以添加用于重置和过滤产品的辅助函数:
const handleFilterProduct = () => {
setStatus("filtering");
workerRef.current.postMessage({ type: "filter", filter, products });
};
const handleReset = () => {
setStatus("resetting");
setTimeout(() => {
setFilter("");
setFilteredProducts(products);
setStatus("idle");
}, 500); // Simulating a short delay for visual feedback
};
上面的代码执行以下操作:
-
该
handleFilterProduct
函数向工作人员发送一条消息以筛选产品。它根据所选过滤器和当前产品进行筛选。 -
该
handleReset
功能将产品重置为其初始状态并清除所有应用的过滤器。
要查看我们当前拥有的内容,请导航到文件App.jsx
并添加组件:
import "./App.css";
import Products from "./components/Products";
function App() {
return (
<>
<Products />
</>
);
}
export default App;
接下来,让我们关注工作器代码。创建一个名为的文件夹workers
,并在其中添加一个名为的文件data-worker.js
。
在此文件中,写出以下代码:
function getProducts() {
const products = Array.from({ length: 5000 }, () => ({
id: Math.random().toString(36).substring(2, 9),
name: `Product #${
Math.floor(Math.random() * 1000)}`,
category: ["Electronics", "Clothing", "Toys"][
Math.floor(Math.random() * 3)
],
price: Math.floor(Math.random() * 100),
}));
return Promise.resolve(products);
}
上述函数生成一个包含 5000 个产品的数组,并将其作为已解决的承诺返回。这模拟了异步数据获取操作。
仍然在 中data-worker.js
,添加以下代码来处理工作者的功能:
self.addEventListener("message", async (event) => {
const { type, products, filter } = event.data;
switch (type) {
case "generate":
const generatedProducts = await getProducts();
self.postMessage({ products: generatedProducts, type: "generated" });
break;
case "filter":
if (products && filter) {
const filteredProducts = products.filter(
(prod) => prod.category === filter
);
self.postMessage({ products: filteredProducts, type: "filtered" });
}
break;
default:
break;
}
});
代码监听来自主线程的消息。它根据收到的数据生成新产品或过滤现有产品。然后将结果作为消息发送回主线程。
恭喜您完成项目!您可以通过访问现场演示来查看最终产品的运行情况。如果您想查看整个代码库或探索进一步的增强功能,您可以在 GitHub 上找到完整的源代码。
结论
在本指南中,我们探讨了 Web Worker 的概念,包括其类型以及如何将其集成到 React 应用程序中。Worker 有助于将密集型任务转移到后台线程,从而增强用户体验。