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 开头的钩子函数,就能实现各种复杂的功能。虽然刚开始接触的时候会有点不习惯,但一旦上手了,就会发现它的魅力所在。

相关推荐
musk12129 分钟前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
万少1 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL1 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl021 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang2 小时前
前端如何实现电子签名
前端·javascript·html5
今天又在摸鱼2 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿2 小时前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再2 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
jingling5552 小时前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架
拾光拾趣录2 小时前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css