基础类型看值、引用类型看地址——浅比较规则 + 5 个场景拆解重渲染时机
我们在开发中,对于一些开销较大且 props 不经常变动的子组件,通常使用 memo 优化处理。默认情况下,当父组件重渲染时,其所有子组件都会无条件跟随重渲染,无论子组件的 props 是否发生变化。React.memo 的作用就是:对被包裹的函数组件进行"props 浅比较",如果 props 没有变化,则阻止子组件重渲染。
// 未使用 memo:父组件重渲染时,Child 必然重渲染
const Child = (props) => { ... }
// 使用 memo:仅当 props 变化时,Child 才重渲染
const MemoizedChild = React.memo(Child);
React.memo 默认使用浅比较(shallow comparison)来判断 props 是否变化:
| 类型 | 比较规则 | 示例 |
|---|---|---|
| 基本类型(string/number/boolean/null/undefined) | 比较值是否相等 | count 从 5 变为 5 → 没变化;name 从 "abc" 变为 "abc" → 没变化 |
| 引用类型(object/array/function) | 比较引用地址是否相同 | 内容相同但每次新建对象 → 引用变化 → 判定为变化 |
// 父组件
const Parent = () => {
// 每次 Parent 重渲染,都会创建新的对象(引用地址变化)
const user = { name: "张三" };
return <MemoizedChild user={user} />;
};
// 子组件(被 memo 包裹)
const Child = ({ user }) => { ... };
const MemoizedChild = React.memo(Child);
函数也是引用类型,同样遵循"引用地址比较"规则。如果父组件在渲染时动态创建函数,会导致子组件 props 中函数的引用变化,进而触发重渲染:
// 父组件
const Parent = () => {
// 每次重渲染都会创建新的函数(引用变化)
const handleClick = () => { console.log("点击"); };
return <MemoizedChild onClick={handleClick} />;
};
解决办法:使用 useCallback 缓存函数引用,确保每次渲染时函数引用不变:
import { useCallback } from 'react';
const Parent = () => {
// 用 useCallback 缓存函数,引用地址不变
const handleClick = useCallback(() => {
console.log("点击");
}, []); // 依赖为空,函数始终不变
return <MemoizedChild onClick={handleClick} />;
};
如果默认的浅比较满足不了需求(比如需要深比较对象内容),可以给 React.memo 传递第二个参数(一个比较函数),自定义判断逻辑:
// 自定义比较函数:比较 user 对象的内容是否相同
const arePropsEqual = (prevProps, nextProps) => {
return prevProps.user.name === nextProps.user.name;
};
// 使用自定义比较函数的子组件
const MemoizedChild = React.memo(Child, arePropsEqual);
// 子组件(被 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(基础类型)点击按钮变化 | 判定为「变化」 |
| user(对象)内容不变但每次新建 | 判定为「变化」 |
// 父组件改进:用 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>
);
};
// 子组件
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} />;
};
// 父组件改进:用 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} />;
};
// 子组件
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>
);
};
| 注意点 | 说明 |
|---|---|
| 只优化纯函数组件 | memo 仅对函数组件有效,且组件必须是「纯函数」(相同 props 始终返回相同 UI) |
| 不影响自身 state 导致的重渲染 | 子组件自身 state 或 context 变化时,即使 memo 包裹仍然会重渲染(memo 只处理 props 变化) |
| 避免过度使用 | 比较 props 本身有性能成本,渲染成本低的组件使用 memo 可能得不偿失 |
| 术语 | 定义 |
|---|---|
| HOC(高阶组件) | 接收组件并返回新组件的函数,React.memo 即为此类 |
| 浅比较(shallow comparison) | 只比较第一层:基本类型比值、引用类型比地址 |
| useMemo | 缓存计算值/对象引用的 Hook,依赖不变时引用不变 |
| useCallback | 缓存函数引用的 Hook,依赖不变时函数引用不变 |
| arePropsEqual | React.memo 第二参数:自定义 props 比较函数 |