文章目录
-
- 前言
- 一、函数引用的陷阱
-
- [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 黄金法则
- 诚实声明依赖 :所有在
useEffect中使用的值都应在依赖数组中声明 - 稳定函数引用 :使用
useCallback包装函数,避免不必要的重新创建 - 最小化依赖:只依赖真正需要变化时重新执行的值
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]);
这种写法是:
- 符合 React 规则:诚实地声明了所有依赖
- 性能友好 :假设这些函数都用
useCallback包装且依赖合理 - 可维护:明确显示了副作用的依赖关系
- 安全:避免了过时闭包问题
👉点击欢迎进入 我的网站