1. 什么是函数的副作用(Side Effects)
副作用是指在组件渲染过程中,除了返回 JSX 之外的其他操作,例如:
- 数据获取(API 调用)
- 订阅数据源
- 手动修改 DOM
- 设置定时器
- 存储数据
- 日志记录
纯函数是特定的输入只会有特定的输出,也就是说组件会输出特定的DOM给浏览器渲染,除去这份逻辑以外的操作就称之为副作用,比如获取数据,监听,订阅等等
2. useEffect 的执行时机
2.1 省略依赖项
jsx
useEffect(() => {
console.log('每次渲染都会执行');
}); // 没有依赖项数组
- 组件每次渲染都会执行
- 包括首次渲染和后续更新
2.2 指定依赖项
jsx
useEffect(() => {
console.log(`count 发生变化:${count}`);
}, [count]); // 依赖于 count
- 首次渲染时执行
- 依赖项发生变化时执行
- 多个依赖项时,任意一个变化都会触发执行
2.3 空数组依赖项
jsx
useEffect(() => {
console.log('只在组件挂载时执行一次');
}, []); // 空数组
- 仅在组件首次渲染(挂载)时执行一次
- 类似于 class 组件的 componentDidMount
3. 常见问题和最佳实践
3.1 避免依赖项循环
jsx
// ❌ 错误示例:造成无限循环
function BadExample() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 直接修改依赖项
}, [count]);
return <div>{count}</div>;
}
// ✅ 正确示例:使用函数式更新
function GoodExample() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(prevCount => prevCount + 1);
}, []); // 不需要依赖项
return <div>{count}</div>;
}
3.2 分离关注点
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// ✅ 分开声明不同功能的 useEffect
useEffect(() => {
// 获取用户信息
fetchUser(userId).then(setUser);
}, [userId]);
useEffect(() => {
// 获取用户帖子
fetchUserPosts(userId).then(setPosts);
}, [userId]);
return (
<div>
<UserInfo user={user} />
<UserPosts posts={posts} />
</div>
);
}
4. 清除副作用
4.1 清理函数的执行时机
清理函数会在以下情况执行:
- 组件卸载时
- 下一次 effect 执行前
4.2 事件监听示例
jsx
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
// 添加事件监听
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空依赖数组,只在挂载和卸载时执行
return <div>Window width: {width}</div>;
}
4.3 定时器示例
jsx
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// 清理函数:组件卸载时清除定时器
return () => clearInterval(timer);
}, []); // 空依赖数组
return <div>Count: {count}</div>;
}
4.4 数据订阅示例
jsx
function DataSubscriber({ dataSource }) {
const [data, setData] = useState(null);
useEffect(() => {
let isSubscribed = true;
const handleData = (newData) => {
if (isSubscribed) {
setData(newData);
}
};
// 订阅数据源
const subscription = dataSource.subscribe(handleData);
// 清理函数:取消订阅
return () => {
isSubscribed = false;
subscription.unsubscribe();
};
}, [dataSource]); // 依赖于 dataSource
return <div>{data ? <DataView data={data} /> : 'Loading...'}</div>;
}
4.5 WebSocket 连接示例
jsx
function WebSocketComponent({ url }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
// 清理函数:关闭 WebSocket 连接
return () => {
ws.close();
};
}, [url]);
return (
<div>
{messages.map((msg, index) => (
<div key={index}>{msg}</div>
))}
</div>
);
}
5. 实际应用场景
5.1 表单自动保存
jsx
function AutoSaveForm() {
const [content, setContent] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
// 防抖处理
const timeoutId = setTimeout(() => {
if (content) {
setSaving(true);
saveContent(content)
.then(() => setSaving(false));
}
}, 1000);
return () => clearTimeout(timeoutId);
}, [content]);
return (
<div>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
/>
{saving && <span>Saving...</span>}
</div>
);
}
5.2 实时搜索
jsx
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// 避免空查询
if (!query.trim()) {
setResults([]);
return;
}
const abortController = new AbortController();
async function fetchResults() {
try {
const response = await fetch(
`/api/search?q=${query}`,
{ signal: abortController.signal }
);
const data = await response.json();
setResults(data);
} catch (error) {
if (error.name === 'AbortError') {
// 忽略中止的请求错误
return;
}
console.error('搜索出错:', error);
}
}
const timeoutId = setTimeout(fetchResults, 300);
// 清理函数:取消请求和清除定时器
return () => {
clearTimeout(timeoutId);
abortController.abort();
};
}, [query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索..."
/>
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
6. 最佳实践总结
- 保持 effect 函数简洁,专注于单一功能
- 合理使用依赖项,避免不必要的执行
- 始终清理副作用,防止内存泄漏
- 使用条件语句控制 effect 的执行
- 考虑使用自定义 Hook 封装常用的副作用逻辑
- 在开发环境下使用 ESLint 的 exhaustive-deps 规则检查依赖项
- 使用 useCallback 和 useMemo 优化依赖项
通过合理使用 useEffect,我们可以优雅地处理组件的副作用,实现更复杂的交互逻辑,同时保持代码的可维护性和性能。