React Hooks 进阶篇:useMemo / useCallback / useReducer / useImperativeHandle,用「算账 +

React Hooks 进阶篇:useMemo / useCallback / useReducer / useImperativeHandle,用「算账 + 审批」讲透

💡 承接基础篇。基础篇讲了 4 个基础 Hook(useState / useEffect / useRef / useContext),这一篇讲 4 个进阶 Hook。它们是性能优化和面试的高频考点,同样用生活比喻讲透。

一、先记住进阶篇的四个"工具"

Hook 比喻 一句话作用
useMemo 🧮 账本 算过的账别重算
useCallback 📇 固定名片 别每次渲染都换手机号
useReducer 📋 审批中心 改状态走流程,集中处理
useImperativeHandle 🎛️ 受控按钮 给子组件开几个指定开关

二、useMemo:把算过的账记在账本上

1. 痛点:每次渲染都在"白算一遍"

先看这段代码:

jsx 复制代码
function TodoList({ todos }) {
  const [keyword, setKeyword] = useState('');

  // ❌ 每次渲染都会重新执行 filter,哪怕 keyword 根本没变
  const filtered = todos.filter(t => t.title.includes(keyword));

  return <ul>{filtered.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}

假设 todos 有一万条,你只是在输入框里敲了一个字,组件重新渲染,这 1 万条数据就要从头过滤一遍------哪怕过滤结果其实没变。

这就像你每次进门都要重新心算一遍这个月的水电费总额,明明"单价"和"用量"都没变,纯属浪费脑细胞。

2. 解法:用 useMemo 把结果"记下来"

jsx 复制代码
import { useMemo, useState } from 'react'; 
function TodoList({ todos }) {
  const [keyword, setKeyword] = useState('');

  // ✅ 只有 todos 或 keyword 变化时,才重新计算;否则直接返回上次的结果
  const filtered = useMemo(() => {
    console.log('正在过滤...'); // 只有真正重算时才会打印
    return todos.filter(t => t.title.includes(keyword));
  }, [todos, keyword]); // 👈 依赖数组

  return <ul>{filtered.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}

useMemo 的写法拆开看:

jsx 复制代码
const 结果 = useMemo(() => 计算函数, [依赖]);
  • 第一次渲染:执行计算,存下结果;
  • 之后渲染:依赖没变 → 直接返回缓存的结果,计算函数根本不执行
  • 依赖变了 → 重新计算。

3. 另一个隐藏用途:保持"引用稳定"

useMemo 不只是为了省计算,还常用来让一个对象/数组的引用保持不变 (配合 React.memo 时很重要):

jsx 复制代码
// ❌ 每次渲染都 new 一个新对象,引用变了
const config = { theme: 'dark', size: 'large' };

// ✅ 只有依赖变化时,才产生新对象,否则一直用同一个引用
const config = useMemo(
  () => ({ theme: 'dark', size: 'large' }),
  []
);

⚠️ 别滥用 :如果计算本身很便宜(比如过滤 3 条数据),用 useMemo 反而增加了"比较依赖 + 缓存管理"的开销,可能更慢。先测量、再优化。


三、useCallback:别每次渲染都"换手机号"

1. 一个隐蔽的坑:函数每次渲染都是"新函数"

函数组件里定义的函数,每次渲染都会重新创建,引用地址会变:

jsx 复制代码
function Parent() {
  const handleClick = () => console.log('clicked');
  // 这个函数每次渲染都是一个新的,引用不一样
}

平时没事,但当这个函数被传给一个用 React.memo 包裹的子组件时,问题就来了------子组件会因为"函数引用变了"而白白重渲染

jsx 复制代码
import { memo, useState } from 'react';

// memo 包裹:props 不变,子组件就不重渲染
const Child = memo(function Child({ onClick }) {
  console.log('子组件渲染了');
  return <button onClick={onClick}>点我</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // ❌ 每次父组件渲染,都产生新函数 → Child 的 props 变了 → 跟着重渲染
  const handleClick = () => console.log('clicked');

  return (
    <>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>父组件 +1:{count}</button>
    </>
  );
}

你点父组件的按钮时,会看到控制台不停打印"子组件渲染了"------可子组件跟 count 半毛钱关系都没有,纯属被误伤。

2. 解法:用 useCallback 把函数"固定住"

jsx 复制代码
import { memo, useCallback, useState } from 'react';

const Child = memo(function Child({ onClick }) {
  console.log('子组件渲染了');
  return <button onClick={onClick}>点我</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // ✅ 缓存函数:依赖 [] 不变,永远返回同一个函数引用
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>父组件 +1:{count}</button>
    </>
  );
}

现在父组件更新 count 时,handleClick 的引用没变,Child 的 props 没变,memo 就会帮它"偷懒"不重渲染。

3. useMemo 和 useCallback 到底啥区别?

这是面试必考题,其实一句话:

useCallback(fn, deps) 本质上就是 useMemo(() => fn, deps)

  • useMemo 缓存的是函数的返回值(一个值)
  • useCallback 缓存的是函数本身

就像:useMemo 是把"算出来的结果"记在账本上,useCallback 是把"那个会算账的人"固定下来,别每次换人。

⚠️ 同样别滥用 :单独用 useCallback 而不配合 React.memo,基本是"优化了个寂寞",还多了一层开销。


四、useReducer:把「改状态」送进审批中心

1. 什么时候 useState 不够用了

useState 很轻量,但当状态逻辑复杂时,代码会变得难维护:

jsx 复制代码
// ❌ 多个状态互相纠缠,set 散落各处,逻辑不清
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const [history, setHistory] = useState([]);

// 加一减一、重置、步进......逻辑全堆在事件里

useReducer 就是为"复杂的状态逻辑 "准备的:把所有"怎么改状态"的规则,集中到一个 reducer 函数里。

2. 核心概念:dispatch 一个"动作",reducer 负责处理

jsx 复制代码
import { useReducer } from 'react';

// ① reducer:接收「当前状态 + 动作」,返回「新状态」(纯函数)
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: action.payload }; // payload 携带数据
    default:
      return state;
  }
}

