useProgressiveCompute
一个用于渐进式计算的 React Hook,通过时间片调度和智能任务分割,避免长时间阻塞主线程,保持 UI 流畅响应。
交互演示 React
亲手试一试,更容易理解。
在当前页面运行 useProgressiveCompute 的交互示例。特性
- ✅ 非阻塞计算 - 使用时间片调度,不会长时间阻塞主线程
- ✅ 实时进度 - 提供实时进度反馈(0-100%)
- ✅ 暂停/恢复 - 支持暂停和恢复计算
- ✅ 取消操作 - 可随时取消正在进行的计算
- ✅ 重置状态 - 一键重置到初始状态
- ✅ 智能调度 - 优先使用
requestIdleCallback,降级到setTimeout - ✅ 防抖更新 - 可配置的 UI 更新防抖,减少渲染次数
- ✅ 错误处理 - 内置错误捕获和状态管理
- ✅ 内存安全 - 自动清理异步任务,防止内存泄漏
安装
bash
# 将 useProgressiveCompute.ts 复制到你的项目中
cp useProgressiveCompute.ts src/hooks/基本用法
tsx
import { useProgressiveCompute } from './hooks/useProgressiveCompute';
function MyComponent() {
const data = [1, 2, 3, 4, 5, ...]; // 大量数据
// 定义转换函数
const transformFn = (item: number) => item * 2;
// 使用 hook
const { result, isComputing, progress, start } = useProgressiveCompute(
data,
transformFn,
{
batchSize: 500, // 每批处理 500 条
debounceMs: 16, // UI 更新防抖 16ms
timeout: 1000 // requestIdleCallback 超时时间
}
);
return (
<div>
<button onClick={start} disabled={isComputing}>
开始计算
</button>
<div>进度: {progress.toFixed(1)}%</div>
<div>结果数量: {result.length}</div>
</div>
);
}API
参数
typescript
useProgressiveCompute<T, R>(
data: T[], // 源数据数组
transformFn: (item: T) => R, // 转换函数
options?: ProgressiveComputeOptions // 配置选项
)Options
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
batchSize | number | 500 | 每批处理的数据量 |
debounceMs | number | 16 | UI 更新防抖延迟(毫秒) |
timeout | number | 1000 | requestIdleCallback 的超时时间 |
返回值
typescript
{
result: R[]; // 计算结果数组
isComputing: boolean; // 是否正在计算
progress: number; // 进度百分比 (0-100)
error: Error | null; // 错误信息
start: () => void; // 开始计算
pause: () => void; // 暂停计算
resume: () => void; // 恢复计算
cancel: () => void; // 取消计算
reset: () => void; // 重置到初始状态
}高级用法
1. 搜索/过滤场景
tsx
interface DataItem {
id: number;
name: string;
description: string;
}
function SearchComponent() {
const [searchQuery, setSearchQuery] = useState('');
const data: DataItem[] = [...]; // 大量数据
// 过滤函数,返回 null 表示不匹配
const filterFn = (item: DataItem): DataItem | null => {
if (item.name.includes(searchQuery) ||
item.description.includes(searchQuery)) {
return item;
}
return null;
};
const { result, isComputing, progress, start, reset } =
useProgressiveCompute(data, filterFn);
// 过滤掉 null 值
const matchedResults = result.filter(item => item !== null);
return (
<div>
<input
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
if (!e.target.value) {
reset(); // 清空时重置
}
}}
/>
<button onClick={start}>搜索</button>
<div>找到 {matchedResults.length} 条结果</div>
<div>进度: {progress}%</div>
</div>
);
}2. 数据转换场景
tsx
interface RawData {
id: number;
value: number;
}
interface ProcessedData {
id: number;
squared: number;
cubed: number;
isPrime: boolean;
}
function DataProcessor() {
const rawData: RawData[] = [...];
const transformFn = (item: RawData): ProcessedData => {
return {
id: item.id,
squared: item.value ** 2,
cubed: item.value ** 3,
isPrime: isPrimeNumber(item.value)
};
};
const { result, isComputing, progress, start, pause, resume, cancel } =
useProgressiveCompute(rawData, transformFn, {
batchSize: 1000,
debounceMs: 32
});
return (
<div>
<button onClick={start} disabled={isComputing}>开始</button>
<button onClick={pause} disabled={!isComputing}>暂停</button>
<button onClick={resume} disabled={isComputing}>恢复</button>
<button onClick={cancel}>取消</button>
<progress value={progress} max={100} />
<div>已处理: {result.length} / {rawData.length}</div>
</div>
);
}3. 带错误处理
tsx
function SafeProcessor() {
const data = [...];
const transformFn = (item: any) => {
// 可能抛出错误的转换逻辑
if (!item.valid) {
throw new Error('Invalid data');
}
return processItem(item);
};
const { result, error, isComputing, start, reset } =
useProgressiveCompute(data, transformFn);
return (
<div>
<button onClick={start}>开始</button>
{error && (
<div style={{ color: 'red' }}>
错误: {error.message}
<button onClick={reset}>重试</button>
</div>
)}
{isComputing && <div>处理中...</div>}
{!isComputing && !error && <div>完成!</div>}
</div>
);
}4. 性能监控
tsx
function PerformanceMonitor() {
const [startTime, setStartTime] = useState(0);
const [duration, setDuration] = useState(0);
const data = [...];
const { result, isComputing, progress, start } =
useProgressiveCompute(data, transformFn);
useEffect(() => {
if (isComputing && startTime === 0) {
setStartTime(Date.now());
}
if (!isComputing && startTime > 0) {
setDuration(Date.now() - startTime);
setStartTime(0);
}
}, [isComputing]);
return (
<div>
<button onClick={start}>开始</button>
<div>进度: {progress.toFixed(1)}%</div>
<div>耗时: {duration}ms</div>
<div>速度: {(result.length / duration * 1000).toFixed(0)} 条/秒</div>
</div>
);
}工作原理
时间片调度
Hook 使用生成器函数将大任务分割成小批次,每批处理完成后:
- 检查是否超过 16ms(一帧的时间)
- 如果超过,让出主线程,等待下一帧
- 使用
requestIdleCallback或setTimeout调度下一批
数据: [1, 2, 3, ..., 10000]
↓
批次1: [1...500] → 处理 → 更新 UI → 让出线程
批次2: [501...1000] → 处理 → 更新 UI → 让出线程
...
批次20: [9501...10000] → 处理 → 完成防抖更新
为了减少渲染次数,UI 更新使用防抖策略:
debounceMs <= 16: 使用requestAnimationFramedebounceMs > 16: 使用setTimeout
智能调度
优先使用 requestIdleCallback,在浏览器空闲时执行:
typescript
if (window.requestIdleCallback) {
requestIdleCallback(callback, { timeout: 1000 });
} else {
setTimeout(callback, 0);
}性能对比
普通同步处理 vs 渐进式处理
| 场景 | 数据量 | 普通处理 | 渐进式处理 | UI 流畅度 |
|---|---|---|---|---|
| 搜索 | 100,000 | 阻塞 500ms | 分批 520ms | ✅ 流畅 |
| 转换 | 50,000 | 阻塞 300ms | 分批 310ms | ✅ 流畅 |
| 过滤 | 200,000 | 阻塞 1000ms | 分批 1050ms | ✅ 流畅 |
关键优势: 虽然总耗时略有增加(~5%),但 UI 始终保持响应,动画不卡顿。
最佳实践
1. 选择合适的批次大小
typescript
// 简单计算:较大批次
{
batchSize: 1000;
}
// 复杂计算:较小批次
{
batchSize: 100;
}
// 搜索/过滤:中等批次
{
batchSize: 500;
}2. 防抖配置
typescript
// 高频更新(如进度条)
{
debounceMs: 16;
} // 使用 RAF
// 低频更新(如列表渲染)
{
debounceMs: 100;
} // 减少渲染次数3. 清理和重置
typescript
// 组件卸载时自动清理(内置)
// 手动重置
const handleClear = () => {
reset(); // 清空结果、进度、错误
};4. 避免频繁重新创建
typescript
// ❌ 不好:每次渲染都创建新函数
const transformFn = (item) => item * 2;
// ✅ 好:使用 useCallback
const transformFn = useCallback((item) => item * 2, []);注意事项
- 数据依赖:
data和transformFn变化时,需要重新调用start() - 内存占用:
result数组会累积所有结果,大数据量时注意内存 - 浏览器兼容:
requestIdleCallback在某些浏览器不支持,会降级到setTimeout - 并发控制: 同时只能有一个计算任务,新任务会取消旧任务
常见问题
Q: 为什么总耗时比同步处理略长?
A: 因为需要时间片调度和任务切换的开销,但换来的是 UI 流畅度,这是值得的权衡。
Q: 如何处理大量结果的渲染?
A: 建议配合虚拟滚动(如 react-window)或分页显示,避免一次性渲染大量 DOM。
Q: 可以在 Node.js 中使用吗?
A: 不建议。这个 Hook 依赖浏览器 API(requestAnimationFrame、requestIdleCallback),主要用于浏览器环境。
Q: 如何测试?
A: 可以 mock requestIdleCallback 和 requestAnimationFrame,或使用 @testing-library/react 的异步工具。
示例项目
查看完整示例:
src/components/SearchDemo- 搜索场景src/components/ProgressiveComputeDemo- 数据转换场景
License
MIT
实现源码
查看 useProgressiveCompute 的实现
tsx
import { useState, useRef, useCallback, useEffect } from "react";
interface ProgressiveComputeOptions {
batchSize?: number;
debounceMs?: number;
timeout?: number; // requestIdleCallback 的兜底超时(ms)
}
// 定义 Generator 返回值类型
type YieldValue<R> = {
type?: "pause";
partial?: R[];
progress?: number;
};
type ComputeGenerator<R> = Generator<YieldValue<R>, R[], void>;
export function useProgressiveCompute<T, R>(
data: T[],
transformFn: (item: T) => R,
options: ProgressiveComputeOptions = {}
) {
const { batchSize = 500, debounceMs = 16, timeout = 1000 } = options;
const [result, setResult] = useState<R[]>([]);
const [isComputing, setIsComputing] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<Error | null>(null);
const generatorRef = useRef<ComputeGenerator<R> | null>(null);
const isCancelledRef = useRef(false);
const isPausedRef = useRef(false);
const isMountedRef = useRef(true); // 防止内存泄漏
const rafIdRef = useRef<number | null>(null);
const timeoutIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const idleCallbackIdRef = useRef<number | null>(null);
// 清理所有异步任务
const cleanup = () => {
isCancelledRef.current = true;
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
if (timeoutIdRef.current !== null) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
if (
idleCallbackIdRef.current !== null &&
typeof window !== "undefined" &&
window.cancelIdleCallback
) {
window.cancelIdleCallback(idleCallbackIdRef.current);
idleCallbackIdRef.current = null;
}
if (isMountedRef.current) {
setIsComputing(false);
}
};
// 创建生成器
const createGenerator = useCallback(
function* (): ComputeGenerator<R> {
const results: R[] = [];
for (let i = 0; i < data.length; i += batchSize) {
if (isCancelledRef.current) return results;
// 暂停检查,直接 yield 并在外部处理
if (isPausedRef.current) {
yield { type: "pause" };
i -= batchSize; // 回退索引,下次继续当前批次
continue;
}
const chunk = data.slice(i, i + batchSize);
const transformedChunk = chunk.map(transformFn);
results.push(...transformedChunk);
// 修正进度计算
const currentProgress = Math.min(
100,
Math.round(((i + chunk.length) / data.length) * 100)
);
yield { partial: transformedChunk, progress: currentProgress };
}
return results;
},
[data, batchSize, transformFn]
);
// 智能调度器:优先 requestIdleCallback,否则 setTimeout
const scheduleNextFrame = useCallback(
(callback: () => void) => {
if (typeof window === "undefined") {
setTimeout(callback, 0);
return;
}
if (window.requestIdleCallback) {
idleCallbackIdRef.current = window.requestIdleCallback(
(deadline) => {
if (deadline.timeRemaining() > 0 || deadline.didTimeout) {
callback();
} else {
scheduleNextFrame(callback);
}
},
{ timeout }
);
} else {
setTimeout(callback, 0);
}
},
[timeout]
);
// 核心执行逻辑(带时间片控制)
const executeStep = useCallback(() => {
if (
!generatorRef.current ||
isCancelledRef.current ||
!isMountedRef.current
)
return;
try {
const startTime = performance.now();
let genResult;
while (true) {
if (isPausedRef.current) {
if (isMountedRef.current) {
setIsComputing(false);
}
return;
}
if (isCancelledRef.current) return;
genResult = generatorRef.current.next();
if (genResult.done) {
if (!isCancelledRef.current && isMountedRef.current) {
setResult(genResult.value);
setProgress(100);
setIsComputing(false);
}
return;
}
// 处理暂停信号
if (genResult.value?.type === "pause") {
if (isMountedRef.current) {
setIsComputing(false);
}
return;
}
if (genResult.value?.partial) {
// 防抖更新 UI
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
const { partial, progress: prog } = genResult.value;
const updateUI = () => {
if (!isMountedRef.current) return;
setResult((prev) => [...prev, ...partial]);
setProgress(prog ?? 0);
};
if (debounceMs <= 16) {
rafIdRef.current = requestAnimationFrame(updateUI);
} else {
timeoutIdRef.current = setTimeout(updateUI, debounceMs);
}
}
// 每帧最多执行 16ms,防止卡顿
if (performance.now() - startTime > 16) break;
}
// 智能调度下一帧
scheduleNextFrame(executeStep);
} catch (err) {
if (!isCancelledRef.current && isMountedRef.current) {
setError(err instanceof Error ? err : new Error(String(err)));
setIsComputing(false);
}
}
}, [debounceMs, scheduleNextFrame]);
// 启动
const start = useCallback(() => {
if (isComputing || data.length === 0) return;
cleanup();
isCancelledRef.current = false;
isPausedRef.current = false;
if (isMountedRef.current) {
setError(null);
setResult([]);
setProgress(0);
setIsComputing(true);
}
generatorRef.current = createGenerator();
scheduleNextFrame(executeStep);
}, [
isComputing,
data.length,
createGenerator,
scheduleNextFrame,
executeStep,
]);
const pause = useCallback(() => {
isPausedRef.current = true;
if (isMountedRef.current) {
setIsComputing(false);
}
}, []);
const resume = useCallback(() => {
if (!isPausedRef.current || !generatorRef.current) return;
isPausedRef.current = false;
if (isMountedRef.current) {
setIsComputing(true);
}
scheduleNextFrame(executeStep);
}, [scheduleNextFrame, executeStep]);
const cancel = useCallback(() => {
cleanup();
}, []);
const reset = useCallback(() => {
cleanup();
if (isMountedRef.current) {
setResult([]);
setProgress(0);
setError(null);
}
generatorRef.current = null;
}, []);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
cleanup();
};
}, []);
return {
result,
isComputing,
progress,
error,
start,
pause,
resume,
cancel,
reset,
};
}