性能:React实战优化技巧之函数闭包
- 创业
- 2025-08-24 21:45:01

子组件使用了 React.memo ,为什么 “prop 值未发生改变”,子组件依然被重新渲染了?
🚧 示例:点击子组件中按钮,获取 input 数据进行提交(常见于表单)
index.tsx
import Author from './Author.tsx'; export default function Index() { const [val, setVal] = useState(''); const consoleValue = () => { console.log(val); } return ( <> <input type="text" value={val} onChange={(e) => setVal(e.target.value)} /> <Author name="李刚" onClick={consoleValue} /> </> ); }Author.tsx
export default function Author({ name, onClick }: { name: string; onClick: () => void }) { console.log('Author'); return ( <div> <button onClick={onClick}>{name}</button> </div> ); }🐋 现象:input 每次输入值,<Author> 组件就被重新渲染一次【prop onClick 发生了变化】!
#mermaid-svg-TLo3B0OJ9v83Tf3x {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-TLo3B0OJ9v83Tf3x .error-icon{fill:#552222;}#mermaid-svg-TLo3B0OJ9v83Tf3x .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TLo3B0OJ9v83Tf3x .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-TLo3B0OJ9v83Tf3x .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TLo3B0OJ9v83Tf3x .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TLo3B0OJ9v83Tf3x .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TLo3B0OJ9v83Tf3x .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TLo3B0OJ9v83Tf3x .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TLo3B0OJ9v83Tf3x .marker.cross{stroke:#333333;}#mermaid-svg-TLo3B0OJ9v83Tf3x svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TLo3B0OJ9v83Tf3x .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TLo3B0OJ9v83Tf3x .cluster-label text{fill:#333;}#mermaid-svg-TLo3B0OJ9v83Tf3x .cluster-label span{color:#333;}#mermaid-svg-TLo3B0OJ9v83Tf3x .label text,#mermaid-svg-TLo3B0OJ9v83Tf3x span{fill:#333;color:#333;}#mermaid-svg-TLo3B0OJ9v83Tf3x .node rect,#mermaid-svg-TLo3B0OJ9v83Tf3x .node circle,#mermaid-svg-TLo3B0OJ9v83Tf3x .node ellipse,#mermaid-svg-TLo3B0OJ9v83Tf3x .node polygon,#mermaid-svg-TLo3B0OJ9v83Tf3x .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TLo3B0OJ9v83Tf3x .node .label{text-align:center;}#mermaid-svg-TLo3B0OJ9v83Tf3x .node.clickable{cursor:pointer;}#mermaid-svg-TLo3B0OJ9v83Tf3x .arrowheadPath{fill:#333333;}#mermaid-svg-TLo3B0OJ9v83Tf3x .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TLo3B0OJ9v83Tf3x .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TLo3B0OJ9v83Tf3x .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-TLo3B0OJ9v83Tf3x .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-TLo3B0OJ9v83Tf3x .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TLo3B0OJ9v83Tf3x .cluster text{fill:#333;}#mermaid-svg-TLo3B0OJ9v83Tf3x .cluster span{color:#333;}#mermaid-svg-TLo3B0OJ9v83Tf3x div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TLo3B0OJ9v83Tf3x :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} state val值变化 prop onClick变化 用户输入 触发index组件重新渲染 consoleValue重新生成 Author重新渲染🐒 使用 memo ,在 props 没有改变的情况下跳过重新渲染!
index.tsx
const AuthorMemo = React.memo(Author); export default function Index() { const consoleValue = useCallback(() => { console.log(val); }, [val]); return ( <> {/* 省略 */} <AuthorMemo name="李刚" onClick={consoleValue} /> </> ); }这里需要使用 useCallback(fn, dependencies) 来处理渲染期间传递普通函数,避免传递给组件的 props 始终不同!
因为 <Author> 中要获取最新的 val 值,因此 useCallback() 中追加了 val 作为 dependencies。
每次 <input> 输入,val 值都发生变化、从而导致 consoleValue 重新生成,因此 <AuthorMemo> 依然每次会重新渲染!
1️⃣ 传递依赖项数组: 初始渲染后以及依赖项变更后 运行
const consoleValue = useCallback(() => { // ... }, [val]); // val 变更时返回一个新的函数2️⃣ 传递空依赖项数组:仅在 初始渲染后 运行
const consoleValue = useCallback(() => { // ... }, []); // 初始化后,不会再执行3️⃣ 不传递依赖项数组:每次渲染之后 运行
const consoleValue = useCallback(() => { // ... }); // 每一次都返回一个新函数:没有依赖项数组🐇 上述问题应该如何解呢?
【有缺陷】方案一:memo 支持自定义 arePropsEqual 来确定是否重新渲染!
const MemoizedComponent = memo(SomeComponent, arePropsEqual?) const AuthorMemo = React.memo(Author, (_prevProps, nextProps) => { /** * 可选参数 arePropsEqual:一个函数,接受两个参数:组件的前一个 props 和新的 props。 * 如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 true。否则返回 false。 * 通常情况下,你不需要指定此函数。默认情况下,React 将使用 Object.is 比较每个 prop。 */ return _prevProps.name === nextProps.name; }); export default function Index () { const consoleValue = useCallback(() => { console.log(val); }, [val]); }该方案的问题:获取的 val 值为一直为初始值,无法获取输入的最终 val 值。
🐇 问题分析:典型的 React 中的闭包问题。
React 中:组件内的每个函数都是一个闭包,因为组件本身只是一个函数。
理论上,当 val 发生变化时,consoleValue 函数会被重新创建,从而捕获最新的 val 值。然而,如果 AuthorMemo 没有重新渲染,或者 Author 组件内部没有正确处理 onClick 的更新【React.memo 比较算法导致 _prevProps.name === nextProps.name;】,可能会导致 consoleValue 没有捕获到最新的 val 值。
【推荐】方案二:ref + useEffect 组合实现。
借助 ref 每次渲染间存储信息及修改不会触发渲染的特性;
const AuthorMemo = React.memo(Author); export default function Index() { const [val, setVal] = useState(''); const ref = useRef<(() => void) | undefined>(); // 省略 dependencies 参数,则在每次重新渲染组件之后,将重新运行 Effect 函数 useEffect(() => { // 每次更新,指向一个新的函数 // .current 属性可以随时被更新,因此它不会受到闭包的限制 ref.current = () => { console.log(val); }; }); // 依赖数组为空 [] 在整个组件的生命周期中只会被创建一次(初始化) const consoleValue = useCallback(() => { ref.current?.(); }, []); return ( <> <input type="text" value={val} onChange={(e) => setVal(e.target.value)} /> <AuthorMemo name="李刚" onClick={consoleValue} /> </> ); } ref 用于渲染之间 存储信息(普通对象存储的值每次渲染都会重置);useEffect(() => {}) 每次渲染执行; ref.current = ... 改变 ref.current 属性时,React 不会重新渲染组件; const consoleValue = useCallback(() => {}, []) 只初始渲染运行、确保了 consoleValue 不发生变化(useCallback 在多次渲染中缓存函数)。 #mermaid-svg-eXBVYSeIS1gUNIsM {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-eXBVYSeIS1gUNIsM .error-icon{fill:#552222;}#mermaid-svg-eXBVYSeIS1gUNIsM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eXBVYSeIS1gUNIsM .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-eXBVYSeIS1gUNIsM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eXBVYSeIS1gUNIsM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eXBVYSeIS1gUNIsM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eXBVYSeIS1gUNIsM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eXBVYSeIS1gUNIsM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eXBVYSeIS1gUNIsM .marker.cross{stroke:#333333;}#mermaid-svg-eXBVYSeIS1gUNIsM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eXBVYSeIS1gUNIsM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eXBVYSeIS1gUNIsM .cluster-label text{fill:#333;}#mermaid-svg-eXBVYSeIS1gUNIsM .cluster-label span{color:#333;}#mermaid-svg-eXBVYSeIS1gUNIsM .label text,#mermaid-svg-eXBVYSeIS1gUNIsM span{fill:#333;color:#333;}#mermaid-svg-eXBVYSeIS1gUNIsM .node rect,#mermaid-svg-eXBVYSeIS1gUNIsM .node circle,#mermaid-svg-eXBVYSeIS1gUNIsM .node ellipse,#mermaid-svg-eXBVYSeIS1gUNIsM .node polygon,#mermaid-svg-eXBVYSeIS1gUNIsM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eXBVYSeIS1gUNIsM .node .label{text-align:center;}#mermaid-svg-eXBVYSeIS1gUNIsM .node.clickable{cursor:pointer;}#mermaid-svg-eXBVYSeIS1gUNIsM .arrowheadPath{fill:#333333;}#mermaid-svg-eXBVYSeIS1gUNIsM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eXBVYSeIS1gUNIsM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eXBVYSeIS1gUNIsM .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-eXBVYSeIS1gUNIsM .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-eXBVYSeIS1gUNIsM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eXBVYSeIS1gUNIsM .cluster text{fill:#333;}#mermaid-svg-eXBVYSeIS1gUNIsM .cluster span{color:#333;}#mermaid-svg-eXBVYSeIS1gUNIsM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eXBVYSeIS1gUNIsM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-eXBVYSeIS1gUNIsM .emph>*{fill:#f9f!important;stroke:#333!important;stroke-width:2px!important;}#mermaid-svg-eXBVYSeIS1gUNIsM .emph span{fill:#f9f!important;stroke:#333!important;stroke-width:2px!important;} state变化&触发渲染 初始化 input 输入值 Author不重新渲染 (prop未变化) consoleValue 因useCallback被缓存 useEffect 执行 ref.current 被赋值 Author组件渲染 useCallback 执行 useEffect 执行 ref.current 被赋值 useState(initialState) useRef(initialValue)initialState:这个参数在首次渲染后被忽略。
🐇 原理分析:为什么没有闭包问题
为了让函数能够访问最新状态,每次重新渲染时都需要重新创建函数,这是无法避免的,这也是闭包的本质,与 React 无关;利用 Ref 是一个可变对象这一特性,从而摆脱 “过期闭包” 的问题。我们可以在过期闭包之外更改 ref.current,然后在闭包之内访问它,就可以获取最新的数据。通过 useRef 和 useEffect 动态更新引用的函数,避免了闭包问题。consoleValue 函数虽然在整个组件生命周期中保持不变,但它通过调用 ref.current 来间接访问最新的 val 值。
1️⃣ 传递依赖项数组: 初始渲染后以及依赖项变更的重新渲染后 运行
useEffect(() => { // ... }, [a, b]); // 如果 a 或 b 不同则会再次运行2️⃣ 传递空依赖项数组:仅在 初始渲染后 运行
useEffect(() => { // ... }, []); // 不会再次运行(开发环境下除外)3️⃣ 不传递依赖项数组:每次渲染之后 运行
useEffect(() => { // ... }); // 总是再次运行性能:React实战优化技巧之函数闭包由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“性能:React实战优化技巧之函数闭包”