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. 安全:避免了过时闭包问题

👉点击欢迎进入 我的网站

相关推荐
Beginner x_u23 分钟前
前端八股文 Vue下
前端·vue.js·状态模式
Easonmax7 小时前
零基础入门 React Native 鸿蒙跨平台开发:7——双向滚动表格实现
react native·react.js·harmonyos
Easonmax7 小时前
零基础入门 React Native 鸿蒙跨平台开发:6——竖向滚动表格实现
react native·react.js·harmonyos
提笔了无痕7 小时前
Web中Token验证如何实现(go语言)
前端·go·json·restful
戌中横7 小时前
JavaScript——Web APIs DOM
前端·javascript·html
Beginner x_u7 小时前
如何解释JavaScript 中 this 的值?
开发语言·前端·javascript·this 指针
HWL56798 小时前
获取网页首屏加载时间
前端·javascript·vue.js
烟锁池塘柳08 小时前
【已解决】Google Chrome 浏览器报错 STATUS_ACCESS_VIOLATION 的解决方案
前端·chrome
速易达网络8 小时前
基于RuoYi-Vue 框架美妆系统
前端·javascript·vue.js
LYS_06188 小时前
RM赛事C型板九轴IMU解算(4)(卡尔曼滤波)
c语言·开发语言·前端·卡尔曼滤波