1. React副作用机制的设计哲学
在React的架构体系中,副作用(Effect)处理一直是核心难点之一。要真正理解useEffect的工作原理,我们需要先回到React的设计初衷。React本质上是一个状态到视图的映射系统,其理想状态应该是纯函数式的渲染流程:给定相同的props和state,组件总是返回相同的JSX结构。
但现实世界的应用不可能完全纯净。数据获取、订阅管理、DOM手动操作等"副作用"操作不可避免。React团队对此的解决方案是:将这些副作用操作与主渲染流程分离,通过专门的Effect机制进行管理。这种设计带来了几个关键优势:
- 可预测性:副作用被明确隔离,不会干扰主渲染流程
- 可测试性:副作用可以独立mock和验证
- 性能优化:React可以智能调度副作用执行时机
javascript复制function Example() {
const [count, setCount] = useState(0);
// 典型的副作用使用场景
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Effect的完整生命周期解析
2.1 挂载阶段的初始化流程
当组件首次渲染时,React会创建一个特殊的"effect链表"来管理所有副作用。这个过程发生在commit阶段,具体步骤如下:
- render阶段:React执行函数组件,识别出所有的useEffect调用
- effect对象创建:为每个useEffect创建一个effect对象,包含:
- 创建函数(effect create)
- 销毁函数(effect destroy)
- 依赖数组(deps)
- 标签(tag,标识effect类型)
- 链表构建:将这些effect对象按声明顺序连接成链表
- commit阶段:React完成DOM更新后,按顺序执行effect的创建函数
注意:在严格模式下(React 18+),开发环境会故意双调用effect以暴露问题。这是为什么有时你会看到effect被意外执行两次的原因。
2.2 更新阶段的差异化处理
当组件更新时,React会对比新旧依赖数组,决定如何处理effect:
javascript复制// React源码中的大致逻辑
function updateEffect(create, deps) {
const prevDeps = currentEffect.deps;
if (areHookInputsEqual(deps, prevDeps)) {
// 依赖未变化,跳过effect执行
return;
}
// 标记需要重新执行
currentEffect.tag |= HookHasEffect;
// 更新依赖引用
currentEffect.deps = deps;
}
这个对比过程使用Object.is进行浅比较,这意味着:
- 对象和数组的引用变化会被识别为依赖变化
- 函数组件内声明的对象/函数每次都会是新引用
- 基本类型的值比较是安全的
2.3 卸载阶段的清理机制
组件卸载时,React会执行所有effect的清理函数。这个清理过程是同步且不可中断的:
- 遍历effect链表
- 对每个标记了HasEffect的节点:
- 如果存在destroy函数,执行它
- 清除effect引用,帮助GC回收内存
javascript复制// 典型的清理示例
useEffect(() => {
const subscription = dataSource.subscribe();
return () => {
subscription.unsubscribe(); // 清理函数
};
}, []);
3. 依赖数组的深度解析
依赖数组是React优化effect执行的关键机制,但也是最容易误用的部分。
3.1 空依赖数组的特殊语义
空数组[]表示"这个effect不依赖任何值,只在挂载时运行一次"。但实际语义更微妙:
- 它确实只在挂载时执行create
- 但会在每次依赖变化时执行destroy
- 最终在卸载时执行destroy
这种设计确保了资源管理的正确性,但也可能导致困惑。
3.2 依赖项遗漏的静态检查
React团队提供了eslint-plugin-react-hooks来自动检测依赖项遗漏:
javascript复制useEffect(() => {
console.log(props.someValue);
// ESLint会警告: someValue应该加入依赖数组
}, []); // 🔴 警告: 缺少someValue依赖
但有些情况需要手动处理:
javascript复制useEffect(() => {
const id = setInterval(() => {
console.log(count); // 闭包捕获了初始count值
}, 1000);
return () => clearInterval(id);
}, []); // 故意不依赖count,但会导致问题
3.3 函数依赖的处理技巧
当effect依赖函数时,推荐将函数移到effect内部或使用useCallback:
javascript复制// 方案1:将函数移入effect
useEffect(() => {
function fetchData() {
// 使用props和state
}
fetchData();
}, [props.someId]); // 只需要依赖变化的id
// 方案2:使用useCallback
const fetchData = useCallback(() => {
// 函数体
}, [deps]);
useEffect(() => {
fetchData();
}, [fetchData]);
4. 高级使用模式与性能优化
4.1 竞态条件处理
在数据获取场景中,网络响应顺序不确定可能导致状态错乱:
javascript复制useEffect(() => {
let didCancel = false;
async function fetchData() {
const result = await axios.get(`/api/${id}`);
if (!didCancel) {
setData(result.data);
}
}
fetchData();
return () => {
didCancel = true; // 标记请求已取消
};
}, [id]);
4.2 批量更新与调度优化
React默认会批量处理状态更新,但有时需要更精细的控制:
javascript复制useEffect(() => {
// 紧急更新:需要立即反映到UI
flushSync(() => {
setSomethingImportant(123);
});
// 非紧急更新:可以延迟
startTransition(() => {
setOtherState(456);
});
}, [deps]);
4.3 自定义Hook封装模式
将复杂effect逻辑封装成自定义Hook:
javascript复制function useWindowSize() {
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 size;
}
5. React 18并发模式下的变化
React 18引入的并发渲染对effect行为有重要影响:
5.1 双重调用检测
开发模式下,React会故意卸载并重新挂载组件,以帮助发现未正确清理的effect:
javascript复制// React 18严格模式下的行为
* Mount component
- React calls useEffect
* Simulate unmounting component
- React cleans up effects
* Simulate mounting component
- React runs effects again
5.2 过渡更新的区别
使用startTransition标记的更新不会触发同步effect:
javascript复制useEffect(() => {
// 常规更新会触发这里
}, [value]);
useEffect(() => {
// 过渡更新不会立即触发这里
}, [transitionValue]);
5.3 新的Effect Hook
React 18引入了useInsertionEffect和useLayoutEffect的变体:
javascript复制useInsertionEffect(() => {
// 在DOM变更前同步执行
// 适合CSS-in-JS库注入样式
}, [deps]);
6. 常见问题与调试技巧
6.1 无限循环的成因与修复
最常见的effect问题是意外导致的无限渲染循环:
javascript复制// 错误示例
useEffect(() => {
setCount(count + 1); // 🔴 每次effect执行都改变依赖,导致循环
}, [count]);
// 解决方案1:使用函数式更新
useEffect(() => {
setCount(prev => prev + 1); // ✅ 不依赖count
}, []);
// 解决方案2:重新设计状态依赖
const [state, setState] = useState({
count: 0,
other: 'value'
});
useEffect(() => {
setState(prev => ({...prev, count: prev.count + 1}));
}, [state.other]); // 只在other变化时更新count
6.2 Effect时序问题调试
当多个effect相互依赖时,时序问题可能很难调试:
javascript复制useEffect(() => {
// Effect A
}, [a]);
useEffect(() => {
// Effect B依赖Effect A的结果
}, [b]); // 如何确保在A之后执行?
解决方案是合并effect或使用useReducer管理复杂状态:
javascript复制useEffect(() => {
// 合并相关逻辑
const resultA = computeA(a);
const resultB = computeB(resultA);
setB(resultB);
}, [a]);
6.3 内存泄漏检测
未正确清理的effect可能导致内存泄漏。使用DevTools的"Record heap allocations"功能可以帮助发现:
- 执行可能泄漏的操作
- 触发几次组件卸载/重挂载
- 检查内存是否持续增长
典型的内存泄漏模式:
javascript复制useEffect(() => {
const timer = setInterval(() => {}, 1000);
// 🔴 忘记清理定时器
return () => clearInterval(timer); // ✅ 必须清理
}, []);
7. 底层实现原理深度剖析
7.1 Fiber架构中的Effect表示
在React Fiber架构中,每个effect都被表示为fiber节点上的一个字段:
typescript复制type Fiber = {
// ...其他字段
flags: Flags, // 标记需要执行的effect类型
updateQueue: UpdateQueue | null, // effect链表
memoizedProps: any,
memoizedState: any,
};
effect链表的结构示例:
javascript复制{
create: () => { /* 副作用函数 */ },
destroy: () => { /* 清理函数 */ },
deps: [dep1, dep2],
next: { /* 下一个effect */ }
}
7.2 调度算法中的优先级处理
React根据effect类型分配不同优先级:
- 布局effect(useLayoutEffect):同步执行,优先级最高
- 普通effect:异步执行,可中断
- 插入effect(useInsertionEffect):DOM变更前同步执行
调度的大致流程:
- 完成DOM更新
- 调度布局effect(同步)
- 调度普通effect(异步,可中断)
- 如有更高优先级更新到来,可能中断并重新开始
7.3 垃圾回收与内存管理
React的effect清理机制与JavaScript引擎的GC协同工作:
- 组件卸载时,React执行所有destroy函数
- destroy函数应该释放所有外部资源引用
- 组件fiber树被完全解除引用
- GC可以回收相关内存
常见的GC问题模式:
javascript复制useEffect(() => {
const data = heavyObject;
window.addEventListener('click', () => {
console.log(data); // 🔴 保持对data的引用
});
return () => window.removeEventListener('click');
}, []);
即使清理了事件监听器,闭包仍保持对data的引用,阻止其被GC回收。解决方案:
javascript复制useEffect(() => {
const data = heavyObject;
const handler = () => {
console.log(data);
};
window.addEventListener('click', handler);
return () => {
window.removeEventListener('click', handler);
data.cleanup(); // 显式释放资源
};
}, []);
8. 实战中的最佳实践
8.1 数据获取模式演进
从简单到复杂的数据获取方案:
javascript复制// 方案1:基础fetch
useEffect(() => {
let ignore = false;
fetch(url)
.then(res => res.json())
.then(data => {
if (!ignore) setData(data);
});
return () => { ignore = true; };
}, [url]);
// 方案2:错误处理
useEffect(() => {
let ignore = false;
setLoading(true);
fetch(url)
.then(handleErrors)
.then(data => {
if (!ignore) {
setData(data);
setError(null);
}
})
.catch(err => {
if (!ignore) setError(err.message);
})
.finally(() => {
if (!ignore) setLoading(false);
});
return () => { ignore = true; };
}, [url]);
// 方案3:使用AbortController
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(/* ... */)
.catch(err => {
if (err.name !== 'AbortError') {
/* 处理真实错误 */
}
});
return () => controller.abort();
}, [url]);
8.2 复杂状态依赖管理
当多个状态相互依赖时,考虑使用useReducer:
javascript复制function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setText':
return { ...state, text: action.text };
default:
throw new Error();
}
}
function Component() {
const [state, dispatch] = useReducer(reducer, { count: 0, text: '' });
useEffect(() => {
// 所有相关状态都在一个对象中
console.log('State updated:', state);
}, [state]); // 只需依赖整个state对象
}
8.3 与第三方库的集成
集成如D3、Three.js等命令式库的模式:
javascript复制function Chart({ data }) {
const ref = useRef(null);
useEffect(() => {
const svg = d3.select(ref.current);
// 初始化图表
return () => {
// 清理D3创建的DOM元素
svg.selectAll("*").remove();
};
}, []);
useEffect(() => {
if (ref.current) {
// 更新图表数据
updateChart(ref.current, data);
}
}, [data]);
return <div ref={ref} />;
}
关键点:
- 初始化effect只运行一次(空依赖数组)
- 数据更新effect响应数据变化
- 清理时移除所有第三方库创建的DOM
9. 未来演进方向
React团队正在探索的effect相关改进:
9.1 编译器优化的可能性
未来React编译器可能自动优化effect依赖:
javascript复制// 开发者编写的代码
useEffect(() => {
console.log(props.id);
}, [props.id]); // 需要手动指定依赖
// 编译器可能自动转换为
useEffect(() => {
console.log(props.id);
}, [props.id]); // 自动生成最优依赖数组
9.2 更细粒度的响应式机制
可能引入类似Signal的细粒度响应式方案:
javascript复制const count = createSignal(0);
// 自动追踪依赖
createEffect(() => {
console.log('Count:', count());
});
9.3 服务端组件的影响
随着React服务端组件的发展,effect在服务端的执行限制:
- 服务端组件不能使用effect
- 客户端组件通过"use client"标记
- 数据获取模式可能转向async/await服务端组件
10. 从源码看关键实现
10.1 useEffect的挂载实现
在ReactFiberHooks.js中,useEffect的挂载逻辑:
javascript复制function mountEffect(create, deps) {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps
);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps
);
}
10.2 effect的调度入口
在ReactFiberWorkLoop.js中,effect的调度入口:
javascript复制function flushPassiveEffects() {
if (rootWithPendingPassiveEffects !== null) {
// 执行所有被调度的effect
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root.current);
}
}
10.3 依赖比较算法
React使用的浅比较算法:
javascript复制function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
11. 性能优化策略
11.1 按需执行重型effect
对于计算密集型effect,考虑使用防抖或节流:
javascript复制useEffect(() => {
const timer = setTimeout(() => {
// 重型计算
computeExpensiveValue();
}, 500);
return () => clearTimeout(timer);
}, [input]);
11.2 避免不必要的effect执行
使用useMemo缓存计算结果,减少effect触发:
javascript复制const processedData = useMemo(() => {
return process(props.rawData);
}, [props.rawData]);
useEffect(() => {
// 只有当processedData真正变化时执行
visualize(processedData);
}, [processedData]);
11.3 批量状态更新
合并相关状态更新,减少effect执行次数:
javascript复制useEffect(() => {
// 不好的做法:触发多次渲染
setA(a);
setB(b);
// 好的做法:合并更新
setState(prev => ({ ...prev, a, b }));
}, [a, b]);
12. 测试策略
12.1 单元测试effect行为
使用React Testing Library测试effect:
javascript复制test('should update document title', () => {
render(<Component count={5} />);
expect(document.title).toBe('Count: 5');
rerender(<Component count={6} />);
expect(document.title).toBe('Count: 6');
});
12.2 模拟时钟测试异步effect
使用jest.useFakeTimers测试定时器effect:
javascript复制test('should cleanup interval', () => {
jest.useFakeTimers();
const { unmount } = render(<TimerComponent />);
act(() => {
jest.advanceTimersByTime(1000);
});
unmount();
// 验证定时器被清理
expect(jest.getTimerCount()).toBe(0);
});
12.3 集成测试第三方库交互
测试与D3等库的集成:
javascript复制test('should render D3 chart', () => {
const { container } = render(<Chart data={testData} />);
// 验证D3创建了预期的DOM结构
expect(container.querySelector('.d3-node')).not.toBeNull();
// 测试数据更新
rerender(<Chart data={updatedData} />);
expect(container.querySelector('.updated-element')).not.toBeNull();
});
13. 设计模式与架构思考
13.1 状态与effect的分离原则
良好的React架构应该:
- 将状态管理集中在少数几个组件
- 将effect逻辑封装在自定义Hook中
- 保持大多数组件为纯展示组件
javascript复制// 不好的做法:混合状态和effect
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
// 渲染逻辑...
}
// 好的做法:分离关注点
function useUser(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return user;
}
function UserProfile({ userId }) {
const user = useUser(userId);
// 纯渲染逻辑
}
13.2 Effect与事件处理的界限
区分应该用effect还是事件处理:
javascript复制// 适合用effect的场景:
// - 数据获取
// - 订阅
// - 响应props/state变化
// 适合用事件处理程序的场景:
// - 用户交互直接触发
// - 不需要响应状态变化
function SearchBox() {
const [query, setQuery] = useState('');
// 🔴 不好的做法:用effect处理用户输入
useEffect(() => {
if (query) {
search(query);
}
}, [query]);
// ✅ 好的做法:直接用事件处理
const handleSearch = () => {
search(query);
};
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
);
}
13.3 状态机模式管理复杂流程
对于复杂的状态转换,考虑使用状态机:
javascript复制function useFetch(url) {
const [state, setState] = useState({
status: 'idle',
data: null,
error: null
});
useEffect(() => {
if (!url) return;
setState({ status: 'pending' });
fetch(url)
.then(res => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then(data => {
setState({ status: 'resolved', data });
})
.catch(error => {
setState({ status: 'rejected', error });
});
}, [url]);
return state;
}
14. 与其他特性的协同
14.1 与Context的结合
在effect中安全使用context:
javascript复制const UserContext = createContext();
function UserProfile() {
const user = useContext(UserContext);
useEffect(() => {
// 当user变化时重新获取数据
if (user) {
fetchProfile(user.id);
}
}, [user]); // 依赖context值
}
14.2 与Ref的配合使用
用ref保存可变值而不触发effect:
javascript复制function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
// 可以在事件处理中访问intervalRef
const stop = () => clearInterval(intervalRef.current);
}
14.3 与useMemo/useCallback的优化组合
避免不必要的effect触发:
javascript复制function Parent() {
const [count, setCount] = useState(0);
// 用useCallback记忆化函数
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
return <Child onIncrement={increment} />;
}
function Child({ onIncrement }) {
// 现在这个effect只会在onIncrement真正变化时执行
useEffect(() => {
console.log('Increment callback changed');
}, [onIncrement]);
}
15. 从类组件到函数组件的思维转变
15.1 生命周期方法的对应关系
类组件生命周期到Hook的映射:
componentDidMount→useEffect(fn, [])componentDidUpdate→useEffect(fn, [deps])componentWillUnmount→useEffect(() => { return fn }, [])shouldComponentUpdate→React.memo或useMemo
15.2 实例变量的替代方案
类组件中的实例变量可以用ref替代:
javascript复制class MyComponent extends React.Component {
intervalId = null;
componentDidMount() {
this.intervalId = setInterval(() => {}, 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
}
// 函数组件等价实现
function MyComponent() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
}
15.3 派生状态的正确处理
类组件中的getDerivedStateFromProps模式:
javascript复制class MyComponent extends React.Component {
state = {
derivedValue: computeDerivedValue(this.props)
};
static getDerivedStateFromProps(props, state) {
if (props.input !== state.prevInput) {
return {
derivedValue: computeDerivedValue(props),
prevInput: props.input
};
}
return null;
}
}
// 函数组件等价实现
function MyComponent({ input }) {
const [derivedValue, setDerivedValue] = useState(() =>
computeDerivedValue(input)
);
const [prevInput, setPrevInput] = useState(input);
if (input !== prevInput) {
setDerivedValue(computeDerivedValue(input));
setPrevInput(input);
}
}
16. 生态系统工具与库
16.1 常用effect相关工具库
-
swr/stale-while-revalidate:智能数据获取
javascript复制import useSWR from 'swr'; function Profile() { const { data, error } = useSWR('/api/user', fetcher); if (error) return <div>failed to load</div>; if (!data) return <div>loading...</div>; return <div>hello {data.name}!</div>; } -
react-query:全面的异步状态管理
javascript复制const { data, isLoading, isError } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); -
ahooks:高质量的React Hooks库
javascript复制const debouncedValue = useDebounce(value, { wait: 500 }); useEffect(() => { // 只在防抖后值变化时执行 }, [debouncedValue]);
16.2 调试工具
- React DevTools:检查组件hooks和effect列表
- use-effect-debugger:追踪effect执行原因
javascript复制useEffectDebugger(() => { // effect代码 }, [deps], 'effect名称'); - why-did-you-render:识别不必要的effect触发
16.3 性能分析工具
- React Profiler:记录组件渲染和effect执行时间
- Chrome Performance Tab:分析effect对主线程的影响
- Sentry/LogRocket:监控生产环境effect异常
17. 生产环境经验总结
17.1 监控effect异常
在生产环境捕获effect错误:
javascript复制useEffect(() => {
const handleError = (error) => {
logErrorToService(error);
};
try {
// effect逻辑
} catch (error) {
handleError(error);
}
}, [deps]);
17.2 内存泄漏的预防
建立effect清理检查清单:
- 所有setInterval/clearInterval成对出现
- 事件监听器正确移除
- 取消未完成的网络请求
- 清理第三方库创建的DOM元素
- 断开WebSocket连接
17.3 性能关键路径优化
识别并优化关键effect:
- 使用React.memo减少不必要渲染
- 对重型effect使用防抖/节流
- 考虑Web Worker转移计算密集型任务
- 使用React.lazy和Suspense代码分割
18. 源码导读建议
要深入理解React effect实现,建议按以下顺序阅读源码:
- react/packages/react/src/ReactHooks.js:Hook类型定义
- react/packages/react-reconciler/src/ReactFiberHooks.js:核心Hook逻辑
- react/packages/react-reconciler/src/ReactFiberWorkLoop.js:调度系统
- react/packages/react-reconciler/src/ReactFiberCommitWork.js:commit阶段
- react/packages/scheduler/src/forks/Scheduler.js:优先级调度
关键数据结构:
Hook:存储hook状态的对象Effect:effect链表节点结构Fiber:React fiber节点定义UpdateQueue:更新队列结构
调试技巧:
javascript复制// 在应用中添加以下代码可以打印hook调用栈
window.__REACT_DEVTOOLS_GLOBAL_HOOK__?.injectProfilingHooks?.({
markComponentPassiveEffectMountStarted: (fiber) => {
console.log('Effect mount:', fiber);
},
markComponentPassiveEffectUnmountStarted: (fiber) => {
console.log('Effect unmount:', fiber);
}
});
19. 社区资源推荐
19.1 必读文章
19.2 视频教程
19.3 开源项目参考
- react-use:丰富的自定义Hook集合
- swr:数据获取Hook实现
- react-query:生产级数据管理
20. 个人实践心得
在长期React开发中,关于effect我总结了以下几点经验:
- 最小化effect使用:不是所有副作用都需要effect,优先考虑事件处理或状态提升
- 明确依赖关系:每个effect应该有一个清晰、完整、正确的依赖数组
- 单一职责原则:每个effect只做一件事,复杂逻辑拆分为多个effect
- 清理即正义:总是返回清理函数,即使当前不需要也要养成习惯
- 测试驱动开发:为effect编写测试,特别是异步和清理逻辑
一个典型的effect重构案例:
javascript复制// 重构前:混合多个关注点
useEffect(() => {
// 1. 数据获取
fetchData().then(setData);
// 2. 事件监听
window.addEventListener('resize', handleResize);
// 3. 定时器
const timer = setInterval(pollUpdates, 5000);
return () => {
window.removeEventListener('resize', handleResize);
clearInterval(timer);
};
}, []);
// 重构后:分离关注点
useEffect(() => {
fetchData().then(setData);
}, []);
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
const timer = setInterval(pollUpdates, 5000);
return () => clearInterval(timer);
}, []);
这种分离使得每个effect的职责更清晰,依赖关系更明确,也更容易测试和维护。
