React中如何在map循环迭代时更新加载状态?
解决方案
核心问题分析
你的代码里getAnimationFrames是同步阻塞式计算——整个map循环会一次性执行完毕,期间JavaScript线程被完全占用,浏览器根本没时间处理React的DOM更新请求。所以哪怕你在回调里更新了状态,也得等整个计算结束后才会一次性渲染,中间的百分比自然看不到。
可行方案:异步分批计算
把同步的map循环拆分成异步分批执行,每次计算一小部分后让出线程,让浏览器有机会更新DOM。具体分两步改:
1. 重构getAnimationFrames为异步函数
把原来的同步map改成异步迭代,通过Promise逐步执行计算,每次迭代后调用回调并让出线程:
export const getAnimationFrames = async ( loading?: (percentage: number) => void, pgn: string = defaultPgn ): Promise<AnimationFrame[]> => { const history = getHistoryFromPgn(pgn); const totalMoves = history.length; const animationFrames: AnimationFrame[] = []; for (let moveNo = 0; moveNo < totalMoves; moveNo++) { const move = history[moveNo]; const animationFrame = getAnimationFrame(move); animationFrames.push(animationFrame); // 计算百分比并调用回调 const loadingPercentage = ((moveNo + 1) * 100) / totalMoves; if (loading) { loading(loadingPercentage); } // 让出线程,让浏览器处理渲染 await new Promise(resolve => setTimeout(resolve, 0)); } return animationFrames; };
这里用for循环替代map,每次迭代后用setTimeout(resolve, 0)让出线程,确保React有时间更新DOM。
2. 在React组件中异步调用并更新状态
使用useState管理加载百分比,在useEffect里异步调用重构后的函数,每次回调时更新状态:
import { useState, useEffect } from 'react'; export const YourComponent = () => { const [loadingPercentage, setLoadingPercentage] = useState(0); const [frames, setFrames] = useState<AnimationFrame['analysis'][]>([]); const [boards, setBoards] = useState<AnimationFrame['board'][]>([]); useEffect(() => { const loadFrames = async () => { const fullFrames = await getAnimationFrames((percentage) => { // 更新状态,触发DOM渲染 setLoadingPercentage(Math.round(percentage)); }); // 计算完成后更新最终数据 setFrames(fullFrames.map(frame => frame.analysis)); setBoards(fullFrames.map(frame => frame.board)); // 重置百分比到100 setLoadingPercentage(100); }; loadFrames(); }, []); return ( <div> <div>加载进度:{loadingPercentage}%</div> {/* 其他组件内容 */} </div> ); };
为什么之前的方案无效?
- 方案1:把
loadingPercentage加入useEffect依赖会导致useEffect重复执行,确实不合理; - 方案2:用
loadingPercentage作为依赖会触发无限渲染,因为每次更新状态都会重新执行useEffect; - 方案3:
useRef的更新不会触发React渲染,所以DOM不会更新; - 方案4:原生JS操作DOM在React中可能被React的虚拟DOM覆盖,因为React会在渲染时重新生成DOM元素。
优化点(可选)
如果觉得setTimeout(0)的间隔太频繁,可以换成requestIdleCallback,让计算在浏览器空闲时执行,避免影响页面交互:
// 替换await那一行 await new Promise(resolve => requestIdleCallback(resolve));
内容的提问来源于stack exchange,提问作者keremduran
相关产品推荐
相关产品推荐

