react - useEffect中函数依赖管理:使用函数引用作为依赖项

文章目录

    • 前言
    • 一、函数引用的陷阱
      • [1.1 函数在每次渲染中都不同](#1.1 函数在每次渲染中都不同)
      • [1.2 解决方案:useCallback](#1.2 解决方案:useCallback)
    • 二、实战场景分析
      • [2.1 场景一:数据初始化(最常见的用例)](#2.1 场景一:数据初始化(最常见的用例))
      • [2.2 场景二:带参数的动态获取](#2.2 场景二:带参数的动态获取)
      • [2.3 场景三:条件执行](#2.3 场景三:条件执行)
    • 三、常见陷阱与解决方案
      • [3.1 陷阱:依赖过多导致频繁执行](#3.1 陷阱:依赖过多导致频繁执行)
      • [3.2 陷阱:不必要的依赖](#3.2 陷阱:不必要的依赖)
      • [3.3 高级技巧:使用 useRef](#3.3 高级技巧:使用 useRef)
    • 四、最佳实践总结
      • [4.1 黄金法则](#4.1 黄金法则)
      • [4.2 代码审查清单](#4.2 代码审查清单)
      • [4.3 性能优化建议](#4.3 性能优化建议)
    • 五、回到最初的问题

前言

React 函数组件中,我们经常会看到这样的代码:

javascript 复制代码
/** 每一个都是一个函数,用于获取不同的列表数据 */
function fetchInstitutionList() {
  console.log("Fetching institution list...");
}

function fetchProcessStateList() {
  console.log("Fetching process state list...");
}

function fetchDeliveryTargetList() {
  console.log("Fetching delivery target list...");
}

function fetchDeliveryTypeList() {
  console.log("Fetching delivery type list...");
}

useEffect(() => {
  fetchInstitutionList();
  fetchProcessStateList();
  fetchDeliveryTargetList();
  fetchDeliveryTypeList();
}, [fetchInstitutionList, fetchProcessStateList, fetchDeliveryTargetList, fetchDeliveryTypeList]);

初看之下,这种写法似乎有些奇怪:为什么要把函数本身作为依赖项 ?这背后隐藏着 React Hooks 的重要设计哲学和最佳实践。

一、函数引用的陷阱

1.1 函数在每次渲染中都不同

JavaScript 中,函数声明在每次渲染时都会创建一个新的函数引用

javascript 复制代码
function MyComponent() {
  // 每次渲染都创建新的函数
  const fetchData = () => {
    console.log("Fetching data...");
  };

  // ❌ 这会导致无限循环!
  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return <div>Component</div>;
}

每次组件渲染,fetchData 都是一个新的函数,导致 useEffect 依赖变化,重新执行,触发重新渲染...形成无限循环。

1.2 解决方案:useCallback

为了解决这个问题,React 提供了 useCallback Hook:

简单说:useCallback 能记住一个函数,只有当依赖变化时才创建新的函数,否则返回缓存的旧函数。

核心作用:优化性能,特别当函数作为 prop 传递给子组件时。

javascript 复制代码
const fetchData = useCallback(() => {
  console.log("Fetching data...");
  // 这里可以使用 count 等状态
}, []); // ✅ 依赖数组决定何时创建新函数

useEffect(() => {
  fetchData();
}, [fetchData]); // ✅ 现在依赖是稳定的

二、实战场景分析

2.1 场景一:数据初始化(最常见的用例)

javascript 复制代码
// 使用 useCallback 稳定函数引用
const fetchInstitutionList = useCallback(async () => {
  const result = await api.getInstitutionList();
  setInstitutionList(result);
}, []); // 空依赖:函数只创建一次

const fetchProcessStateList = useCallback(async () => {
  const result = await api.getProcessStateList();
  setProcessStateList(result);
}, []);

// 正确的写法
useEffect(() => {
  fetchInstitutionList();
  fetchProcessStateList();
  fetchDeliveryTargetList();
  fetchDeliveryTypeList();
}, [fetchInstitutionList, fetchProcessStateList, fetchDeliveryTargetList, fetchDeliveryTypeList]); // ✅ 依赖稳定,只会执行一次

2.2 场景二:带参数的动态获取

javascript 复制代码
const [filters, setFilters] = useState({});

// 依赖 filters,当 filters 变化时创建新函数
const fetchFilteredData = useCallback(async () => {
  const result = await api.getData(filters);
  setData(result);
}, [filters]); // ✅ 明确依赖

useEffect(() => {
  fetchFilteredData();
}, [fetchFilteredData]); // ✅ filters 变化时重新获取

2.3 场景三:条件执行

javascript 复制代码
const [shouldFetch, setShouldFetch] = useState(false);

const fetchData = useCallback(async () => {
  if (!shouldFetch) return;
  const result = await api.getData();
  setData(result);
}, [shouldFetch]); // ✅ 依赖 shouldFetch

useEffect(() => {
  fetchData();
}, [fetchData]); // ✅ shouldFetch 变化时重新评估

三、常见陷阱与解决方案

3.1 陷阱:依赖过多导致频繁执行

javascript 复制代码
// ❌ 问题:任何依赖变化都导致重新执行
const fetchData = useCallback(() => {
  // ... 复杂的逻辑
}, [dep1, dep2, dep3, dep4, dep5]);

// ✅ 解决方案:分离关注点
const fetchCoreData = useCallback(() => {
  // 只依赖必要的状态
}, [essentialDep]);

const fetchAdditionalData = useCallback(() => {
  // 依赖其他状态
}, [otherDep]);

3.2 陷阱:不必要的依赖

javascript 复制代码
// ❌ 不必要的依赖
const fetchData = useCallback(() => {
  // 实际不使用 count
  console.log("fetching");
}, [count]); // 不需要 count

// ✅ 正确的依赖
const fetchData = useCallback(() => {
  console.log("fetching");
}, []); // 空依赖

3.3 高级技巧:使用 useRef

javascript 复制代码
// 当确实无法避免函数变化,但又不想触发 useEffect
const fetchDataRef = useRef();

useEffect(() => {
  fetchDataRef.current = async () => {
    // 可以使用最新的状态
    console.log("Fetching with latest state");
  };
});

useEffect(() => {
  const fetchLatest = async () => {
    await fetchDataRef.current?.();
  };
  fetchLatest();
}, []); // 空依赖,只执行一次

四、最佳实践总结

4.1 黄金法则

  1. 诚实声明依赖 :所有在 useEffect 中使用的值都应在依赖数组中声明
  2. 稳定函数引用 :使用 useCallback 包装函数,避免不必要的重新创建
  3. 最小化依赖:只依赖真正需要变化时重新执行的值

4.2 代码审查清单

  • 是否所有在 useEffect 中使用的变量都声明了依赖?
  • 函数是否用 useCallback 包装?
  • useCallback 的依赖数组是否合理?
  • 是否存在循环依赖的风险?
  • 是否可以考虑拆分过大的 useEffect

4.3 性能优化建议

javascript 复制代码
// 优化前:一个大的 useEffect
useEffect(() => {
  fetchA();
  fetchB();
  fetchC();
}, [fetchA, fetchB, fetchC]);

// 优化后:分离关注点
useEffect(() => {
  fetchA();
}, [fetchA]);

useEffect(() => {
  fetchB();
}, [fetchB]);

useEffect(() => {
  fetchC();
}, [fetchC]);

五、回到最初的问题

现在我们可以理解开篇代码的智慧:

javascript 复制代码
useEffect(() => {
  fetchInstitutionList();
  fetchProcessStateList();
  fetchDeliveryTargetList();
  fetchDeliveryTypeList();
}, [fetchInstitutionList, fetchProcessStateList, fetchDeliveryTargetList, fetchDeliveryTypeList]);

这种写法是:

  1. 符合 React 规则:诚实地声明了所有依赖
  2. 性能友好 :假设这些函数都用 useCallback 包装且依赖合理
  3. 可维护:明确显示了副作用的依赖关系
  4. 安全:避免了过时闭包问题

👉点击欢迎进入 我的网站

相关推荐
代码匠心17 小时前
AI 自动编程:一句话设计高颜值博客
前端·ai·ai编程·claude
_AaronWong18 小时前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
cxxcode18 小时前
I/O 多路复用:从浏览器到 Linux 内核
前端
用户54330814419418 小时前
AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例
前端
JarvanMo18 小时前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
恋猫de小郭19 小时前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木19 小时前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮19 小时前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati19 小时前
Vue3 父子组件通信完全指南
前端·面试
是一碗螺丝粉19 小时前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain