【react深入】useCallback与useMemo
继续react API的深入学习,读官网可得,useCallback与useMemo都是用来优化页面性能的。主要实现的原理(个人理解):
1.react中缓存了该函数(用useCallback)
2.因为react中的组件更新,会引发子组件的全部更新,所以当父组件更新时,有必要控制子组件的更新频率来优化性能
3.常用场景:
1)useCallback:当父组件传递一个函数到子组件时,因为函数每次调用都会返回一个新的值(函数是堆内存,内存中只有引用地址,每调用一次会生成一个新的返回值),导致子组件就算用useMemo包裹也会失效,重新执行了一次,这时父组件中的函数就需要用useCallback缓存
2)useMemo:当子组件中有大量的计算或者消耗性能的操作时需要用到useMemo包裹,防止传入的Props值改变导致子组件再次更新
React 核心 Hook 学习笔记:useCallback 与 useMemo 深度解析
一、前置认知:为什么需要这两个 Hook?
1. React 渲染的 “痛点”
- 组件重渲染触发条件:自身 state/props 变化、父组件重渲染、Context 值变化;
- 重渲染的副作用:组件内函数 / 变量会重新创建(即使逻辑 / 值相同,引用也会改变);
- 性能损耗点:若这些函数 / 变量作为 props 传给子组件,会导致子组件 “无意义重渲染”(尤其复杂子组件,浪费 CPU 资源)。
2. 核心解决思路
useCallback 与 useMemo 通过 “缓存引用 / 值” 打破 “重渲染→重新创建→子组件误渲染” 的链条,本质是「空间换时间」的性能优化方案。
二、基础用法:语法与核心差异
1. useCallback:缓存函数引用
|
要素 |
说明 |
|
语法 |
const 缓存函数 = useCallback(函数体, [依赖数组]) |
|
核心作用 |
稳定函数引用,避免函数在组件重渲染时频繁重建 |
|
关键逻辑 |
仅当依赖数组中的变量变化时,才生成新的函数引用;否则复用缓存的函数引用 |
|
典型返回值 |
缓存的函数本身(而非函数执行结果) |
示例代码:
|
// 仅当 count 变化时,handleClick 才重新创建 const handleClick = useCallback(() => { console.log("当前 count:", count); }, [count]); |
2. useMemo:缓存计算结果 / 值
|
要素 |
说明 |
|
语法 |
const 缓存值 = useMemo(计算函数, [依赖数组]) |
|
核心作用 |
避免重复执行耗时计算,或稳定引用类型值(如对象、数组)的引用 |
|
关键逻辑 |
仅当依赖数组变化时,才重新执行计算函数;否则复用缓存的计算结果 |
|
典型返回值 |
计算函数的返回值(可能是基本类型,也可能是引用类型) |
示例代码:
|
// 仅当 list/filterKey 变化时,才重新执行过滤+排序(耗时操作) const filteredList = useMemo(() => { return list.filter(item => item.name.includes(filterKey)).sort((a, b) => a.id - b.id); }, [list, filterKey]); |
3. 核心差异对比表
|
对比维度 |
useCallback |
useMemo |
|
缓存对象 |
函数引用 |
计算结果 / 值(含引用类型) |
|
适用场景 |
函数作为 props 传递、函数作为 Hook 依赖 |
耗时计算、稳定引用类型值 |
|
优化目标 |
减少子组件无意义重渲染 |
提升组件自身执行效率,或稳定子组件 props |
|
常见搭配 |
与 React.memo 配合使用 |
单独使用(优化计算)或与 React.memo 配合 |
三、实战场景:精准判断 “用不用”
1. 必须使用的场景
(1)useCallback 必用场景
- 场景 A:函数作为 props 传给「被 React.memo 包裹的子组件」
若不缓存函数,父组件重渲染会生成新函数引用,导致 React.memo 失效,子组件误渲染(如上文 “子组件按钮案例”)。
- 场景 B:函数作为依赖传入其他 Hook(如 useEffect、useMemo)
若不缓存函数,函数每次重建会触发依赖变化,导致 Hook 频繁执行(如上文 “监听窗口 resize 案例”)。
(2)useMemo 必用场景
- 场景 A:组件内有 “耗时计算”
如大数据量过滤、排序、循环处理(若不缓存,每次重渲染都会重复执行,导致卡顿)。
- 场景 B:引用类型值作为 props 传给子组件
若不缓存,每次重渲染会创建新的引用类型(如 const obj = { name: "张三" }),导致子组件误渲染(如上文 “用户信息传递案例”)。
2. 无需使用的场景(滥用反降性能)
- 场景 A:简单函数 / 计算
如无依赖的简单点击事件(() => setCount(count + 1))、基础变量赋值(const num = a + b),缓存的 “内存占用 + 依赖比较” 成本大于收益。
- 场景 B:函数 / 值仅在父组件内部使用
若函数不传递给子组件,也不作为其他 Hook 的依赖,重建不会影响外部,无需缓存。
- 场景 C:依赖项频繁变化
如依赖 “每次渲染都会新建的引用类型”(const obj = { key: "value" }),缓存会每次失效,相当于 “白缓存”,还浪费内存。
四、底层原理:理解 “缓存的本质”
1. 核心机制:依赖浅比较 + 缓存槽
- 缓存槽存储:React 为每个 useCallback/useMemo 分配独立 “缓存槽”,存储在组件的 Fiber 节点中,内容包括「缓存结果」和「上一次的依赖数组」。
- 重渲染时的判断逻辑:
- 对比当前依赖数组与缓存槽中的 “上一次依赖数组”(浅比较);
- 若依赖完全相同(浅比较相等),直接返回缓存结果;
- 若依赖不同,重新执行函数 / 计算,更新缓存槽中的结果和依赖数组。
2. 关键细节:浅比较规则
- 基本类型(string/number/boolean/null/undefined):比较值是否相等(如 2 === 2、"a" === "a");
- 引用类型(object/array/function):比较引用地址是否相同(如 {} !== {}、[] !== []);
- 注意点:依赖数组是 “全量比较”,只要有一个依赖项变化,就会触发缓存更新。
3. 与 React.memo 的配合逻辑
- React.memo:组件级缓存,浅比较 props 决定是否重渲染(“拦截重渲染的大门”);
- useCallback/useMemo:值 / 函数级缓存,确保传递给子组件的 props 引用稳定(“保证大门不被误触发”);
- 黄金组合:只有二者配合,才能真正实现 “子组件按需重渲染”,缺一不可。
五、避坑指南:常见误区与解决方案
1. 误区 1:依赖数组 “漏加 / 多加” 依赖
- 错误表现:
- 漏加依赖:函数内使用的变量未加入依赖数组,导致闭包陷阱(如 useCallback(() => console.log(count), []),count 变化后仍打印初始值);
- 多加依赖:加入未使用的变量,导致缓存频繁失效(如 useCallback(() => console.log(count), [count, text]),text 变化也会重建函数)。
- 解决方案:
- 严格遵循 “函数内用什么,依赖数组加什么”;
- 配合 ESLint 规则 react-hooks/exhaustive-deps,自动检测依赖问题。
2. 误区 2:认为 “缓存能阻止函数执行”
- 错误认知:用 useCallback 缓存函数后,函数就不会重新执行;
- 正确认知:useCallback 仅缓存函数引用,函数是否执行取决于 “是否被调用”(如点击按钮触发),它不影响函数的执行逻辑,只影响函数的引用稳定性。
3. 误区 3:缓存 “频繁变化的引用类型”
- 错误示例:
|
const obj = { key: "value" }; // 每次渲染新建对象(引用变化) const cachedFn = useCallback(() => console.log(obj.key), [obj]); // 缓存无效 |
- 问题本质:依赖项(obj)每次渲染都重建,导致缓存每次失效,相当于 “白缓存”;
- 解决方案:先用 useMemo 缓存引用类型,再将缓存后的结果作为依赖:
|
const obj = useMemo(() => ({ key: "value" }), []); // 缓存 obj 引用 const cachedFn = useCallback(() => console.log(obj.key), [obj]); // 依赖稳定 |
4. 误区 4:混淆二者使用场景
- 错误行为:用 useCallback 缓存计算结果(如 useCallback(() => a + b, [a, b])),或用 useMemo 缓存函数(如 useMemo(() => () => console.log(count), [count]));
- 记忆口诀:“Callback 存函数,Memo 存结果”,虽然后者偶尔能实现类似效果,但不符合设计初衷,且易造成代码混淆。
六、进阶技巧:优化效果最大化
1. 依赖数组精简:避免 “冗余依赖”
- 若函数依赖 “对象的某个属性”,而非整个对象,直接依赖属性而非对象,减少缓存失效频率:
|
// 优化前:依赖整个 user 对象,user 任何属性变化都会重建函数 const handleClick = useCallback(() => console.log(user.name), [user]); // 优化后:仅依赖 user.name,只有 name 变化才重建函数 const handleClick = useCallback(() => console.log(user.name), [user.name]); |
2. 多层组件传递:“顶层缓存,下层复用”
- 若函数 / 值需经过多层组件传递(如祖父→父→子),在 “最顶层组件” 缓存,避免中间层组件重建导致缓存失效(如上文 “Grandparent→Parent→Child 案例”)。
3. 与 React 编译器(React Forget)的兼容
- React Forget(实验阶段)可自动识别需缓存的函数 / 值,生成 useCallback/useMemo 代码,减少手动样板;
- 但编译器仍需遵循 “纯函数 / 无副作用” 原则,复杂场景(如含副作用的函数)仍需手动缓存,因此理解底层原理仍是关键。
七、核心要点速记(复习用)
- 目标:useCallback 稳函数引用,useMemo 省计算 / 稳值引用;
- 原则:“需要才用”,不滥用(简单场景、内部使用、依赖频繁变,不用);
- 搭配:useCallback/useMemo + React.memo,优化子组件渲染;
- 依赖:“用啥加啥”,避闭包,禁冗余;
- 原理:依赖浅比较 + Fiber 缓存槽,变则更,不变则复用。
(注:文档部分内容可能由 AI 生成)
更多推荐
所有评论(0)