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

👉点击欢迎进入 我的网站

相关推荐
QQ1__8115175152 小时前
Spring boot名城小区物业管理系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
前端·vue.js·spring boot
钛态2 小时前
前端微前端架构:大项目的救命稻草还是自找麻烦?
前端·vue·react·web
一粒黑子2 小时前
【实战解析】阿里开源 PageAgent:纯前端 GUI Agent,一行JS让网页支持自然语言操控
前端·javascript·开源
独角鲸网络安全实验室2 小时前
2026微信小程序抓包全解析:从实操落地到合规风控,解锁前端调试新范式
前端·微信小程序·小程序·抓包·系统代理绕过·https证书严格校验·进程隔离
紫微AI2 小时前
前端文本测量成了卡死一切创新的最后瓶颈,pretext实现突破了
前端·人工智能·typescript
GISer_Jing2 小时前
AI前端(From豆包)
前端·aigc·ai编程
IT枫斗者2 小时前
前端部署后如何判断“页面是不是最新”?一套可落地的版本检测方案(适配 Vite/Vue/React/任意 SPA)
前端·javascript·vue.js·react.js·架构·bug
测试修炼手册2 小时前
[测试技术] 深入理解 JSON Web Token (JWT)
前端·json
AI老李2 小时前
2026 年 Web 前端开发的 8 个趋势!
前端
里欧跑得慢2 小时前
15. Web可访问性最佳实践:让每个用户都能平等访问
前端·css·flutter·web