上次简单说明了一下为什么会产生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:这里我们使用
useReducer将useEffect以及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 来使用(上一节说到过嘿嘿)