function Counter() {
  // ② 传入 reducer 和初始状态
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <>
      <p>计数:{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset', payload: 0 })}>重置</button>
    </>
  );
}

拆解三个核心:

概念 是什么 比喻
reducer 纯函数 (state, action) => newState 审批中心的规则表
action { type, payload } 你填的"审批单"
dispatch 派发动作的函数 把审批单投进信箱

3. 什么时候选 useReducer?

  • 多个状态相互关联、需要一起变(比如表单里十几个字段联动)
  • 状态更新逻辑复杂(一堆 if/switch 堆在事件里)
  • 逻辑需要复用或写单元测试

如果只是"一个数字 +1",老老实实用 useState 就好,杀鸡别用牛刀。


五、useImperativeHandle:给子组件开几个「受控按钮」

1. 痛点:父组件想"命令"子组件做事

默认情况下,ref 只能拿到原生 DOM 元素 。如果你给一个自定义组件传 ref,是拿不到它的内部方法的:

jsx 复制代码
const Child = (props) => {
  return <input />;
};

function Parent() {
  const childRef = useRef(null);
  // ❌ 拿不到 Child 的内部输入框,没法主动 .focus()
  return <Child ref={childRef} />;
}

有些场景父组件确实需要主动"命令"子组件:聚焦输入框、滚动到某个位置、触发动画、调子组件的方法......

这时就要用 forwardRef + useImperativeHandle 组合拳。

2. 解法:子组件"开几个指定按钮"给父组件按

jsx 复制代码
import { forwardRef, useImperativeHandle, useRef } from 'react';

// ① forwardRef:让子组件能接收 ref
const Input = forwardRef(function Input(props, ref) {
  const inputRef = useRef(null);

  // ② useImperativeHandle:决定对外暴露哪些方法
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
  }));

  return <input ref={inputRef} />;
});

function Parent() {
  const childRef = useRef(null);

  return (
    <>
      <Input ref={childRef} />
      <button onClick={() => childRef.current.focus()}>聚焦</button>
      <button onClick={() => childRef.current.clear()}>清空</button>
    </>
  );
}

比喻:子组件是一个黑盒useImperativeHandle 就是"在盒子上开几个指定按钮"。父组件只能按这几个按钮(focusclear),盒子内部的东西碰不到------这样既给了控制权,又保护了内部实现。

⚠️ 尽量少用 :能用 props 通信就别用它。它让数据流变得"隐晦",不好追踪,容易写出难以维护的代码。只有聚焦、滚动、动画这种"命令式操作"才值得用它。


六、一张表收下全部重点

Hook 解决什么 一句话记忆
useMemo 缓存计算结果 算过的账别重算
useCallback 缓存函数引用 别每次换手机号
useReducer 复杂状态逻辑 改状态走审批
useImperativeHandle 暴露子组件方法 开几个受控按钮

七、写在最后

进阶篇的四个 Hook,本质都在解决**"性能""复杂逻辑"**两件事:

  • useMemo / useCallback:让组件别做无谓的重算和重渲染
  • useReducer:让复杂状态变得有章可循;
  • useImperativeHandle:在极少数命令式场景里,安全地暴露子组件能力。

不过要牢记一条铁律:它们都是"锦上添花",不是"雪中送炭"。先保证代码写对、写清晰,再考虑优化------过度优化是 React 新手最容易踩的坑之一。


📝 本文基于 React 16.8+,适用于 React 18 / 19。 🔗 官方文档:react.dev/reference/r...

相关推荐
Jackson__1 小时前
面试官:如何在老项目中使用新框架,并实现通信?
前端·javascript·面试
拾年2751 小时前
React Hooks 保姆级教程:把组件想象成失忆的打工人,4 个 Hook 带你原地起飞
前端·javascript·react.js
mmsx1 小时前
一个 Bug 修了 12 轮,根因只有一句话:AI 编程时代,日志才是唯一的真相
前端
默_笙1 小时前
😝 5 亿次循环卡死页面?我用 Web Worker 把它扔进后台线程,页面丝滑如初
前端·javascript
CAD老兵1 小时前
一行代码集成 DWG/DXF 图纸查看:测量批注,数据不出站
前端·javascript·github
小林ixn1 小时前
单例模式:从弹窗管理到全局状态,一个模式搞定
前端·javascript·设计模式
AI编程实验室1 小时前
Node.js Agent Handoff 仓库扫描 MVP:忽略规则、include 通配与稳定输出实现
前端·ai编程
猫不易1 小时前
从 Virtual DOM 到 Vapor:Vue 3.6 的另一条路
前端·vue.js
小林ixn1 小时前
在浏览器里跑 DeepSeek-R1:WebGPU + Transformers.js 实战
react.js·llm·浏览器