useEffect玩转React Hooks生命周期

今天重新认识了一下useEffect这个钩子函数,以前的时候只会傻乎乎的用,没想到还跟组件的生命周期有关。

先说说 useEffect 最直观的感受吧。在 class 组件里,我们要处理一些副作用操作,比如数据请求、订阅事件、手动修改 DOM 之类的,通常会放在 componentDidMount、componentDidUpdate 这些生命周期函数里。但用了 useEffect 之后,这些操作好像都能放在一个地方处理了,不用再拆分到不同的生命周期里,这点感觉还挺方便的。

就拿我写的那个计数器例子来说吧。我定义了 count 和 num 两个状态,然后在 useEffect 里写了个 console.log (' 数据变化了 '),并且把 count 和 num 放在了依赖项数组里。

js 复制代码
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
useEffect(() => {
  console.log('数据变化了');
}, [count, num]);
return (
  <div>
    {count}
    <button onClick={() => setCount(count + 1)}>点击count</button>
    <br />
    {num}
    <button onClick={() => setNum(num + 1)}>点击num</button>
  </div>
);

这样一来,不管是点击 count 按钮让 count 变了,还是点击 num 按钮让 num 变了,这个 effect 都会执行,控制台都会打印出 "数据变化了"。这其实就对应着 class 组件中的 componentDidUpdate 生命周期,当组件更新时触发。要是依赖项数组里只放 count,那只有 count 变化时才会执行 effect,num 变化就不会管,这种精确控制还挺实用的。

再试试把依赖项数组空着,也就是 [],这时候 effect 只会在组件挂载的时候执行一次。我用这个特性做了个请求 GitHub 仓库列表的功能,在 effect 里写了个异步函数 fetchRepos,调用 GitHub 的 API 获取数据,然后更新 repos 状态。

js 复制代码
const [repos, setRepos] = useState([]);
useEffect(() => {
  const fetchRepos = async () => {
    const response = await fetch('xxx');
    const data = await response.json();
    setRepos(data);
  };
  fetchRepos();
}, []);
return (
  <ul>
    {repos.map(item => (
      <li key={item.id}>{item.full_name}</li>
    ))}
  </ul>
);

这就相当于 class 组件里的 componentDidMount 生命周期,只在组件第一次渲染完成后执行一次,不会因为其他状态变化而重复请求,避免了不必要的网络请求,还挺高效的。要是忘了加这个空数组,那每次组件更新的时候都会发起请求,不仅浪费资源,还可能导致一些奇怪的问题。

不过最让我觉得巧妙的是 useEffect 的清理机制,这和组件卸载的生命周期有关。我写了个 Timer 组件,用 setInterval 实现了一个秒数计时器。刚开始的时候,我发现当组件卸载再重新挂载的时候,计时器会变得混乱,后来才知道是因为之前的计时器没有被正确清理,导致了内存泄漏。

js 复制代码
const Timer = () => {
  const [time, setTime] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      setTime(prevTime => prevTime + 1);
    }, 1000);
    return () => {
      clearInterval(timer);
      console.log('计时器被清理了');
    };
  }, []);
  return <div>已经运行了{time}秒</div>;
};

这时候 useEffect 的返回值就派上用场了,我在 effect 里返回了一个函数,在这个函数里用 clearInterval 清除了计时器。这个返回的函数就对应着 class 组件中的 componentWillUnmount 生命周期,在组件卸载前执行,用于清理资源。然后在父组件里用一个状态控制 Timer 组件的显示和隐藏:

js 复制代码
const [isTimerOn, setIsTimerOn] = useState(true);
return (
  <div>
    {isTimerOn && <Timer />}
    <button onClick={() => setIsTimerOn(!isTimerOn)}>切换计时器</button>
  </div>
);

这样一来,每当点击按钮让 Timer 组件卸载的时候,那个清理函数就会执行,控制台会打印 "计时器被清理了",保证了资源的正确释放,再重新挂载时计时器也能正常工作。

其实,React Hooks 的生命周期可以更细致地理解。组件挂载时,会先初始化状态,然后执行渲染,接着运行所有不带依赖项或依赖项数组为空的 useEffect。就像上面请求 GitHub 仓库列表的例子,组件一挂载,就会执行这个 useEffect 来获取数据。

当组件更新时,也就是状态或 props 发生变化时,会先重新计算状态,然后重新渲染,之后执行依赖项发生变化的 useEffect。比如计数器例子中,count 或 num 变化后,对应的 useEffect 就会执行。而且在执行新的 effect 之前,会先执行上一次 effect 返回的清理函数(如果有的话),这样能避免一些冲突。

组件卸载时,会执行所有 effect 返回的清理函数,确保资源被妥善处理,就像 Timer 组件卸载时,会清除计时器一样。

另外,关于 useEffect 还有个小细节,就是它不能直接用 async/await。刚开始我还纳闷为什么,后来才明白,因为 useEffect 要么不返回值,要么就得返回一个清理函数,而 async 函数返回的是一个 Promise,这就不符合要求了。就像这样写是不行的:

js 复制代码
// 错误写法
useEffect(async () => {
  const response = await fetch('xxx');
  const data = await response.json();
  setRepos(data);
}, []);

所以正确的做法是在 effect 内部定义一个异步函数,然后再调用它,就像我在请求 GitHub 数据时做的那样,就是前面那段获取仓库列表的代码,那样写才不会有问题。

总的来说,useEffect 把 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 这几个生命周期函数的功能整合到了一起,让函数组件拥有了处理副作用的能力。

现在再看 Hooks,发现它确实让 React 组件的编写变得更加灵活和简洁了。不用再写 class,不用再考虑 this 指向的问题,只需要用这些以 use 开头的钩子函数,就能实现各种复杂的功能。虽然刚开始接触的时候会有点不习惯,但一旦上手了,就会发现它的魅力所在。

相关推荐
恋猫de小郭4 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅10 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端