React的Hooks函数useEffect和useLayoutEffect

React 的 useEffect 是一个重要的 Hook,用于处理组件的副作用,而useLayoutEffect和它一样,都是处理函数组件内的副作用的,下面就来说说这两个Hook函数的用法。

useEffect

useEffect是类组件中的生命周期componentDidMount,componentDidUpdate 和 componentWillUnmount 的合并版。

在函数组件中,使用useEffect,它接收两个参数:一个函数和一个依赖数组,定义如下:

useEffect( ()=>{ return ()=>{ } } , [ ])

()=>{ return ()=>{ } }:这个函数会在组件的挂载和更新时被调用,它可以包含一些副作用操作,比如操作DOM、发送网络请求等;在这个函数里还可以选择返回一个return函数,这个return函数会在组件卸载的时候执行。
[]:这个数组是一个可选参数,用来指定 useEffect 的依赖项,依赖项包括函数组件的props、state 以及直接在组件主体中声明的所有变量和函数,当依赖数组中的任何一个值发生变化时,都会触发 useEffect 重新执行,如果依赖数组为空,则表示 useEffect 没有任何依赖,只会在组件挂载和卸载时执行。

useEffect基础使用

下面代码是useEffect最简单的用法:

const Parent = () => {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  useEffect(() => {
    console.log('我改变了'+count+'次')
  })
  const handleClick =  () =>{
    setCount(count + 1)
  }
  return (
    <div>
      <h2>React的Hooks函数useEffect和useLayoutEffect</h2>
      <p>Count: {count}</p>
      <p>Num: {num}</p>
      <button onClick={handleClick}>改变状态count</button>
      <button onClick={() => setNum(num + 1)}>改变状态num</button>
    </div>
  );
};

运行上面代码,可以知道组件挂载时执行了useEffect,因此我们可以在useEffect函数组件中执行一些副作用,比如:数据请求、操作DOM、绑定事件等。

当点击按钮"改变状态count"时,执行了useEffect;当点击按钮“改变状态num”时,也执行了useEffect,因此不管组件内的哪个状态或属性改变都会重新执行useEffect

useEffect数组依赖

情景一
下面代码里useEffect添加了一个空数组依赖项[]:

const Parent = () => {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  useEffect(() => {
    console.log('count改变了'+count+'次')
  }, [])
  const handleClick =  () =>{
    setCount(count + 1)
  }
  return (
    <div>
      <h2>React的Hooks函数useEffect和useLayoutEffect</h2>
      <p>Count: {count}</p>
      <p>Num: {num}</p>
      <button onClick={handleClick}>改变状态count</button>
      <button onClick={() => setNum(num + 1)}>改变状态num</button>
    </div>
  );
};

运行上面代码,依然可以看到组件挂载时,执行了useEffect;当点击按钮"改变状态count"时,没有执行useEffect;当点击按钮“改变状态num”时,也没有执行useEffect;因此知道,如果useEffect添加了空数组依赖项后,只有组件挂载时才会执行useEffect,其他操作改变组件内的属性状态等都不会执行useEffect

情景二
下面代码里useEffect添加了非空数组依赖项count

const Parent = () => {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  useEffect(() => {
    console.log('count改变了'+count+'次')
  }, [count])
  const handleClick =  () =>{
    setCount(count + 1)
  }
  return (
    <div>
      <h2>React的Hooks函数useEffect和useLayoutEffect</h2>
      <p>Count: {count}</p>
      <p>Num: {num}</p>
      <button onClick={handleClick}>改变状态count</button>
      <button onClick={() => setNum(num + 1)}>改变状态num</button>
    </div>
  );
};

运行上面代码,当点击按钮"改变状态count"时,改变count状态,重新执行了useEffect,而点击按钮“改变状态num”时,没有执行 useEffect。因此useEffect的依赖项有值时,依赖项的值发生变化,那么函数就会重新执行useEffect,而没在依赖项的值发生了变化,并不会触发useEffect函数的执行。

useEffect的return函数

情景一
下面代码是个子组件,在子组件内写了useEffect方法,并且返回了一个return函数:

function Child(){
  useEffect(() => {
    console.log('我是子组件的useEffect')
    return () => {
      console.log('我被卸载了')
    }
  })
  return (
    <div>
      我是子组件
    </div>
  )
}

下面代码是父组件 ,在父组件内使用上面的子组件,并且在父组件内做了个判断,当count > 2时,隐藏子组件:

const Parent = () => {
  const [count, setCount] = useState(0);
  const handleClick =  () =>{
    setCount(count + 1)
  }
  return (
    <div>
      <h2>React的Hooks函数useEffect和useLayoutEffect</h2>
      <p>Count: {count}</p>
      <button onClick={handleClick}>改变状态count</button>
      {count > 2?null:<Child></Child>}
    </div>
  );
};

浏览器效果
useEffect的return函数
运行上面的代码,点击按钮"改变状态count",改变count的值,子组件内的useEffect每次都被执行了,且里面的return函数先于其他操作执行,那是因为每次重新执行函数时,会先卸载上次的函数执行return函数操作。当count > 2时,子组件被隐藏了,这时,只执行了子组件内的return函数。因此,useEffect内的return函数会在组件被卸载的时候执行。
情景二
下面代码还是上面的子组件,只是修改了子组件内的useEffect方法,给方法添加了空依赖项数组,并且返回了一个return函数:

function Child(props){
  useEffect(() => {
    console.log('我是子组件的useEffect')
    return () => {
      console.log('我被卸载了')
    }
  },[])
  return (
    <div>
      我是子组件
    </div>
  )
}

下面代码依然是上面的父组件不做其他改变:

