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

相关推荐
小妖666几秒前
react-router 怎么设置 basepath 设置网站基础路径
前端·react.js·前端框架
xvmingjiang6 分钟前
Element Plus 中 el-input 限制为数值输入的方法
前端·javascript·vue.js
XboxYan23 分钟前
借助CSS实现自适应屏幕边缘的tooltip
前端·css
极客小俊24 分钟前
iconfont 阿里巴巴免费矢量图标库超级好用!
前端
小杨 想拼31 分钟前
使用js完成抽奖项目 效果和内容自定义,可以模仿游戏抽奖页面
前端·游戏
yvvvy34 分钟前
🐙 Git 从入门到面试能吹的那些事
前端·trae
EmmaGuo20151 小时前
flutter3.7.12版本设置TextField的contextMenuBuilder的文字颜色
前端·flutter
pepedd8642 小时前
全面解析this-理解this指向的原理
前端·javascript·trae
渔夫正在掘金2 小时前
神奇魔法类:使用 createMagicClass 增强你的 JavaScript/Typescript 类
前端·javascript
雲墨款哥2 小时前
一个前端开发者的救赎之路-JS基础回顾(三)-Function函数
前端·javascript