深入剖析前端组件生命周期管理与实战应用
我的写法,亲测靠谱
在 React 项目里搞生命周期,我折腾过好几轮。早期用 class 组件时,componentDidMount、componentWillUnmount 写得飞起,但后来 Hooks 横空出世,很多老习惯反而成了坑。现在我基本只用函数组件 + useEffect,但怎么写才不容易出问题?我总结了一套自己的写法,亲测在多个中大型项目里跑得稳。
核心原则就一条:把副作用拆干净,别混在一起。很多人喜欢在一个 useEffect 里干一堆事:发请求、监听事件、操作 DOM……结果清理逻辑乱成一锅粥,内存泄漏、重复请求、状态错乱全来了。
我现在的做法是:每个 useEffect 只干一件事,依赖项明确,清理函数也只管自己的事。比如下面这个获取用户信息的例子:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (!userId) return;
let isCancelled = false;
fetch(https://jztheme.com/api/user/${userId})
.then(res => res.json())
.then(data => {
if (!isCancelled) {
setUser(data);
}
});
return () => {
isCancelled = true;
};
}, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
这里用了 isCancelled 标志位来避免组件卸载后还更新状态(虽然 React 18+ 会自动忽略,但老项目或严格模式下还是保险点好)。关键是,这个 effect 只负责拉数据,不掺和别的逻辑。如果还要监听窗口 resize,那就另开一个 effect,别塞一起。
这几种错误写法,别再踩坑了
我见过太多人栽在这些写法上,自己也踩过,列几个典型反面教材:
- 依赖项写成空数组,但实际用了 props 或 state:比如在 effect 里用了
props.id,但依赖项写成[],结果永远拿不到最新 id。React 官方 ESLint 插件能帮你抓这种问题,但很多人关了警告,图省事。 - 在 effect 里直接调用 setState 触发无限循环:比如监听某个状态变化,然后又去改它,没加条件判断。常见于表单联动场景,折腾半天发现是自己触发的 re-render。
- 清理函数里用错闭包变量:比如在
addEventListener时传了一个函数,removeEventListener时传了另一个(因为每次 render 都生成新函数),导致监听器根本没被移除。正确做法是用useCallback包裹事件处理器,或者用 ref 存函数引用。
举个具体例子,下面这段代码在组件卸载时无法正确移除监听器:
// ❌ 错误写法
function BadComponent() {
useEffect(() => {
const handler = () => console.log('resize');
window.addEventListener('resize', handler);
return () => {
// 每次 render 都生成新 handler,remove 的不是同一个
window.removeEventListener('resize', handler);
};
}, []);
}
正确写法应该是:
// ✅ 正确写法
function GoodComponent() {
const handlerRef = useRef();
useEffect(() => {
handlerRef.current = () => console.log('resize');
});
useEffect(() => {
const handler = () => handlerRef.current?.();
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, []);
}
或者更简单点,用 useCallback(如果 handler 不依赖外部变量):
// ✅ 也可以这样
function AnotherGoodComponent() {
const handler = useCallback(() => console.log('resize'), []);
useEffect(() => {
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, [handler]);
}
实际项目中的坑
在真实业务里,生命周期的问题往往藏得更深。比如我们有个列表页,点击 item 进入详情页,返回时要保持滚动位置。一开始我在 useEffect 里用 window.scrollTo,但发现有时候 scroll 位置不对,因为数据还没加载完就执行了。
后来改成:等数据加载完成 + 组件挂载后,再恢复滚动。但要注意,不能只依赖 useEffect,因为可能多次触发。最后我加了个标志位,确保只执行一次:
function ListPage() {
const [items, setItems] = useState([]);
const hasRestoredScroll = useRef(false);
useEffect(() => {
fetchItems().then(data => {
setItems(data);
if (!hasRestoredScroll.current) {
restoreScrollPosition();
hasRestoredScroll.current = true;
}
});
}, []);
// 注意:这里不能把 hasRestoredScroll 放进依赖项,
// 因为它是 ref,不会触发 re-render
}
另一个常见问题是:在 useEffect 里调用 async 函数。很多人直接写 async () => { ... },但 React 要求 effect 返回的是清理函数,不是 Promise。所以得在 effect 内部定义 async 函数并立即调用:
// ✅ 正确方式
useEffect(() => {
const fetchData = async () => {
const data = await api.get('/data');
setData(data);
};
fetchData();
}, []);
别写成:
// ❌ 错误!effect 不能是 async
useEffect(async () => {
const data = await api.get('/data');
setData(data);
}, []);
虽然有些项目里这么写暂时没报错,但严格模式下会警告,而且未来可能出问题。
一点不完美的妥协
说实话,没有 100% 完美的方案。比如在某些复杂表单里,我为了快速上线,还是会把多个逻辑塞进一个 effect,靠注释分隔。虽然心里知道不好,但 deadline 逼人啊。不过我会在注释里标清楚“TODO: 拆分”,等有空再重构。
还有,React 18 的并发特性让一些旧的生命周期假设失效了(比如 useEffect 可能执行多次),但大部分项目还没升级到 strict mode,所以暂时按老方式处理也能跑。只是心里得清楚,有些写法在未来版本里可能翻车。
总之,我的经验是:能拆就拆,依赖项别偷懒,清理函数要配对,异步别直接写。虽然啰嗦点,但省下的 debug 时间绝对值回票价。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流。

暂无评论