背景
我们在开发中,对于一些开销较大且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的第二个参数手动实现,但需注意深比较的性能成本。