创见博客
如何用useCallback和useMemo优化useEffect的依赖项
七崽爱吃小饼干2026/01/06阅读 2专栏 React

用 useCallback 和 useMemo 优化 useEffect(或其他 Hook)的依赖项,核心目标是减少不必要的重渲染/Effect 重执行——二者分别针对“函数引用”和“计算结果(对象/数组)”做缓存,让依赖项仅在真正需要变化时才更新。

一、先理解:为什么需要优化依赖项?

React 用 Object.is 做浅比较,引用类型(函数、对象、数组)每次渲染都会生成新引用,即使内容完全相同,也会被判定为“依赖变化”,导致:

  1. useEffect 频繁执行清理+回调(比如定时器被反复创建/清除);
  2. 子组件因 props 引用变化而不必要的重渲染。

而 useCallback(缓存函数)、useMemo(缓存计算结果)的核心作用就是:让引用类型的依赖项,仅在“核心依赖”变化时才更新引用。

二、核心用法:useCallback(优化函数依赖)

1. 基本语法

jsx
const 缓存的函数 = useCallback(
  () => {
    // 函数逻辑(依赖 coreDep1、coreDep2)
  },
  [coreDep1, coreDep2] // 仅当核心依赖变化时,才生成新的函数引用
);
  • 无依赖([]):组件生命周期内函数引用永远不变;
  • 有依赖:仅依赖变化时,函数引用才更新。

2. 实战场景:优化 useEffect 的函数依赖

问题代码(每次渲染都生成新的 handleClick,导致 useEffect 反复执行):

jsx
import { useEffect, useState } from 'react';

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

  // 每次渲染都创建新函数 → 引用变化
  const handleClick = () => {
    console.log('count:', count);
  };

  // 依赖 handleClick → 每次渲染都执行(清理+重建)
  useEffect(() => {
    const timer = setInterval(handleClick, 1000);
    return () => clearInterval(timer);
  }, [handleClick]); // 问题:handleClick 每次都是新引用

  return <button onClick={() => setCount(c => c + 1)}>count: {count}</button>;
}

优化后(用 useCallback 缓存函数,仅 count 变化时更新引用):

jsx
import { useEffect, useState, useCallback } from 'react';

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

  // 仅 count 变化时,才生成新的 handleClick 引用
  const handleClick = useCallback(() => {
    console.log('count:', count);
  }, [count]); // 核心依赖:count

  // 仅 handleClick 引用变化时(即 count 变化),才重建定时器
  useEffect(() => {
    const timer = setInterval(handleClick, 1000);
    return () => clearInterval(timer);
  }, [handleClick]); // 依赖缓存后的函数

  return <button onClick={() => setCount(c => c + 1)}>count: {count}</button>;
}
  • 效果:只有 count 变化时,handleClick 引用才更新,useEffect 才重建定时器;
  • 对比:若 handleClick 无核心依赖(比如不依赖 count),依赖项写 [],则函数引用永远不变,useEffect 仅挂载时执行一次。

三、核心用法:useMemo(优化对象/数组/计算结果依赖)

1. 基本语法

jsx
const 缓存的结果 = useMemo(
  () => {
    // 计算逻辑:返回对象/数组/复杂计算值(依赖 coreDep1、coreDep2)
    return { a: coreDep1, b: coreDep2 };
  },
  [coreDep1, coreDep2] // 仅核心依赖变化时,才重新计算并生成新引用
);
  • 注意:useMemo 是“计算缓存”,不是“单纯的引用缓存”——它会执行回调并缓存返回值;
  • 无依赖([]):仅首次渲染计算一次,后续引用不变。

2. 实战场景1:优化 useEffect 的对象依赖

问题代码(每次渲染生成新对象,导致 useEffect 反复执行):

jsx
import { useEffect, useState } from 'react';

function Demo() {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);

  // 每次渲染创建新对象 → 引用变化
  const config = { a, b };

  // 依赖 config → 每次渲染都执行
  useEffect(() => {
    console.log('config 变化:', config);
  }, [config]);

  return <div>
    <button onClick={() => setA(a+1)}>修改a</button>
    <button onClick={() => setB(b+1)}>修改b</button>
  </div>;
}

优化后(用 useMemo 缓存对象,仅 a/b 变化时更新引用):

jsx
import { useEffect, useState, useMemo } from 'react';

function Demo() {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);

  // 仅 a/b 变化时,才生成新的 config 引用
  const config = useMemo(() => ({ a, b }), [a, b]);

  // 仅 config 引用变化时(即 a/b 变化),才执行
  useEffect(() => {
    console.log('config 变化:', config);
  }, [config]);

  return <div>
    <button onClick={() => setA(a+1)}>修改a</button>
    <button onClick={() => setB(b+1)}>修改b</button>
  </div>;
}

2. 实战场景2:优化子组件的 props 依赖(避免子组件重渲染)

