浅谈useEffect

本文详细探讨了React Hooks中的useEffect,包括它的作用、使用方式、执行时机、有条件执行、清除副作用、类比生命周期函数,以及如何处理依赖项。强调了依赖项的重要性,解释了忽略依赖项可能导致的错误,并提供了减少或移除依赖的技巧,如使用函数更新状态、将函数移入effect内部、使用useCallback和useReducer。最后讨论了useEffect在处理数据变化和竞态条件时的策略,如防抖、节流和使用Ref解决竞态问题。
摘要由CSDN通过智能技术生成

作用

useEffect接收一个函数,可以让用户在函数组件中执行副作用操作,如:

  1. 设置订阅和事件处理
  2. ajax请求等异步操作
  3. 更改DOM对象及其他会对外部产生影响的操作等

使用方式

useEffect(create[, deps]);

第一个参数是要执行的 effect,而第二个参数是依赖项,依赖项是选填的。

例如

function App() {
  useEffect(() => {
    document.title = 'example'; // 副作用操作
  });
  return <div />;
}

执行时机

传递给useEffect的函数(effect)会在浏览器完成布局与绘制之后延迟(异步)执行,这里的异步实现优先级如下:setImmediate > MessageChannel > setTimeout,并且 React 会保证每次运行 effect 的时候 DOM 都已经更新完毕。虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行 ,这是官网上的一句描述,很不起眼的一句话,甚至不知道怎么理解这句话,我一开始也这样,直到后面看到这样一个例子

import "./styles.css";
import { useState, useEffect } from "react";

export default function App() {
  const [a, setA] = useState("a");
  const [b, setB] = useState("b");

  console.log("[render]", a, b);

  useEffect(() => {
    console.log("[useEffect]", a, b);
  });

  function handleClickWithPromise() {
    Promise.resolve().then(() => {
      console.log("async handler1", a, b);
      setA("aa");
      console.log("async handler2", a, b);
      setB("bb");
      console.log("async handler3", a, b);
    });
  }

  function handleClick() {
    console.log("sync handler1", a, b);
    setA("aaa");
    console.log("sync handler2", a, b);
    setB("bbb");
    console.log("sync handler3", a, b);
  }

  return (
    <div className="App">
      <button onClick={handleClickWithPromise}>
        {a} - {b} with Promise
      </button>
      <button onClick={handleClick}>
        {a} - {b} without Promise
      </button>
    </div>
  );
}

这个例子不仅关乎到 useEffect 的执行时机,还涉及到 setState 的执行方式。简单的说 setState 的执行会触发组件的重新渲染,即函数的重新执行。setState 本身是同步执行的,但是在 由 React 控制的 事件处理函数,以及生命周期函数(类组件)调用 setState 时会将多个 setState 进行合并然后延迟执行,在 React控制之外的 如 setTimeout/setInterval、Promise等里面执行 setState 则不会合并处理,表现为同步执行。所以上述例子当我们点击 {a} - {b} with Promise 在 Promise 中调用 setState 时,会立即同步执行重渲染,再来看官网这句话,便明白为什么会看到这样的打印结果。

有条件的执行

默认情况下,effect 会在每轮组件渲染完成后执行,但有些时候我们不想要这样,可能只是想挂载完后设置订阅,或者某个数据改变后才执行effect,以此来做一些优化或者避免 bug 的发生。此时我们可以给 useEffect 传递第二个参数,它是 effect 所依赖的值的数组,当设置了第二个参数 deps 后,effect只会在所依赖的值发生变化时(使用 Object.is 进行比较)才运行。

需要清除的effect

有一些副作用是需要清除的,比如我们绑定的事件在组件卸载的时候需要解绑等,不然可能会导致一些意料之外的错误或者内存溢出,在类组件中通常会在 comonponentWillUnmount(vue则为beforeDestory )中清除副作用,而在useEffect中,我们可以使 effect 返回一个函数,在该函数中清除副作用,React将会在执行清除操作(组件卸载的时候)时调用该函数,我们称之为清理函数。例如:

function App() {
  useEffect(() => {
    const handleScroll = () => {};
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  });
 	return <div />
}
  • 首次渲染组件清理函数不会运行
  • 清理函数的运行时间点是每次运行副作用函数之前
  • 组件被销毁时一定会运行清理函数

在React v17.0之前 useEffect 的清理函数是同步运行的,在React v17.0中清理函数更新为异步运行 —— React v 17.0

看如下代码,首次进入和点击 increase 1 分别打印什么?顺序是怎么样的

export default function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`count is ${count} effect`);
    return () => console.log(`clear count ${count} effect`);
 	}, [count]);
  console.log(`count is ${count} render`);
  return (
    <div>
     	<div>{count}</div>
      <button onClick={() => setCount(count + 1)}>increate 1</button>
    </div>
 	);
}

类比生命周期

如果你熟悉 React Class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。 —— 使用 Effect Hook。如下面的类组件例子

class App extends React.Component {
	componentDidMount() {
    const { id } = this.props;
    fetchData(id);
    subsribe(id);
    window.addEventListener(...);
    ...
  }
  componentDidUpdate() {
    const { id } = this.props;
    fetchData(id);
    subscribe(id);
    ...
  }
  componentWillUnmount() {
    removeSubscribe(this.props.id);
    window.removeEventListener(...);
    ...         
  }
  ...
}

可以看到,我们在 componentDidMount 和 componentDidUpdate 中书写了相同的代码,这在我们日常开发中是非常常见的,因为我们希望在组件挂载和更新的时候做一些同样的操作,比如重新获取数据,亦或是在组件卸载的时候清除副作用,当我们有很多类似的操作的时候,不仅会书写很多重复的代码,而且相关联的代码分散在不同的生命周期函数中,当代码量多且复杂的时候就会变得不好管理。而改用 useEffect Hooks 的话会变成怎么样呢?

function App(props) {
  const { id } = props;
  useEffect(() => {
    fetchData(id);
  }, [id]);
  
  useEffect(() => {
    subscribe(id);
    return () => removeSubscribe(id);
  }, [id]);
  
  useEffect(() => {
    window.addEventListener(...);
    return () => window.removeEventListener(...);
  }, [...]);
      
  ...
}

基于 useEffect 的这种设计,我们不用再去考虑当前的 effect 是“挂载”还是“更新”,可以很好的实现 关注点分离 ,还可以在 effect 中返回一个函数,函数里面清除该 effect 中存在的副作用影响,相关代码都汇聚到了一块。当代码量和复杂度提高的时候甚至可以提取成自定义Hooks进行使用。

模拟componentDidMount

useEffect(() => {
   
  console.log('模拟componentDidMount')
}, [])

模拟componentDidUpdate

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值