const Parent = () => {
  const [count, setCount] = useState(0);
  const handleClick =  () =>{
    setCount(count + 1)
  }
  return (
    <div>
      <h2>React的Hooks函数useEffect和useLayoutEffect</h2>
      <p>Count: {count}</p>
      <button onClick={handleClick}>改变状态count</button>
      {count > 2?null:<Child></Child>}
    </div>
  );
};

浏览器效果
useEffect的return函数
运行上面的代码,点击按钮"改变状态count",改变count的值,子组件内的useEffect没有被执行了,只有当count > 2时,子组件被隐藏了,这时,才会执行子组件内的useEffect里返回的return函数。因此,useEffect内的return函数会在组件被卸载的时候执行。

综合以上,可以在组件卸载时,在useEffect内的return函数执行一些取消请求操作清除定时器移除事件监听断开soket等操作。比如:
清除定时器

useEffect(() => {
    const timer = setInterval(() => {
        //...
    }, 1000);
    return () => {
        clearInterval(timer);
    };
}, []);

事件监听器

useEffect(() => {
    const onResize = () => {
        console.log("监听窗口大小");
    };
    window.addEventListener('resize', onResize);
    return () => {
        window.removeEventListener('resize', onResize);
    };
}, []);

useEffect多次使用

下面是在组件多次使用useEffect的情况:

const Parent = () => {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
    /* 
     在组件 第一次渲染完 && 每一次更新完 调用 
     等同于 componentDidMount && componentDidUpdate
    */
    useEffect(() => {
        console.log('我是第1个', count, num);
    });

    /* 
     只在组件 第一次渲染完 调用 
     等同于 componentDidMount
    */
    useEffect(() => {
        console.log('我是第2个', count, num);
    }, []);

    /* 
     第一次渲染完 以及 依赖的状态发生改变 时调用
    */
    useEffect(() => {
        console.log('我是第3个', count);
    }, [count]);

    /* 
     返回的函数将在 组件卸载后 被调用
     等同于 componentWillUnmount
     */
    useEffect(() => {
        return () => {
            console.log('我是第4个');
        };
    }, []);
  const handleClick =  () =>{
    setCount(count + 1)
  }
  return (
    <div>
      <h2>React的Hooks函数useEffect和useLayoutEffect</h2>
      <p>Count: {count}</p>
      <p>Num: {num}</p>
      <button onClick={handleClick}>改变状态count</button>
      <button onClick={() => setNum(num + 1)}>改变状态num</button>
    </div>
  );
};

useEffect原理

函数组件在渲染或更新期间,遇到useEffect操作,会基于MountEffect方法把callback(和依赖项)加入到effect链表中。
在视图渲染完毕后,基于UpdateEffect方法,通知链表中的方法执行。

  • 按照顺序执行期间,首先会检测依赖项的值是否有更新,有更新则把对应的callback执行,没有则继续处理下一项。
  • 遇到依赖项是空数组的,则只在第一次渲染完毕时,执行相应的callback
  • 遇到没有设置依赖项的,则每一次渲染完毕时都执行相应的callback

useLayoutEffect

useLayoutEffectuseEffect 都是 React 提供的用于处理副作用的钩子函数,如果会用useEffect,那么一定也会用useLayoutEffect ,因为它们的用法一模一样,它们的主要区别在于执行时间不同。
运行下面代码:

const Parent = () => {
  const [count, setCount] = useState(0);

  useEffect(()=>{
     console.log('执行的是useEffect')
  })
  useLayoutEffect(() =>{
    console.log('执行的是useLayoutEffect')
  })

  const handleClick =  () =>{
    setCount(count + 1)
  }
  return (
    <div>
      <h2>React的Hooks函数useEffect和useLayoutEffect</h2>
      <p>Count: {count}</p>
      <button onClick={handleClick}>改变状态count</button>
    </div>
  );
};

浏览器效果
useLayoutEffect
通过上面效果,可以知道useLayoutEffectuseEffect执行,那它们分别是在什么时候执行的呢?

useLayoutEffect是在组件基于render方法将寻DOM转换为真实DOM对象之后,在浏览器绘制之前同步执行,useLayoutEffect会阻塞浏览器绘制真实DOM,优先执行Effect链表中的callback,由于已经创建了真实DOM,所以可以在useLayoutEffect中进行对真实 DOM 的操作。
useEffect在浏览器执行绘制之后被调用,也就是组件渲染到页面上后执行。执行的同时,页面上旧的DOM样式可能还存在,所以在更新状态执行useEffect时,会看到旧UI闪一下的情况。

总结

useLayoutEffectuseEffect 在实现上是类似的,不同之处在于 useLayoutEffect 的回调函数会在 DOM 更新之后,浏览器执行绘制之前被调用,而 useEffect 的回调函数则是在浏览器执行绘制之后被调用。
useLayoutEffect 在执行回调函数时会产生阻塞效果,可能会导致页面感觉卡顿,而 useEffect 则不会产生这种阻塞效果。


 上一篇
React的Hooks函数useRef和useImperativeHandle React的Hooks函数useRef和useImperativeHandle
在之前的React的类组件ref使用笔记中,介绍了在类组件中使用ref,本笔记主要记录在函数组件中使用ref,在函数组件中是不能直接使用ref的,而是要使用Hook函数useRef来实现类似在类组件中ref的功能。 初识useRef use
2022-04-12
下一篇 
React的Hooks函数useState React的Hooks函数useState
前言 在React开发中,类组件开发是具备状态的,可以使用this.setState来更新状态,使组件重新渲染,但在函数组件中,是不具备状态,也没有实例的概念,调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文,且在函数组件中不
2022-04-11
  目录