1. 前言
在React函数式组件的开发中,useEffect是一个核心钩子,它为我们提供了在组件渲染后执行副作用操作的能力。副作用操作包括数据获取、订阅、手动DOM操作等。整体类似于Vue中的生命周期+计算属性。下面,将深入探讨useEffect的工作原理、常见应用场景、性能优化以及潜在的陷阱。
2. 基本概念和语法
useEffect是React提供的一个钩子函数,用于在函数式组件中执行副作用操作。其基本语法如下:
javascript
useEffect(callback, dependencies);
- callback:副作用函数,在组件渲染后执行。可以返回一个清理函数,用于在组件卸载前执行清理操作。
- dependencies (可选):依赖项数组,用于控制副作用函数的执行时机。如果省略该参数,副作用函数将在每次渲染后执行;如果传入空数组
[],副作用函数仅在首次渲染后执行;如果传入具体的依赖项,副作用函数将在依赖项变化时执行。
useEffect的执行时机是在浏览器完成DOM渲染之后,但在屏幕更新之前。这意味着副作用操作不会阻塞浏览器的渲染过程,从而保证了应用的流畅性。
3. 核心应用场景
下面是一些核心应用场景,每个都会有一个例子:
3.1. 数据获取
在组件加载时获取数据是一个常见的需求,使用useEffect可以轻松实现这一点:
javascript
import React, { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, []); // 空依赖数组确保副作用只在首次渲染后执行
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data && data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
3.2. DOM操作与动画
useEffect可以用于执行DOM操作,比如设置焦点、调整元素尺寸等:
javascript
import React, { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 在组件挂载后自动聚焦输入框
}, []); // 空依赖数组确保副作用只在首次渲染后执行
return <input ref={inputRef} type="text" />;
}
3.3. 订阅与取消订阅
当需要监听事件或订阅外部数据源时,可以在useEffect中设置订阅,并在清理函数中取消订阅:
javascript
import React, { useState, useEffect } from 'react';
function WindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
// 清理函数在组件卸载前执行
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空依赖数组确保只添加一次事件监听器
return (
<div>
Window size: {size.width} x {size.height}
</div>
);
}
3.4. 计时器与间隔执行
使用useEffect可以设置计时器或间隔执行的任务:
javascript
import React, { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 清理函数在组件卸载前执行
return () => {
clearInterval(interval);
};
}, []); // 空依赖数组确保只设置一次计时器
return <div>Count: {count}</div>;
}
4. 依赖项数组的使用与优化
useEffect的依赖项数组是控制副作用执行时机的关键。合理使用依赖项数组可以避免不必要的副作用执行,从而优化性能。
4.1. 空依赖数组
当依赖项数组为空时,副作用函数仅在首次渲染后执行,类似于componentDidMount:
javascript
useEffect(() => {
// 只在组件挂载时执行
console.log('Component mounted');
return () => {
// 只在组件卸载时执行
console.log('Component will unmount');
};
}, []);
4.2. 包含依赖项的数组
当依赖项数组包含值时,副作用函数将在依赖项变化时执行,类似于componentDidUpdate:
javascript
useEffect(() => {
// 每次count变化时执行
console.log(`Count changed to: ${count}`);
}, [count]); // 依赖于count
4.3. 省略依赖项数组
如果省略依赖项数组,副作用函数将在每次渲染后执行,包括首次渲染和后续更新:
javascript
useEffect(() => {
// 每次渲染后都执行
console.log('Rendered');
});
4.4. 使用复杂依赖项
当依赖项是对象、数组或函数时,需要特别注意引用相等性问题。可以使用useCallback或useMemo来确保依赖项的稳定性:
javascript
import React, { useState, useEffect, useCallback } from 'react';
function ComplexDependency() {
const [data, setData] = useState([]);
// 使用useCallback缓存函数,避免每次渲染时创建新函数
const fetchData = useCallback(async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
}, []); // 空依赖数组确保fetchData引用不变
useEffect(() => {
fetchData();
}, [fetchData]); // 依赖于fetchData的引用
return <div>Data loaded: {data.length}</div>;
}
5. 处理异步操作的陷阱与解决方案
在useEffect中处理异步操作时,需要注意避免常见的陷阱,比如竞态条件和内存泄漏。
5.1. 竞态条件问题
当在useEffect中执行异步操作时,如果在请求完成前组件已经卸载,可能会导致内存泄漏和错误。可以使用一个标记来跟踪组件的挂载状态:
javascript
import React, { useState, useEffect } from 'react';
function SafeDataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // 跟踪组件挂载状态
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
// 只有在组件仍然挂载时才更新状态
if (isMounted) {
setData(result);
setLoading(false);
}
} catch (err) {
if (isMounted) {
console.error(err);
setLoading(false);
}
}
};
fetchData();
// 清理函数在组件卸载前执行
return () => {
isMounted = false;
};
}, []);
return loading ? <div>Loading...</div> : <div>{data}</div>;
}
5.2. 使用AbortController取消请求
对于基于Fetch API的请求,可以使用AbortController来取消未完成的请求,Axios请求也同理。下面是一个Fetch的取消示例:
javascript
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data', { signal });
const result = await response.json();
setData(result);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request aborted');
} else {
console.error(err);
}
}
};
fetchData();
// 组件卸载前取消请求
return () => controller.abort();
}, []);
6. useEffect与类组件生命周期的对比
在类组件中,副作用操作通常分散在多个生命周期方法中,如componentDidMount、componentDidUpdate和componentWillUnmount。而useEffect将这些操作统一到一个API中:
| 类组件生命周期 | useEffect 等价写法 |
|---|---|
| componentDidMount | useEffect(() => { /* 初始化 */ }, []) |
| componentDidUpdate | useEffect(() => { /* 更新 */ }) |
| componentWillUnmount | useEffect(() => { return () => { /* 清理 */ } }, []) |
7. 最佳实践与注意事项
-
保持副作用函数的纯净性:副作用函数应该只执行必要的操作,避免在其中执行会影响渲染结果的计算。
-
避免无限循环:确保依赖项数组正确包含所有需要监听的变量,避免因依赖项缺失导致的无限循环。
-
使用多个useEffect :将不相关的副作用分离到不同的
useEffect中,提高代码的可读性和可维护性。 -
谨慎使用空依赖数组:只有在确实只需要在组件挂载和卸载时执行副作用时才使用空依赖数组。
-
使用useRef存储可变值 :如果需要在副作用中访问之前的值,可以使用
useRef来存储这些值。
8. Vue中对应的useEffect实现
在 Vue 中,与 React 的useEffect最接近的功能是通过生命周期钩子和计算属性组合实现的。Vue 提供了更细分的生命周期钩子来处理不同阶段的副作用,而不是单一的 API。以下是具体的对应关系和实现方式:
在Vue中,与React的useEffect最接近的功能是通过生命周期钩子 和计算属性组合实现的。Vue提供了更细分的生命周期钩子来处理不同阶段的副作用,而不是单一的API。以下是具体的对应关系和实现方式:
8.1. Vue2的类似实现
React useEffect场景 |
Vue 生命周期钩子/方法 |
|---|---|
组件挂载后执行(类似useEffect(() => {}, [])) |
onMounted(组合式API)或mounted(选项式API) |
组件更新后执行(类似useEffect(() => {})) |
onUpdated(组合式API)或updated(选项式API) |
组件卸载前清理(类似useEffect(() => () => {...}, [])) |
onBeforeUnmount/onUnmounted(组合式API)或beforeDestroy/destroyed(选项式API) |
8.2. Vue3的类似实现
Vue 3提供了watchEffect API,它会自动追踪依赖 并在初始化和依赖变化时执行,更接近React的useEffect:
javascript
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
// 自动追踪count的变化
watchEffect(() => {
console.log(`Count is: ${count.value}`);
// 副作用逻辑(会在初始化和count变化时执行)
// 返回清理函数(在组件卸载前或下次执行前调用)
return () => {
console.log('Cleaning up...');
};
});
return { count };
}
};
区别:
watchEffect自动追踪依赖 (通过访问响应式数据),而useEffect需要手动指定依赖数组。watchEffect立即执行一次 (类似useEffect(() => {}, [])),而useEffect默认在首次渲染后执行。
React useEffect |
Vue 3 等效实现方式 |
|---|---|
| 首次渲染后执行 | onMounted 或 watchEffect |
| 依赖变化时执行 | watch 或 watchEffect |
| 组件卸载前清理 | onUnmounted 或 watchEffect 的返回值 |
| 每次渲染后执行 | onUpdated |
| 自动追踪依赖 | watchEffect |
Vue通过更细分的生命周期钩子和响应式API提供了比useEffect更精细的控制,但核心思想都是处理副作用 和依赖变化 。根据具体场景,你可以选择最合适的Vue API来替代useEffect的功能。
9. 总结
useEffect是React函数式组件中一个强大且灵活的钩子,它使我们能够在组件渲染后执行各种副作用操作。通过合理使用依赖项数组和清理函数,我们可以优化性能、避免内存泄漏,并确保代码的健壮性。
本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~
PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~
往期文章
- React无限滚动插件react-infinite-scroll-component的配置+优化+避坑指南
- 前端音频兼容解决:音频神器howler.js从基础到进阶完整使用指南
- 使用React-OAuth进行Google/GitHub登录的教程和案例
- 纯前端人脸识别利器:face-api.js手把手深入解析教学
- 关于React父组件调用子组件方法forwardRef的详解和案例
- React跨组件数据共享useContext详解和案例
- Web图像编辑神器tui.image-editor从基础到进阶的实战指南
- 开发个人微信小程序类目选择/盈利方式/成本控制与服务器接入指南
- 前端图片裁剪Cropper.js核心功能与实战技巧详解
- 编辑器也有邪修?盘点VS Code邪门/有趣的扩展
- js使用IntersectionObserver实现目标元素可见度的交互
- Web前端页面开发阿拉伯语种适配指南
- 让网页拥有App体验?PWA 将网页变为桌面应用的保姆级教程PWA
- 使用nvm管理node.js版本以及更换npm淘宝镜像源
- 手把手教你搭建规范的团队vue项目,包含commitlint,eslint,prettier,husky,commitizen等等