useEffect
副作用
在 React 中,副作用指的是在组件渲染过程中,除了返回 JSX 之外进行的任何操作,这些操作会影响组件外部或与外部系统进行交互。
在函数式编程和 React 上下文中:
纯函数 :相同的输入 ⇒ 相同的输出,无外部影响
副作用:函数执行过程中对外部环境产生的可观察变化
js
// 纯函数 - 无副作用
function add(a, b) {
return a + b; // 只进行计算,不影响外部
}
// 有副作用的函数
function updateTitle(title) {
document.title = title; // 影响外部 DOM
console.log(title); // 影响外部控制台
fetch('/api'); // 影响网络
}
常见的副作用包括:
- 数据获取(API 调用)
- 事件订阅(WebSocket、setTimeout、事件监听器)
- 手动修改 DOM
- 记录日志
- 存储数据到 localStorage
副作用之所以特殊,是因为它们可能在不同时间执行,可能影响其他组件,并且可能导致不一致的 UI 状态。
使用 useEffect 统一管理副作用
useEffect 是 React 专门为函数组件 设计的副作用管理系统 。其核心关系是:
useEffect = 副作用声明 + 生命周期管理 + 清理机制
作用:
- 分离关注点:将副作用逻辑与渲染逻辑分离
- 生命周期模拟:在函数组件中模拟类组件的生命周期方法
- 声明式副作用:通过依赖数组声明副作用执行的条件
useEffect 的执行机制详解
- 执行时机与浏览器渲染流程
- 组件渲染
- React 更新 DOM
- 浏览器绘制屏幕
- useEffect 执行
关键特性:
- useEffect 的回调函数是异步执行的
- 不会阻塞浏览器绘制
- 在布局和绘制之后执行(类似 componentDidMount和 componentDidUpdate)
js
// 不能直接在函数体执行副作用
function Component() {
// 错误:每次渲染都会执行
fetch('/api/data');
document.title = '新标题';
return <div>内容</div>;
}
// 必须用 useEffect 包装
function Component() {
useEffect(() => {
fetch('/api/data'); // 正确执行(DOM 更新后)
document.title = '新标题'; // 正确执行(DOM 更新后)
}, []);
return <div>内容</div>;
}
语法:
js
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数(可选)
};
}, [dependencies]); // 依赖项数组(可选)关于依赖项下面有深度解析
副作用执行时机:在组件渲染到屏幕之后执行。这确保副作用不会阻塞浏览器绘制。
三种依赖模式
模式 A:无依赖数组
js
useEffect(() => {
console.log('每次渲染后都会执行');
// 潜在性能问题:可能导致无限循环
});
- 执行时机:每次组件渲染(包括初始渲染和每次更新)后
- 清理时机:每次执行新副作用前,执行上一次的清理函数
- 使用场景:极少使用,通常只在需要严格同步副作用与渲染时使用
模式 B:空依赖数组 []
js
useEffect(() => {
console.log('仅在挂载时执行一次');
return () => {
console.log('仅在卸载时执行清理');
};
}, []);
执行时机:
- 初始渲染后执行一次
- 组件卸载时执行清理函数
等价于:类组件中的 componentDidMount+ componentWillUnmount
常见用途: - 事件监听器绑定/解绑
- 一次性数据获取
- 第三方库初始化
模式 C:有依赖的数组 [dep1, dep2]
js
useEffect(() => {
console.log('依赖变化时执行');
// 当 count 或 name 变化时执行
return () => {
console.log('执行上一次的清理');
};
}, [count, name]); // 依赖项
执行时机:
- 初始渲染后执行
- 依赖项数组中任一值发生变化时重新执行
- 重新执行前,先执行上一次的清理函数
依赖比较:使用 Object.is进行浅比较
优化技巧:
js
// 避免对象/数组作为依赖时的无限更新
const [user, setUser] = useState({ id: 1, name: 'Alice' });
// 每次渲染 user 都是新对象,会导致副作用无限执行
useEffect(() => {
console.log(user);
}, [user]); // 不推荐
// 解决方案1:提取具体值
useEffect(() => {
console.log(user.id);
}, [user.id]); // 推荐
// 解决方案2:使用 useMemo
const memoizedUser = useMemo(() => user, [user.id, user.name]);
useEffect 的清理机制
- 清理函数的作用
- 防止内存泄漏:清除定时器、取消订阅
- 避免状态不一致:取消未完成的异步操作
- 资源管理:关闭连接、释放资源
- 清理执行的实际顺序
js
// 示例:多个 useEffect 的执行顺序
function Component() {
useEffect(() => {
console.log('Effect 1 - 设置');
return () => console.log('Effect 1 - 清理');
}, []);
useEffect(() => {
console.log('Effect 2 - 设置');
return () => console.log('Effect 2 - 清理');
}, []);
// 执行顺序:
// 挂载时: Effect 1 - 设置 → Effect 2 - 设置
// 更新时: Effect 1 - 清理 → Effect 1 - 设置 → Effect 2 - 清理 → Effect 2 - 设置
// 卸载时: Effect 2 - 清理 → Effect 1 - 清理
}
useEffect 的实践模式
- 数据获取模式
js
useEffect(() => {
let isMounted = true; // 解决竞态条件
const fetchData = async () => {
try {
const result = await fetch(`/api/data/${id}`);
const data = await result.json();
if (isMounted) {
setData(data);
}
} catch (error) {
if (isMounted) {
setError(error);
}
}
};
fetchData();
return () => {
isMounted = false; // 清理时标记组件已卸载
};
}, [id]);
- 事件监听模式
js
useEffect(() => {
const handleScroll = (event) => {
console.log('滚动位置:', window.scrollY);
};
// 节流优化
const throttledHandleScroll = throttle(handleScroll, 100);
window.addEventListener('scroll', throttledHandleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
}, []);
- 定时器模式
js
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
useRef
什么是 useRef?
作用:返回一个可变的 ref 对象,其 .current属性被初始化为传入的参数。它在整个组件的生命周期内保持不变。
特点:
- 返回一个可变对象,其 .current属性被初始化为传入的参数
- 不触发组件重新渲染 当值变化时
- 引用在组件生命周期中保持不变
常用于:
- 直接访问和操作 DOM 元素
- 存储不会触发重新渲染的可变值
- 保持对某些值的持久引用
不是状态值,是储存值
基本语法
js
import { useRef } from 'react';
const refContainer = useRef(initialValue);
主要用途
1. 访问 DOM 元素(最常见用途)
js
import { useEffect, useRef } from 'react'
export const TextInputFocus = () => {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
console.log(inputRef.current); // 可以访问到input元素
}, []) // 组件挂载后自动聚焦输入框
return (
<div>
<input ref={inputRef} type="text" />
<button >聚焦输入框</button>
</div>
)
}
2. 存储可变值(不会触发重新渲染)
存储组件实例变量
js
import { useEffect, useRef } from 'react'
export const TextInputFocus = () => {
const inputRef = useRef<HTMLInputElement>(null)
const isMountedRef = useRef<boolean>(false)
// 组件挂载后自动聚焦输入框,并记录组件是否已挂载
useEffect(() => {
isMountedRef.current = true
inputRef.current?.focus()
console.log(isMountedRef.current);
}, [])
return (
<div>
<input ref={inputRef} type="text" />
<button >聚焦输入框</button>
</div>
)
}
定时器
js
import { useRef, useState } from 'react';
export const TimerComponent = () => {
const [seconds, setSeconds] = useState<number>(0);
const timerRef = useRef<number>(0); // 存储定时器ID
const startTimer = () => {
if (timerRef.current) return; // 如果已经有定时器,不重复创建
timerRef.current = setInterval(() => {
// 定时器回调,每秒执行
setSeconds(s => s + 1);
}, 1000);
console.log(timerRef.current);
};
const stopTimer = () => {
clearInterval(timerRef.current);
timerRef.current = 0; // 清除引用
};
return (
<div>
<p>已过去: {seconds} 秒</p>
<button onClick={startTimer}>开始</button>
<button onClick={stopTimer}>停止</button>
</div>
);
}
3. 存储上一次的状态或 props
React 函数组件没有内置的方法来获取上一次渲染的值,useRef可以解决这个问题。
js
import { useEffect, useRef, useState } from 'react';
export const PreviousValueTracker = () => {
const [value, setValue] = useState<string>('');
const prevValueRef = useRef<string>(''); // 存储上一个值
// revValueRef.current = value; 不可以再渲染期间渲染
useEffect(() => {
// 注意:这里在组件渲染后才执行
prevValueRef.current = value; // 更新上一个值
}, [value]); // 依赖于 value 的变化
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 更新 value,触发重新渲染
setValue(e.target.value);
// 这里 previousValueRef.current 还没更新!
};
return (
<>
<p>当前值: {value}</p>
<p>上一个值: {prevValueRef.current}</p>
<input type="text" value={value} onChange={handleChange} />
</>
)
}
工作原理详解
// 组件渲染过程:
// 第一次渲染: value = '', previousValueRef.current = ''
// 用户输入 "a":
// → 触发 onChange: setValue('a')
// → 组件重新渲染
// → useEffect 运行(在渲染后): previousValueRef.current = 'a'
// 第二次渲染: value = 'ab', previousValueRef.current = 'a'(但显示的是上一次的值)
使用 ref 存储状态/props 的关键要点
- 时机很重要:ref 的赋值应该在 useEffect中,而不是在渲染函数体中
- 异步更新:ref 的更新是同步的,但读取可能在 React 渲染周期中的不同时间点
- 不要依赖它来渲染:ref 的值变化不会触发渲染,所以不能用它来驱动 UI
- 组件卸载时清理:如果 ref 存储了资源(如定时器),需要在卸载时清理
与 useState 的区别
| 特性 | useRef | useState |
|---|---|---|
| 触发重新渲染 | 不会 | 会 |
| 值更新时机 | 立即更新 | 在下一次渲染时更新 |
| 存储位置 | 组件实例 | 组件状态 |
| 异步更新 | 同步 | 异步 |
总结:如果需要值变化时触发组件重新渲染,使用 useState;如果不需要,使用 useRef。