React hook 学习(一)

92 阅读9分钟

0. 关于函数的副作用

函数的副作用是什么?什么是纯函数,什么是非纯函数?(之前这个问题困扰了我很久,很多时候我只是局限在怎么去使用这个hook,比如看了 useEffect 和 useMemo 的基本用法,但是不知道什么时候,为什么要用)

  1. 纯函数的定义:

    纯函数指的是,这个函数和外界进行数据交互只有两个渠道:传入的参数,和返回的数据。其他部分不会和外界有相关的交互或者改变。

  2. 非纯函数:

    非纯函数和上面的纯函数恰恰相反,这个函数有其他的,不通过 参数 or 返回数据 来进行数据交互(获取或者修改、输出数据。for example,通过请求来进行数据获取)

  3. 函数副作用:

    函数副作用主要指的是:这个函数会对主调函数(就是该 function 的 caller)会有一些,除了 返回函数返回值 的影响。那么这就是这个函数产生的副作用。 从上面的介绍来看,只有非纯函数会产生副作用(也不一定,但是纯函数不会产生副作用)

1. React hook:

其实之前都了解了一些基本 hook 的用法,比如 useState、useEffect等基本使用方法。但是很多时候都是从使用层面来看的。

  1. 为什么会产生 React hook?

    为了实现 逻辑复用(非 UI 层面)。UI 层面,之前直接可以使用 组件 的方式来进行解决。但是如果不是UI层面的呢?

    比如,一个经典案例:我需要写一个 用户登录 之后进行操作的页面。但是用户的信息和状态都会不断变化。那么,我们需要对其中的用户状态信息进行订阅(监听是否有变化,并及时更新)。

    其实这种场景很常见,但是在很多情况下,对用户状态的监听都是需要的。这个时候你就需要把这部分相关的逻辑copy出来(比如,componentWillMount() componentDidMount() 各种生命周期函数都需要对这个获取状态、存储状态进行处理),然后塞到别的需要的地方。

    这种事情听起来挺恶心的,毕竟重复代码在维护的时候要修改很多地方,很可能有的时候你就忘记了哪个地方还要修改。

    之前有一种解决方法,就是使用高阶组件。也就是你每次只需要进行基础的ui开发,然后把这些内容输入你的高阶组件包装函数中,让高阶函数为你的组件添加其他的功能。

    但是高阶组件会出现的一些问题。比如,当有的功能进行变动的时候,高阶组件之间要进行嵌套和组合的时候,都很容易出现问题。

    这个时候 react 尝试使用 hook 来进行解决。同样是上面的案例,我们可以使用一个相关的自定义hook,将上面相关 useStateuseEffect 的逻辑包装起来,每次遇到需要订阅相关用户状态的情况,就直接调用这个我们自己已经定义好的 hook 就可以了。

    而且,使用了 hook 之后,你可以将很多的逻辑处理单独的剥离出来,而不是之前那样在每个生命周期函数中都需要糅杂一段又一段不相关的逻辑在其中。

  2. useEffect 的使用:

    useEffect,顾名思义,就是用于管理副作用的 hook。简单的写法如下所示:

    useEffect(()=>{
        // 每次 dom 重新渲染的时候,都会执行 useEffect 的第一个参数的相关函数
        doingSomething();
        
        // return 的函数:最后组件 unmounting 的时候会进行的操作。可以不写 return 相关函数
        return ()=>{
            unmounting(); // 卸载组件进行的收尾处理
        }
    }, [dependence]);
    // useEffect 的第二个参数:是一个数组,这个数组内是相关的依赖项。
    // 如果有依赖项,那么在之后每一次的再次渲染时,都会比对依赖项中的变量是否发生改变。
    // 如果不改变,那么就不会执行 useEffect 中注册的相关函数。
    

    这个函数的两个参数的含义:

    • 第一个参数:function 每次在 dom 重新渲染的时候都会执行这个函数。
    • 第二个参数:Array 如果有这个参数,那么每次在 dom 重新渲染的时候,还会查看相关的数组中的元素是否发生了变化。如果没有发生变化,那么在重新渲染的时候也不会去执行第一个参数的相关函数。

    使用 useEffect 可以让我们把很多代码按照 功能 纬度分离开来,而不是按照 生命周期 的纬度来拆分和执行代码。

    常见的一个用法就是,使用 useEffect 来写与请求相关的操作:

    useEffect(()=>{
        fetchUserInfo(); // 当 id 发生改变的时候,请求用户信息
    }, [id]);
    

    下面会简单说一下,组件会发生重新渲染的情况。这也是 useEffect 中注册的函数会执行的时候。

    Tips:React 组件什么时候会进行重新渲染(关乎到什么时候会执行 useEffect 中的函数)

    1. state 发生改变的时候
    2. props 重新传参的时候
  3. useCallback 的使用:

    useCallback 函数返回传入函数包装后的一个引用。这个函数的基本用法是这样的:

    let testCallback = useCallback(()=>{
        doSomething(); // 内部函数执行操作
    }, [dependence]); // 依赖项数组
    

    其中两个参数的含义如下:

    • 第一个参数:function 表示被包装的函数。
    • 第二个参数:Array 依赖项构成的数组。只有在依赖项发生改变的时候,才会产生一个新的包装之后的函数的引用。

    之前特别好奇为什么要对函数也做一个包装。应该是为了能够让传入组件作为 props 的内联函数,减少不必要的重新生成该函数引用的情况,从而减少子组件传入新的 props 的情况,减少不必要的重新渲染。

    后面看到了这样一个实例,说到的是当父子组件进行通信的时候,可能会产生的一个无限循环情况。例子如下:

    举例子🌰

    // 用于记录 getData 调用次数
    let count = 0;
    
    function App() {
      const [val, setVal] = useState("");
    
      function getData() {
        // 模拟发送请求
        setTimeout(() => {
          setVal("new data " + count);
          count++;
        }, 500);
      }
    
      return <Child val={val} getData={getData} />;
    }
    
    function Child({val, getData}) {
      useEffect(() => {
        getData();
      }, [getData]);
    
      return <div>{val}</div>;
    }
    

    为什么会产生死循环呢?下面来逐步看一下相关的渲染过程。

    • step 1:初始渲染,父组件 App 和子组件 Child 都进行初始化渲染。由于 useEffect 函数中会调用 getData,所以 0.5s 之后会调用相关函数,改变父组件 state。
    • step2:父组件 更新渲染,父组件 App 由于 state 改变,进行重新渲染,同时会传入新的 val 和 新的 getData 作为子组件的 props。
    • step3:子组件 更新渲染,子组件 Child 接受到新的 props,所以会重新进行渲染,并且由于会得到一个新的 getData 索引,导致重新执行 useEffect,改变 val
    • 造成 死~循~环~
  4. useMemo 的使用:

    useMemouseCallback 不同,这个 hook 主要是为了避免重复进行复杂运算,从这方面来提高性能。

    当我们的组件其中可能会出现复杂运算,来计算一个值的时候,在组件重新渲染的时候,可能就要进行一些重复计算。但是这些重复计算可能会非常耗时。而且有些情况下(比如入参相同的情况),得到的结果是相同的,没必要进行重复计算。

    React 设计了这个 hook,来帮助减少复杂计算的重复运算,带来的不必要的消耗。

    下面首先来简单介绍一下 useMemo 的基本使用。后面给出一个实例。

    let num = useMemo(()=>{
        let res = calculate(); // 进行了一个复杂计算
        return res; // 返回这个复杂计算的结果,num 会获取到这个值
    }, [dependence]); // 依赖项数组,只有当这个数组内的内容发生改变之后,才会进行重新计算,返回重新计算的值
    
    • 第一个参数:function 该函数进行计算并返回对应的计算值。
    • 第二个参数:Array 依赖项数组。重新渲染时,只有当依赖项数组内容有改变的时候,才会进行重新计算。返回新的值。
    • @return:第一个参数 function 返回的内容

    实例

    在这个 React 组件中,我们会进行一个复杂计算(当然这个复杂计算我也不知道是什么复杂计算),这个计算很耗时。其他的 state 发生改变的时候,组件会进行重新渲染,但是这个值不一定会改变。重复计算会浪费我们的资源。

    function TestComp(props){
        const [num, setNum] = useState(1); // 新的 num 出现的时候,我们会对新的 num 进行
        const [random, setRandom] = useState(0); // 测试使用。改变 random 的时候,组件也会进行重新渲染
        
        // useMemo use
        let performance = useMemo(()=>{
            console.log("开始复杂计算!");
            
            let res = num * 2; // 当做这个是一个复杂计算(其实实际上这么简单的没必要用 useMemo)
            return res; // res 会给 performance 赋值
        }, [num]); // num 改变时候,重新渲染才会重新计算 performance
        
        return <>
            <div>perdformance: {performance}</div>
            <div>num: {num}</div>
            <div>random: {random}</div>
            
            <Button onClick={()=>{ setNum(num+1) }}>增加 num</Button>
            {/* 不会触发performance重新计算 */}
            <Button onClick={()=>{ setRandom(random+2) }}>增加 random</Button>
        </>
    }
    
  5. useState 注意:

    这个 hook 大家只要用过 hook 应该都是知道的(简单来说就是在 function component 中引入 state 的 hook)。

    但是有一点大家需要注意的。useState 函数内初始值的初始化只会在首次渲染组件的时候执行。之后 update 的时候,其中不会再对对应 state 进行初始化。 (可怜的我之前在这个问题上挣扎了好久)


下一次会介绍 hook 其中的相关闭包问题(写不动了呜呜)