问题代码(父组件每次渲染传递新数组,子组件不必要重渲染):

jsx
// 子组件(接收数组 props)
function Child({ list }) {
  console.log('Child 重渲染');
  return <div>{list.join(',')}</div>;
}

// 父组件
function Parent() {
  const [count, setCount] = useState(0);

  // 每次渲染生成新数组 → 子组件重渲染
  const list = [1, 2, 3];

  return <div>
    <button onClick={() => setCount(c => c + 1)}>count: {count}</button>
    <Child list={list} />
  </div>;
}

优化后(用 useMemo 缓存数组,子组件仅数组内容变化时重渲染):

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

  // 无依赖 → 数组引用永远不变
  const list = useMemo(() => [1, 2, 3], []);

  return <div>
    <button onClick={() => setCount(c => c + 1)}>count: {count}</button>
    <Child list={list} /> {/* 仅首次渲染,后续不重渲染 */}
  </div>;
}

3.实战场景3:依赖项是函数

下面是一段搜索框组件的代码

tsx
const SearchInput = () => {
    const [query, setQuery] = useState('')
    const [result, setResult] = useState('');
    const fetchResult = (curQuery) => {
        console.log('Fetching For:', curQuery);
    }
    useEffect(() => {
        if(query){
            fetchResult(query);
        }
    }, [query, fetchResult]);
    return <input type='text' value={query} onChange={(e) => setQuery(e.target.value)} />
}

上面的代码存在什么问题呢,首先函数fetchResult定义在组件内部,这意味着每次组件渲染,都将重新生成fetchResult(引用发生变化),这就会导致useEffect每次重新渲染都会执行一遍回调。

第一种解决方案就是使用useCallback保持函数的引用。

ts
const SearchInput = () => {
    const [query, setQuery] = useState('')
    const [result, setResult] = useState('');
    const fetchResult = useCallback((curQuery) => {
        console.log('Fetching For:', curQuery);
    }, []) // 用useCallback保持函数的引用,避免重复创建
    useEffect(() => {
        if(query){
            fetchResult(query);
        }
    }, [query, fetchResult]);
    return <input type='text' value={query} onChange={(e) => setQuery(e.target.value)} />
}

第二种解决方法就是将函数写在useEffect内部,如果函数是一个纯函数(不涉及组件内部元素的引用),还可以直接写在组件外。这样就不会因为重新渲染,而导致函数重复创建。这种方法要注意过时闭包问题。

ts
const SearchInput = () => {
    const [query, setQuery] = useState('')
    const [result, setResult] = useState('');
    useEffect(() => {
        const fetchResult = (curQuery) => {
            console.log('Fetching For:', curQuery);
        }
        if(query){
            fetchResult(query);
        }
    }, [query]);
    return <input type='text' value={query} onChange={(e) => setQuery(e.target.value)} />
}

四、关键注意事项(避免滥用)

useCallback/useMemo 本身有微小的性能开销(缓存逻辑),以下场景不需要优化:

  1. 依赖项是基本类型(数字、字符串、布尔值):天然无需缓存,Object.is 能正确判断;
  2. 函数/对象仅在组件内部使用,且不作为 Hook 依赖:比如仅作为点击事件回调,无需缓存;
  3. 计算逻辑极简单(比如 const sum = a + b):useMemo 的开销可能大于重新计算的开销。

优化原则:仅当依赖项导致“明显的性能问题”(比如高频渲染的列表、复杂的 Effect 逻辑)时,才使用 useCallback/useMemo。

五、进阶:useCallback + useMemo 配合使用

当函数依赖于 useMemo 缓存的对象时,可组合使用:

jsx
function Demo() {
  const [a, setA] = useState(1);
  
  // 缓存对象
  const config = useMemo(() => ({ a }), [a]);
  
  // 缓存函数(依赖缓存后的对象)
  const handleConfig = useCallback(() => {
    console.log('config:', config);
  }, [config]); // 仅 config 变化时,函数引用更新

  useEffect(() => {
    handleConfig();
  }, [handleConfig]); // 仅 handleConfig 变化时执行

  return <button onClick={() => setA(a+1)}>修改a</button>;
}

总结

  1. useCallback:缓存函数引用,解决“函数每次渲染生成新引用导致依赖变化”的问题,依赖项写函数内部用到的核心变量;
  2. useMemo:缓存对象/数组/计算结果,解决“引用类型每次渲染生成新引用导致依赖变化”的问题,依赖项写计算逻辑用到的核心变量;
  3. 优化核心:让依赖项的“引用变化”仅发生在“内容真的需要变化”时,减少不必要的 Hook 执行/组件重渲染;
  4. 避坑点:不滥用——仅当性能问题可感知时才使用,避免增加代码复杂度。

简单来说:useCallback 管函数,useMemo 管对象/数组/计算值,二者都是为了让依赖项“变的更懒”,只在必要时更新。

评论
0/100