React Hook 学习(二)

64 阅读5分钟

上次简单说明了一下为什么会产生hook,几种基本 hook 的使用方法。

这次主要介绍的是 hook 使用时候会出现的一些问题,从 function 组件在渲染和更新的时候都发生了什么开始说起。

当然还要提到之前一直折磨我的 hook 闭包问题,深入理解一下 useEffect 后面的依赖项 Array 由什么组成(hook 检查工具有时候会要求你将调用的相关函数也作为其中的一个参数,一直不明白是为什么)

0. function component 的渲染过程

(在学习的过程中开始思考,function 组件 和 class 组件到底是有什么区别,他们形成和更新的过程都是什么样的)

function component 在每一次渲染过程中,从逻辑上看来,都是这个函数再执行一遍,之后得到的内容挂在到 dom 上。

  • useState 得到的相关 state or props 在 function 函数的存在:

    其中 我们使用 useState 得到的 state 变量,其实是每次执行这个渲染函数的时候(mount or update 的时候),得到的一个相关的 const 值。props 也是如此,不会说什么双向绑定啊,监听啊,其实在那一次渲染中就是得到了一个普通的 const 变量。

  • 事件响应函数在 function comp 中的存在:

    和上面的 state 变量一样,每次渲染都会有一个生成的单独的 事件响应函数,来作为这次渲染的 dom 事件响应函数。

  • hook 相关函数:

    和事件响应函数类似,都是在本次 mount 或者 update 的渲染的时候,其中会单独生成相关的 hook 函数(比如 useEffect 中的第一个参数,响应函数。都是在本次渲染的时候生成当前版本的响应函数,之后会在渲染完成的时候,执行这个相关的函数)

    但是有时候,我想使用能够实时拿到最新更新的值,怎么办?最简单粗暴的方法是,可以使用 ref 把这个值保存在 refs 中。refs 是类似 class component 中 this.value 这样保存在对象中的变量。在每次渲染中你得到的都是更新的最新值。

1. 如何正确使用 useEffect:

咱们来看看下面这个案例(当时困扰了我很久的事情,为什么内部的 state 变量就是不变呢?)

在这里我想要在组件一开始渲染的时候,就挂载一个定时器,这个定时器会每隔一秒就让我们设定的变量自增1。

import {useState, useEffect}, React from 'react';
function TestComp(props) {
    const [cont, setCont] = useState(0);
    
    // 初次渲染就开启定时器,每秒自增1
    useEffect(()=>{
        const id = setInterval(()=>{
            console.debug('cont', cont);
            setCont(cont + 1);
        }, 1000);
        
        return ()=>{
            clearInterval(id);
        }
    }, []);
    
    return <div> count is { cont } now! </div>
}

我想的很美好。可是我发现cont只会自增到1,之后再也不会进行变动了。每次在 setInterval 函数中获取到的 cont 都是0。

这就让人难受了。为什么呢?这里是和闭包有关。因为在第一次渲染的时候,得到的 TestComp 中的 cont 数据和上面说到的情况一样,是一个单纯的 const 变量。

在我们执行 useEffect 函数的时候,获取到当前渲染得到的 cont 值,传入 setInterval 函数中,这个时候内部执行改变 cont 的函数,就是一个闭包(内部函数调用外部变量),这个闭包记录了当时的 cont 值,就是 0,于是每次执行的都是第一次渲染时候得到的值。

那么我们该怎么去解决这个事情呢?

  • 第一种方法:使用 useState 的函数传参形式:

    这个时候保证是从 useState 函数内部去获取相关的 state 值。比如更改上面的案例,是这样的:

    import {useState, useEffect}, React from 'react';
    function TestComp(props) {
        const [cont, setCont] = useState(0);
    
        // 初次渲染就开启定时器,每秒自增1
        useEffect(()=>{
            const id = setInterval(()=>{
                console.debug('cont', cont);
                setCont((count)=>{ return count + 1;});
            }, 1000);
    
            return ()=>{
                clearInterval(id);
            }
        }, []);
    
        return <div> count is { cont } now! </div>
    }
    

    但是这样是很有局限性的,因为这个 setInterval 内部是无法获取到除了 cont 以外的值的。这个时候如果有第二个、第三个需要的 state,那就没法了。

  • 第二种方法:使用 useReducer

    这里我们使用 useReduceruseEffect 以及 state 更新的部分逻辑分离开。useReducer 内部的函数是可以读取到 react 目前内部记录的状态内容的。上面的代码进行这样的修改就可以:

    import {useState, useEffect, useReducer}, React from 'react';
    function TestComp({ step }) {
        // reducer
        function reducer = (state, action)=>{
            if(action.type === 'increment'){
                return state + step;
            }
            else {
                return state + 0;
            }
        }
        const [cont, dispatch] = useReducer(reducer, 0);
    
        // 初次渲染就开启定时器,每秒自增1
        useEffect(()=>{
            const id = setInterval(()=>{
                console.debug('cont', cont);
                dispatch('increment');
            }, 1000);
    
            return ()=>{
                clearInterval(id);
            }
        }, [dispatch]);
    
        return <div> count is { cont } now! </div>
    }
    

2. useEffect:是否应该让函数成为 useEffect 的依赖?

当时一开始接触 useEffect 的时候,相关的 lint 会提示我 useEffect 内部使用了外部函数,应该把相关函数也添加为 useEffect 的依赖。为什么要把函数也作为 useEffect 的相关依赖呢?

实际上,如果函数使用了外部的 state 或者 props 参数,如果没有添加作为 useEffect 的相关依赖,就会出现上面的没有获取到相关更新的情况。

下面几种情况可以考虑以下的写法:

  • useEffect 中调用的外部函数没有使用任何相关的 state 和 props 等内部数据:

    这个时候可以将函数写在组件外部,在组件内调用。这个时候这个函数不使用任何的 state 和 props,也不涉及任何组件内的数据流。比如下面的这个情况:

    import React, {useEffect} from 'react';
    
    // 定义在外部
    function getQuery(q) {
        return `https://xx.xxxxxx.com?search_id=${q}`;
    }
    
    function TestComp(props) {
        ...
        
        // 使用 getQuery 函数
        useEffect(()=>{
            const url = getQuery('12345');
            ...
        }, []);
        
        ...
    }
    
  • useEffect 函数中调用的外部函数,只在这个 effect 中使用:

    可以在 useEffect 内部定义这个函数,并将要使用的相关 props 以及 state 作为依赖项传递给 useEffect,这样可以获取到最新更新的相关 state 和 props 值。比如下面这样:

    import React, {useEffect} from 'react';
    
    function TestComp({ url }) {
        ...
        
        // 使用 getQuery 函数
        useEffect(()=>{
            // 定义在内部
            function getQuery(q) {
                ...
                return url + q;
            }
            
            const url = getQuery('12345');
            ...
        }, [url]); // 使用 props 作为 依赖项
        
        ...
    }
    
  • 如果觉得使用函数作为依赖项会导致多余的渲染,可以配合 useCallback hook 来使用(上一节说到过嘿嘿)