React Hook使用之useEffect和useLayoutEffect

组件的写法

传统的class组件的写法

这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性。


class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      

You clicked {this.state.count} times

); } }

由于需要修改document的title属性,不能直接进行绑定,所以需要在组件更新的时候使用代码同步更新title属性的值。同时更新需要在react的生命周期中执行操作,包括了componentDidMountcomponentDidUpdate。下面使用react的hook进行改写。

带有hook函数组件的写法
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    

You clicked {count} times

); }

class组件与hook函数组件对比

比较一下hook组件和class组件的写法,可以发现在hook组件中class组件中的componentDidMountcomponentDidUpdate都没有了,只剩下了useEffect()这个hook函数了。

class组件的写法

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

hook函数组件

 useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

那么我们是不是可以认为useEffect()替代了componentDidMountcomponentDidUpdate的作用,那么useEffect是不是用来在函数中操作react的生命周期的?

useEffect的用法

useEffect怎么用?

下面是useEffect函数的TS定义文件。

 function useEffect(effect: EffectCallback, deps?: DependencyList): void;

通过定义文件,我们可以发现useEffect函数有来两个参数,第一个参数是effect函数,是在react生命周期中会被调用的函数,第二个参数deps可有可无,deps参数是一个数组,当deps参数不填时,只要react发生数据更新时,就会调用effect函数;当deps参数为state变量时,只在state变量值发生变化时,才会调用effect函数;当deps参数值为[]时,effect函数只在组件第一次渲染时被执行。

//在组组件第一次渲染和组件数据发生变化时都会执行effect函数
//相当于componentDidMount,componentDidUpdate中调用
useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

//---------------------------------------------------------------------------
//在组件第一次渲染和count的值发生变化时进行调用,相当于vue中的watch count
//相当于在componentDidUpdate中比较prevState,只有在比较相应state发生变化时进行更新
//对界面的更新进行了相应的优化
useEffect(() => {
    document.title = `You clicked ${count} times`;
  },[count]);

//等于
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}
//---------------------------------------------------------------------------
//effect函数只在组件的第一次渲染时被执行,常用于数据请求和一些页面的初始化工作
useEffect(() => {
    document.title = `You clicked ${count} times`;
  },[]);
//等于
 componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

注意,如果effect函数要执行的内容包括异步函数或者Promise,不要直接将effect函数定义为异步函数,只能在effect函数调用异步函数。

useEffect(() => {
    document.title = `You clicked ${count} times`;
    
    //也可以在外部定义异步函数和promise
    async function fetchData() {
    // You can await here
    const response = await MyAPI.getData(count);
    // ...
    }
  fetchData();
  },[count]);

通过上面的写法,effect函数可以完全替代 componentDidMountcomponentDidUpdate,并且写法更简单,但是useEffect函数还有一个更重要的作用,那就是清除副作用,什么是清楚副作用,当我们在组件渲染的时候订阅了一个事件,那我们是不是应该在组件卸载是清除订阅,这个用的订阅就是副作用,当我们在组件渲染时生成的外部对象,我们是不是在组件卸载时要删除对象,传统的class组件有 componentWillUnmount()生命周期函数可以用来清除副作用。

 componentDidMount() {
     //订阅的函数
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    //删除订阅的函数
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

那么在使用useEffecthook的函数组件中该怎么,答案还是在effect函数中,我们的effect函数可以有返回值,它的返回值可以返回一个函数,我们可以在这个函数中执行清除操作。

useEffect(() => {
   //执行订阅的语句
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  //返回一个清除订阅的函数,这个函数会在组件卸载时进行调用
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  //
  });

总后我们发现,通过各种操作,useEffect函数相当与react生命周期中的componentDidMountcomponentDidUpdateomponentWillUnmount()并且使用起来更加灵活简介。

useEffect 做了什么?

通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffect

useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props,函数组件的参数)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗?

是的,默认情况下,它在第一次渲染之后每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。所以在使用useEffect函数进行更新时,我们一定要清除副作用,这样才能避免重复订阅。对于不同的操作类型,我们要学会分离effect,按顺序使用多个useEffect

多个useEffect使用

useEffect使用时可以根据业务在不同生命周期的逻辑选择不同的参数,对于比较服务的业务逻辑,可以使用多个useEffect进行effect函数的业务逻辑分离

function FriendStatusWithCounter(props) {
    
   //useState不能写在第一个useEffect后面
   //因为第一个useEffect用到了count
  const [count, setCount] = useState(0);
  //-----------------------------------------
  //第一个useEffect,只在count发生改变时,更新document的title值
  //count没有发生改变时,没有必要更新title的值
  //组件创建更新时第一个执行
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  },[count]);
//-----------------------------------------------
  const [isOnline, setIsOnline] = useState(null);
 //--------------------------------------------------------
 //第二个useEffect,在组件初始化的时候进行订阅,在组件数据发生变化时,不更新订阅
 //在组件卸载时清楚订阅
 //这两个effect函数的业务逻辑不一样,所以可以分离effect来写
 //组件创建更新时第二个执行
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    //返回清除订阅的函数,会在组件卸载的时候调用
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  },[]);
   //------------------------------------------------------------------
  //useEffect(()=>{to do something})
  //useEffect(()=>{to do something})
  //more

在分离多个useEffect时,注意useEffect的顺序,React hook是按照代码语句的顺序来执行的,如果useStateuseEffect的顺序写错了,可能会导致出现不正确的业务逻辑,也可能导致代码报错。

useLayoutEffect的使用

其函数签名与 useEffect 相同,

//第一个参数 effect函数
//第一个参数 依赖数组
function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void;

但它会在所有的 DOM 变更之后再同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新,也就是说浏览器会等useLayoutEffect内部的effect函数执行完毕后才会进行更新,那么如果我们在useLayoutEffect内部的effect函数执行非常耗时的操作,这样就会阻塞界面的更新。

所以尽可能使用标准的 useEffect 以避免阻塞视觉更新。

如果你希望界面必须在执行完一些业务代码后才进行渲染,那么可以把代码放到useLayoutEffect的effect中。

function App() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    if (count === 0) {
      const randomNum = 111;
      setCount(111);
    }
  }, [count]);

  return (
      //点击按钮节目会立刻更新变成0
      //然后触发effect函数立刻变成111
      //节目短时间更新两次,会出现闪烁
      <div onClick={() => setCount(0)}>{count}</div>
  );
}
function App() {
  const [count, setCount] = useState(0);
  
  useLayoutEffect(() => {
    if (count === 0) {
      const randomNum = 111
      setCount(111);
    }
  }, [count]);

  return (
      //点击按钮,count的值为0,因为有useLayoutEffect,节面不会立刻更新
      //出发因为有useLayoutEffect的effect函数,将count值设为111
      //effect函数完毕,界面更新,界面只更新了一次
      <div onClick={() => setCount(0)}>{count}</div>
  );
}

PS:如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffectcomponentDidMountcomponentDidUpdate 的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect

注意

一般不要在useEffect中调用 state变量的set函数,比如setAbc,因为React组件每次更新时,都会调用useEffect函数,每次调用useEffect函数时会调用里面的setAbc函数,setAbc函数会导致组件更新,就会再次调用useEffect函数,这样就会导致无尽的循环调用,最终导致程序报错堆栈溢出。

你可能感兴趣的:(React,Hook学习以及常见用法详解,react.js,javascript,前端)