购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.3 React Hooks

React Hooks在React 16.8时正式发布,它使函数组件能拥有自己的状态,对类组件没有影响。实战部分将大量使用函数组件,主要原因是类组件存在如下3个问题。

❑ 必须时常关注this关键字的指向,对初学者而言这不是一件容易的事。

❑ 相同的生命周期在类组件中最多定义一个,这导致彼此无关的逻辑代码被糅杂在同一个函数中。

❑ 不同的生命周期函数可能包含相同的代码,最常见的便是componentDidMount和componentDidUpdate。

React提供了很多内置的Hooks,每个Hooks有各自的用处,本节只介绍实战部分常用的Hooks,并列举一些自定义Hooks。

1.3.1 useState

useState是一个与状态管理相关的Hooks,能让函数组件拥有状态,是最常用的Hooks之一,类型定义如下。

从类型定义可以看出,useState有两个重载,分别是传参数和不传参数。不论是否传参数,useState都返回一个长度为2的数组。数组的第一个位置是状态,可以是任何数据类型,类型参数S用于注释它的类型;第二个位置是一个用于更新状态的函数,为了方便介绍本小节将该函数记为setState。

接下来介绍useState的基本用法。

1.useState的参数不是函数

此时,useState的参数将作为状态的初始值,如果没有传参数,那么状态的初始值为undefined。用法如下。

UseStateWithoutFunc组件有name和age这两个状态,name只能是string类型,初始值为'何遇',age的数据类型是number或undefined,初始值为undefined。

2.useState的参数是函数

此时,函数的返回值是状态的初始值。某些时候,状态的初始值要经过计算才能得到,此时推荐将函数作为useState的参数,该函数只在组件初始渲染时执行一次。用法如下。

上述useState的参数是函数,该函数的返回值是count的初始值。

3.修改状态的值

沿用上述代码中的setCount,修改状态有两种方式,具体如下。

如果setCount的参数是函数,那么count现在的值将以参数的形式传给该函数,函数的返回值用于更新状态。如果setCount的参数不是函数,那么该参数将用于更新状态。状态值发生变更将导致组件重新渲染,重新渲染时,useState返回的第一个值始终是状态最新的值,不会重置为初始值。

目前已经介绍完useState的基本用法,观察代码清单1-1所示代码,分析浏览器打印的结果。

代码清单1-1

如果你理解1.1节中讲的知识,那么一定能轻而易举地分析出浏览器打印的值是0而不是1,这是因为在函数组件中取state和props拿到的都是本次渲染的值,在本次渲染范围内,props和state始终不变。

在代码清单1-1中调用setCount会导致组件重新渲染,在下一次渲染时count的值为1,但console.log(count)打印的是本次渲染时count的值,所以结果为0。

1.3.2 useRef

使用useState能让函数组件拥有状态,状态拥有不变性,它在组件前后两次渲染中相互独立。使用useRef能为组件创建一个可变的数据,该数据在组件的所有渲染中保持唯一的引用,所以对它取值始终会得到最新的值。下面是useRef的用法,分析浏览器打印的结果。

单击按钮,在浏览器控制台上count的打印结果为0,ref.current的打印结果为1。由此可以知道,在setTimeOut回调函数中拿到了ref.current最新的值。

1.3.3 useEffect

useEffect是除useState之外另一个常用的Hooks,理解它比理解useState的难度更大,但只要明白函数组件每次渲染都有它自己的状态和props,那么理解useEffect将变得容易。

useEffect能让开发人员知道DOM什么时候被绘制到了屏幕上,组件什么时候被卸载了。有些开发人员认为useEffect是类组件componentDidMount、componentDidUpdate和componentWillUnmount的生命周期函数的结合,但实际上函数组件没有与类组件类似的生命周期概念。useEffect类型定义如下。

从类型定义可以看出,useEffect最多可接收两个参数。第一个参数是函数,可以有返回值,本小节将该函数称为effect;第二个参数是非必填的,是一个数组,它是effect的依赖,称为deps。deps用于确定effect在本次渲染中是否应该执行,若应该执行,则在浏览器中将DOM绘制到屏幕之后执行,可以将Ajax请求、访问DOM等操作放在effect中,它不会阻塞浏览器绘制。

函数组件可以多次使用useEffect,每使用一次就定义一个effect,这些effect的执行顺序与它们被定义的顺序一致,建议将不同职责的代码放在不同的effect中。接下来从effect的清理工作和依赖这两个方面介绍useEffect。

1.effect的清理工作

effect没有清理工作就意味着它没有返回值,相关代码如下。

上述代码定义了一个effect,它的作用是将document.title设置成本次渲染时name的值。

effect的清理工作由effect返回的函数完成,该函数在组件重新渲染后和组件卸载时调用。代码清单1-2定义了一个有清理工作的effect。

代码清单1-2

上述effect在DOM被绘制到界面之后给body元素绑定click事件,组件重新渲染之后将上一次effect绑定的click事件解绑。该effect在组件首次渲染和之后的每次重新渲染时都会执行,如果组件的状态更新频繁,那么组件重新渲染也会很频繁,这将导致body频繁绑定click事件又解绑click事件。是否有办法使组件只在首次渲染时给body绑定事件呢?当然有,那就是依赖。

