继续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 渲染的 “痛点”
  1. 组件重渲染触发条件:自身 state/props 变化、父组件重渲染、Context 值变化;
  2. 重渲染的副作用:组件内函数 / 变量会重新创建(即使逻辑 / 值相同,引用也会改变);
  3. 性能损耗点:若这些函数 / 变量作为 props 传给子组件,会导致子组件 “无意义重渲染”(尤其复杂子组件,浪费 CPU 资源)。
2. 核心解决思路

useCallbackuseMemo 通过 缓存引用 / 值” 打破 “重渲染→重新创建→子组件误渲染” 的链条,本质是「空间换时间」的性能优化方案。

二、基础用法:语法与核心差异

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 必用场景
  1. 场景 A:函数作为 props 传给「被 React.memo 包裹的子组件」

若不缓存函数,父组件重渲染会生成新函数引用,导致 React.memo 失效,子组件误渲染(如上文 “子组件按钮案例”)。

  1. 场景 B:函数作为依赖传入其他 Hook(如 useEffect、useMemo)

若不缓存函数,函数每次重建会触发依赖变化,导致 Hook 频繁执行(如上文 “监听窗口 resize 案例”)。

(2)useMemo 必用场景
  1. 场景 A:组件内有 “耗时计算”

如大数据量过滤、排序、循环处理(若不缓存,每次重渲染都会重复执行,导致卡顿)。

  1. 场景 B:引用类型值作为 props 传给子组件

若不缓存,每次重渲染会创建新的引用类型(如 const obj = { name: "张三" }),导致子组件误渲染(如上文 “用户信息传递案例”)。

2. 无需使用的场景(滥用反降性能)
  1. 场景 A:简单函数 / 计算

如无依赖的简单点击事件(() => setCount(count + 1))、基础变量赋值(const num = a + b),缓存的 “内存占用 + 依赖比较” 成本大于收益。

  1. 场景 B:函数 / 值仅在父组件内部使用

若函数不传递给子组件,也不作为其他 Hook 的依赖,重建不会影响外部,无需缓存。

  1. 场景 C:依赖项频繁变化

如依赖 “每次渲染都会新建的引用类型”(const obj = { key: "value" }),缓存会每次失效,相当于 “白缓存”,还浪费内存。

四、底层原理:理解 “缓存的本质”

1. 核心机制:依赖浅比较 + 缓存槽
  1. 缓存槽存储:React 为每个 useCallback/useMemo 分配独立 “缓存槽”,存储在组件的 Fiber 节点中,内容包括「缓存结果」和「上一次的依赖数组」。
  2. 重渲染时的判断逻辑
    1. 对比当前依赖数组与缓存槽中的 “上一次依赖数组”(浅比较);
    2. 若依赖完全相同(浅比较相等),直接返回缓存结果;
    3. 若依赖不同,重新执行函数 / 计算,更新缓存槽中的结果和依赖数组。
2. 关键细节:浅比较规则
  1. 基本类型(string/number/boolean/null/undefined:比较值是否相等(如 2 === 2"a" === "a");
  2. 引用类型(object/array/function:比较引用地址是否相同(如 {} !== {}[] !== []);
  3. 注意点:依赖数组是 “全量比较”,只要有一个依赖项变化,就会触发缓存更新。
3. 与 React.memo 的配合逻辑
  1. React.memo:组件级缓存,浅比较 props 决定是否重渲染(“拦截重渲染的大门”);
  2. useCallback/useMemo:值 / 函数级缓存,确保传递给子组件的 props 引用稳定(“保证大门不被误触发”);
  3. 黄金组合:只有二者配合,才能真正实现 “子组件按需重渲染”,缺一不可。

五、避坑指南:常见误区与解决方案

1. 误区 1:依赖数组 “漏加 / 多加” 依赖
  1. 错误表现
    1. 漏加依赖:函数内使用的变量未加入依赖数组,导致闭包陷阱(如 useCallback(() => console.log(count), []),count 变化后仍打印初始值);
    2. 多加依赖:加入未使用的变量,导致缓存频繁失效(如 useCallback(() => console.log(count), [count, text]),text 变化也会重建函数)。
  2. 解决方案
    1. 严格遵循 “函数内用什么,依赖数组加什么”;
    2. 配合 ESLint 规则 react-hooks/exhaustive-deps,自动检测依赖问题。
2. 误区 2:认为 “缓存能阻止函数执行”
  1. 错误认知:用 useCallback 缓存函数后,函数就不会重新执行;
  2. 正确认知:useCallback 仅缓存函数引用,函数是否执行取决于 “是否被调用”(如点击按钮触发),它不影响函数的执行逻辑,只影响函数的引用稳定性。
3. 误区 3:缓存 “频繁变化的引用类型”
  1. 错误示例

const obj = { key: "value" }; // 每次渲染新建对象(引用变化)

const cachedFn = useCallback(() => console.log(obj.key), [obj]); // 缓存无效

  1. 问题本质:依赖项(obj)每次渲染都重建,导致缓存每次失效,相当于 “白缓存”;
  2. 解决方案:先用 useMemo 缓存引用类型,再将缓存后的结果作为依赖:

const obj = useMemo(() => ({ key: "value" }), []); // 缓存 obj 引用

const cachedFn = useCallback(() => console.log(obj.key), [obj]); // 依赖稳定

4. 误区 4:混淆二者使用场景
  1. 错误行为:用 useCallback 缓存计算结果(如 useCallback(() => a + b, [a, b])),或用 useMemo 缓存函数(如 useMemo(() => () => console.log(count), [count]));
  2. 记忆口诀:“Callback 存函数,Memo 存结果”,虽然后者偶尔能实现类似效果,但不符合设计初衷,且易造成代码混淆。

六、进阶技巧:优化效果最大化

1. 依赖数组精简:避免 “冗余依赖”
  1. 若函数依赖 “对象的某个属性”,而非整个对象,直接依赖属性而非对象,减少缓存失效频率:

// 优化前:依赖整个 user 对象,user 任何属性变化都会重建函数

const handleClick = useCallback(() => console.log(user.name), [user]);

// 优化后:仅依赖 user.name,只有 name 变化才重建函数

const handleClick = useCallback(() => console.log(user.name), [user.name]);

2. 多层组件传递:“顶层缓存,下层复用”
  1. 若函数 / 值需经过多层组件传递(如祖父→父→子),在 “最顶层组件” 缓存,避免中间层组件重建导致缓存失效(如上文 “Grandparent→Parent→Child 案例”)。
3. 与 React 编译器(React Forget)的兼容
  1. React Forget(实验阶段)可自动识别需缓存的函数 / 值,生成 useCallback/useMemo 代码,减少手动样板;
  2. 但编译器仍需遵循 “纯函数 / 无副作用” 原则,复杂场景(如含副作用的函数)仍需手动缓存,因此理解底层原理仍是关键。

七、核心要点速记(复习用)

  1. 目标:useCallback 稳函数引用,useMemo 省计算 / 稳值引用;
  2. 原则:“需要才用”,不滥用(简单场景、内部使用、依赖频繁变,不用);
  3. 搭配:useCallback/useMemo + React.memo,优化子组件渲染;
  4. 依赖:“用啥加啥”,避闭包,禁冗余;
  5. 原理:依赖浅比较 + Fiber 缓存槽,变则更,不变则复用。

(注:文档部分内容可能由 AI 生成)

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