React井字棋开发:如何避免未点击Square子组件不必要重渲染
React井字棋全方格重渲染问题修复
问题根因
React默认渲染规则是父组件一旦重渲染,所有子组件会默认跟随重渲染,不会自动判断props是否变化,你之前给handleClick加空依赖useCallback不生效,核心是三个问题没解决:
- Square组件没有做props比较逻辑,默认每次父组件渲染就跟着重渲染
- Board组件渲染Square时,每次都传新创建的内联箭头函数
() => props.onClick(i),就算上层传的onClick没变,这个内联函数都是全新引用,缓存会直接失效 - 空依赖的useCallback会让函数拿到初始状态的闭包,本身逻辑就会出错,更起不到缓存作用
分步修复方案
1. 给Square组件加memo缓存
React.memo会对组件传入的props做浅比较,只有props实际发生变化时才会触发组件重渲染,修改Square组件代码:
import { memo } from 'react'; const Square = memo(({ value, onClick, ...rest }) => { return ( <button className="square" onClick={onClick} {...rest}> {value} </button> ); }); export default Square;
这里加...rest是为了后续透传方格索引的自定义属性。
2. 移除Board组件内的内联回调,避免生成新函数引用
之前renderSquare里写的箭头函数每次Board渲染都会重新创建,是memo失效的核心原因,直接改成事件委托的写法,所有Square共用同一个回调函数,通过DOM上的data属性拿方格索引:
import { useCallback } from 'react'; const Board = (props) => { // 用useCallback固定回调引用,依赖只有props.onClick const handleSquareClick = useCallback((e) => { const index = Number(e.target.dataset.index); props.onClick(index); }, [props.onClick]); const renderSquare = (i) => { return ( <Square value={props.square[i]} onClick={handleSquareClick} data-index={i} /> ); }; return ( <div> <div className="border-row"> {renderSquare(0)} {renderSquare(1)} {renderSquare(2)} </div> <div className="border-row"> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </div> <div className="border-row"> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </div> </div> ); }; export default Board;
3. 正确给Game组件的事件回调加缓存
之前空依赖的useCallback会导致闭包陷阱,把回调用到的所有状态都加入依赖数组,保证函数引用只在依赖变化时更新:
import { useState, useCallback, useMemo } from 'react'; const Game = () => { const [xIsNext, setXIsNext] = useState(true); const [stepNumber, setStepNumber] = useState(0); const [history, setHistory] = useState([{ squares: Array(9).fill(null) }]); const current = history[stepNumber]; // 顺便把winner计算用useMemo缓存,避免每次渲染重复计算 const winner = useMemo(() => CalculateWinner(current.squares), [current.squares]); // 缓存跳转步骤的回调 const jumpTo = useCallback((step) => { setStepNumber(step); setXIsNext(step % 2 === 0); }, []); // 缓存历史步骤列表,避免每次渲染重新生成 const move = useMemo(() => { return history.map((step, move) => { const desc = move ? `Go to # ${move}` : "Game Start"; return ( <div key={move}> <button className="btninfo" onClick={() => jumpTo(move)}>{desc}</button> </div> ); }); }, [history, jumpTo]); let status; if (winner) { status = `Winner is ${winner}`; } else { status = `Turn for Player ${xIsNext ? "X" : "O"}`; } // 正确写依赖的handleClick缓存 const handleClick = useCallback((i) => { const latestHistory = history.slice(0, stepNumber + 1); const current = latestHistory[latestHistory.length - 1]; const squares = current.squares.slice(); const winner = CalculateWinner(squares); if (winner || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory(history.concat({ squares: squares })); setXIsNext(!xIsNext); setStepNumber(history.length); }, [history, stepNumber, xIsNext]); const handleRestart = useCallback(() => { setXIsNext(true); setStepNumber(0); setHistory([{ squares: Array(9).fill(null) }]); }, []); return ( <div className="game"> <div className="game-board"> <div className="game-status">{status}</div> <Board onClick={handleClick} square={current.squares} /> </div> <div className="game-info"> <button className="btninfo" onClick={handleRestart}>Restart</button> <div>{move}</div> </div> </div> ); }; export default Game; // 胜负判断逻辑不用改 const CalculateWinner = (squares) => { const lines = [ [0,1,2],[3,4,5],[6,7,8], [0,3,6],[1,4,7],[2,5,8], [0,4,8],[2,4,6], ]; for (let i = 0; i < lines.length; i++) { const [a,b,c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[b] === squares[c]) { return squares[a]; } } return null; };
验证方式
打开React DevTools,勾选「Highlight updates when components render」选项,点击单个方格时,只会高亮当前点击的方格和顶部状态条,其余未修改的方格不会出现更新高亮,就说明优化生效了。
提示:井字棋本身节点极少,全量重渲染的性能损耗几乎可以忽略,这个优化的核心价值是理清React的渲染逻辑和缓存API的正确用法,不要在小型项目里为了缓存过度增加代码复杂度。
内容的提问来源于stack exchange,提问作者Rohit kumar
相关产品推荐
相关产品推荐

