深入剖析前端组件生命周期管理与实战应用

一玉淇 前端 阅读 511
赞 27 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

在 React 项目里搞生命周期,我折腾过好几轮。早期用 class 组件时,componentDidMountcomponentWillUnmount 写得飞起,但后来 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 时间绝对值回票价。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论