2.effect的依赖

前面两个示例定义的effect没有指明依赖,因此组件的每一轮渲染都会执行它们。修改代码清单1-2,让组件只在首次渲染时给body绑定事件,实现代码如下。

给useEffect的第二个参数传空数据意味着effect没有依赖,该effect只在组件初始渲染时执行,它的清理工作在组件卸载时执行。对于绑定DOM事件而言这是一件好事,它可以防止事件反复绑定和解绑,但问题是,如果在事件处理程序中访问组件的状态和props,那么只能拿到它们的初始值,拿不到最新的值。是否有办法让effect始终拿到状态和props最新的值呢?有。

给effect传递依赖项,React会将本次渲染时依赖项的值与上一次渲染时依赖项的值进行浅对比,如果它们当中的一个有变化,那么该effect会被执行,否则不会执行。为了让effect拿到它所需状态和props的最新值,effect中所有要访问的外部变量都应该作为依赖项放在useEffect的第二个参数中。相关代码如下。

上述effect在组件初始渲染时会执行,当name发生变化导致组件重新渲染时也会执行,相应地,组件卸载时和由name的变化导致组件重新渲染之后将清理上一个effect。

注意

函数组件每次渲染时,effect都是一个不同的函数,在函数组件内的每一个位置(包括事件处理函数、effects、定时器等)只能拿到定义它们的那次渲染的状态和props。

1.3.4 useReducer

useReducer是除useState之外另一个与状态管理相关的Hooks。对于熟悉Redux的工程师而言,理解useReducer会很简单。在React内部,useState由useReducer实现。useReducer的类型定义如下。

useReducer的类型定义很复杂,一共有5个重载,总体而言,它最多可接收三个参数。第一个参数是一个用于更新状态的函数,本书将它称为reducer。第三个参数非必填,如果不存在第三个参数,那么第二个参数将作为状态的初始值;如果存在第三个参数,那么它必须是函数,第二个参数被传递给该函数用于计算状态的初始值,该函数只在组件初始渲染时执行一次。useReducer的返回值是一个长度为2的数组,数组的第一个位置是状态值,第二个位置是一个用于触发状态更新的函数,该函数被记为dispatch,调用dispatch将导致reducer被调用。接下来通过计数器示例对比useState和useReducer在用法上的差异。

1.用useState实现计数器

用useState定义两个状态,分别表示计数器的值和步长。完整的代码如下。

2.用useReducer实现计数器

要用useReducer就离不开reducer,reducer是一个函数,用于更新useReducer返回的状态。相关代码如下。

计数器Demo有两个状态,分别是当前值和加减步数,这两个状态密切相关,这里用一个TS接口去描述它们,代码如下。

计数器有4种操作,分别是加、减、重置和修改步数,这里用TS接口描述这些行为,代码如下。

在组件中使用useReducer的代码如下。

只考虑代码量,读者应该会认为useState比useReducer更简洁。仔细观察可以发现,上述计数器除了有value还有step,step对value有影响。UseStateCounterDemo组件将value和step零散地保存在不同的状态中,然而UseStateCounterDemo组件将它们关联在同一个状态中,内聚性更高。useState与useReducer没有优劣之分,它们有各自适用的场景,这里有如下建议。

❑ 当状态是一个拥有很多属性的复杂对象,并且状态更新涉及复杂的逻辑时,推荐使用useReducer。

❑ 当某个状态的更新受另一个状态影响时,推荐使用useReducer将两个状态放在一起。

❑ 当状态只是单独的基本数据类型时,推荐使用useState。

在介绍useEffect时强调过,为了在effect中拿到状态最新的值,必须给effect设置正确的依赖项。在effect中使用useReducer返回的dispatch能让effect“自给自足”,减少依赖项。示例代码如下。

上述代码中useEffect的第二个参数为空数组,这意味着effect只在组件初始渲染时执行。由于React会让dispatch在组件的每次渲染中保持唯一的引用,所以dispatch不必出现在effect的依赖中,此特性与Ref类似。虽然dispatch的引用保持不变,但它能调用组件本次渲染时的reducer,在reducer中能得到最新的状态和props。

注意

可以从依赖中去除dispatch、setState和Ref,因为React会确保它们的引用保持不变。

1.3.5 自定义Hooks

如果在多个组件中使用了相同的useEffect或useState逻辑,推荐将这些相同的逻辑封装到函数中,这类函数被称为自定义Hooks。下面举3个自定义Hooks的示例。

1.usePrevVal

usePrevVal的功能是获取状态上一次的值,它利用了Ref的可变性,以及effect在DOM被绘制到屏幕上才执行的特性。

2.useVisible

useVisible的功能是检测DOM元素是否在浏览器视口内,它在effect中创建observer来异步观察目标元素是否与顶级文档视口相交。

3.useForceUpdate

useForceUpdate是返回一个让组件重新渲染的函数。 LQ+xU9vCP/VGqQdo794ql5DyGKTozIzv323TbYZgACwDXMyT+Bj8J1mxgiUOEutk

点击中间区域
呼出菜单
上一章
目录
下一章
×