背景
我们在开发中,对于一些开销较大且props
不经常变动的子组件,通常使用的 memo
优化处理,但是真的了解memo
的更新时机吗,下面基于 基础类型
引用类型
详细解释下 添加了memo
的子组件更新时机
React.memo介绍
在 React 中,React.memo
是一个用于优化函数组件性能的高阶组件(HOC),其核心作用是减少不必要的重渲染。理解它的工作机制需要从「props 比较逻辑」和「重渲染触发条件」两方面展开
一、React.memo
的基本作用
默认情况下,当父组件重渲染时,其所有子组件都会无条件跟随重渲染,无论子组件的 props
是否发生变化。这在某些场景下会造成性能浪费(比如子组件渲染成本高,且 props
很少变化)。
React.memo
的作用就是:对被包裹的函数组件进行「props 浅比较」,如果 props 没有变化,则阻止子组件重渲染。
js
// 未使用 memo:父组件重渲染时,Child 必然重渲染
const Child = (props) => { ... }
// 使用 memo:仅当 props 变化时,Child 才重渲染
const MemoizedChild = React.memo(Child);
二、关键问题:如何判断「props 没变化」?
React.memo
默认使用「浅比较(shallow comparison)」来判断 props 是否变化,具体规则如下:
1. 对于基本类型(string/number/boolean/null/undefined)
比较的是值是否相等。例如:
props.count
从5
变为5
→ 认为没变化(不重渲染)props.name
从"abc"
变为"abc"
→ 认为没变化(不重渲染)
2. 对于引用类型(object/array/function 等)
比较的是引用地址是否相同,而不是内容是否相同。例如
js
// 父组件
const Parent = () => {
// 每次 Parent 重渲染,都会创建新的对象(引用地址变化)
const user = { name: "张三" };
return <MemoizedChild user={user} />;
};
// 子组件(被 memo 包裹)
const Child = ({ user }) => { ... };
const MemoizedChild = React.memo(Child);
此时,即使 user
的内容始终是 {name: "张三"}
,但每次父组件重渲染都会创建新对象(引用地址不同),memo
会认为 props
变化了,导致子组件仍然重渲染。
三、特殊情况:函数 / 事件处理函数
函数也是引用类型,同样遵循「引用地址比较」规则。如果父组件在渲染时动态创建函数 ,会导致子组件的 props
中函数的引用变化,进而触发重渲染:
js
// 父组件
const Parent = () => {
// 每次重渲染都会创建新的函数(引用变化)
const handleClick = () => { console.log("点击"); };
return <MemoizedChild onClick={handleClick} />;
};
解决办法:使用 useCallback
缓存函数引用,确保每次渲染时函数引用不变:
javascript
import { useCallback } from 'react';
const Parent = () => {
// 用 useCallback 缓存函数,引用地址不变
const handleClick = useCallback(() => {
console.log("点击");
}, []); // 依赖为空,函数始终不变
return <MemoizedChild onClick={handleClick} />;
};
四、自定义比较逻辑
如果默认的浅比较满足不了需求(比如需要深比较对象内容),可以给 React.memo
传递第二个参数(一个比较函数),自定义判断逻辑:
js
// 自定义比较函数:比较 user 对象的内容是否相同
const arePropsEqual = (prevProps, nextProps) => {
return prevProps.user.name === nextProps.user.name;
};
// 使用自定义比较函数的子组件
const MemoizedChild = React.memo(Child, arePropsEqual);
此时,即使 user
对象的引用变化,但只要 name
字段相同,子组件就不会重渲染。
五、注意事项
- 只优化纯函数组件 :
memo
仅对函数组件有效,且组件必须是「纯函数」(相同 props 始终返回相同 UI)。 - 不影响自身 state 导致的重渲染 :如果子组件自身的
state
或context
变化,即使memo
包裹,仍然会重渲染(memo
只处理 props 变化)。 - 避免过度使用 :比较 props 本身有性能成本,对于渲染成本低的组件,使用
memo
可能得不偿失。
使用场景
场景 1:props 包含基础类型 + 对象(对象引用变化)
javascript
// 子组件(被 memo 包裹)
const MemoizedChild = React.memo(({ count, user }) => {
console.log("子组件渲染了");
return <div>{count} - {user.name}</div>;
});
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
// 每次父组件重渲染,都会创建新对象(引用变化)
const user = { name: "张三" };
return (
<div>
<button onClick={() => setCount(count + 1)}>加1</button>
<MemoizedChild count={count} user={user} />
</div>
);
};
变化分析:
count
是基础类型,当点击按钮时count
值变化 → 判定为「变化」user
是对象,即使内容不变,每次父组件渲染都会创建新对象(引用变化)→ 判定为「变化」
结果:
- 初始渲染:子组件渲染
- 点击按钮(
count
变化):子组件重渲染(因count
变化) - 父组件因其他原因重渲染(如父组件自身
state
变化但count
不变):子组件仍重渲染(因user
引用变化)
场景 2:props 包含基础类型 + 对象(对象引用不变)
js
// 父组件改进:用 useMemo 缓存对象引用
const Parent = () => {
const [count, setCount] = useState(0);
// 用 useMemo 缓存对象,依赖为空时引用不变
const user = useMemo(() => ({ name: "张三" }), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>加1</button>
<MemoizedChild count={count} user={user} />
</div>
);
};
变化分析:
count
变化时 → 判定为「变化」user
被useMemo
缓存,引用始终不变 → 判定为「未变化」
结果:
- 当
count
变化时:子组件重渲染(因count
变化) - 当
count
不变时:子组件不重渲染(所有props
均未变化)
场景 3:props 包含函数(函数引用变化)
js
// 子组件
const MemoizedChild = React.memo(({ onSubmit, config }) => {
console.log("子组件渲染了");
return <button onClick={onSubmit}>{config.text}</button>;
});
// 父组件
const Parent = () => {
const config = useMemo(() => ({ text: "提交" }), []);
// 每次渲染创建新函数(引用变化)
const handleSubmit = () => {
console.log("提交");
};
return <MemoizedChild onSubmit={handleSubmit} config={config} />;
};
变化分析:
config
被缓存,引用不变 → 判定为「未变化」handleSubmit
每次渲染都是新函数(引用变化)→ 判定为「变化]
结果:
- 父组件任何重渲染(如自身
state
变化)都会导致子组件重渲染(因onSubmit
引用变化)
场景 4:props 包含函数(函数引用不变)+ 基础类型(值不变)
arduino
// 父组件改进:用 useCallback 缓存函数
const Parent = () => {
const [text, setText] = useState("");
const config = useMemo(() => ({ text: "提交" }), []);
// 用 useCallback 缓存函数,引用不变
const handleSubmit = useCallback(() => {
console.log("提交:", text);
}, [text]); // 依赖 text,text 变化时函数才更新
return <MemoizedChild onSubmit={handleSubmit} config={config} />;
};
变化分析:
config
引用不变 → 「未变化」handleSubmit
仅当text
变化时引用才变化 → 其他时候「未变化」
结果:
- 当
text
变化时:handleSubmit
引用变化 → 子组件重渲染 - 当
text
不变时:所有props
均未变化 → 子组件不重渲染
场景 5:部分 props 变化,部分不变
javascript
// 子组件
const MemoizedChild = React.memo(({ a, b, c }) => {
console.log("子组件渲染了");
return <div>{a} - {b.name} - {c()}</div>;
});
// 父组件
const Parent = () => {
const [a, setA] = useState(1);
const b = useMemo(() => ({ name: "固定值" }), []); // 不变
const c = useCallback(() => "固定函数", []); // 不变
return (
<div>
<button onClick={() => setA(a + 1)}>修改a</button>
<MemoizedChild a={a} b={b} c={c} />
</div>
);
};
变化分析:
a
变化时 → 「变化」b
和c
始终不变 → 「未变化]
结果:
- 点击按钮(
a
变化):子组件重渲染(只要有一个props
变化就会触发) - 其他情况(
a
不变):子组件不重渲染
结尾总结
-
「一票否决制」 :
props
中只要有任何一项被浅比较判定为「变化」,子组件就会重渲染。 -
基础类型看「值」 :
number/string/boolean
等只要值相同,就判定为「未变化」。 -
引用类型看「地址」 :
object/array/function
等即使内容相同,只要引用地址变化,就判定为「变化」。 -
解决引用类型问题:
- 对象 / 数组:用
useMemo
缓存引用 - 函数:用
useCallback
缓存引用
- 对象 / 数组:用
-
自定义比较逻辑 :如果需要深比较(如比较对象内容),可通过
React.memo
的第二个参数手动实现,但需注意深比较的性能成本。