随着业务需求越来越复杂,代码的复杂度也随之上升,在近期接手的一个公司项目中,发现大部分代码我连仅仅是看懂都很吃力,主要原因是因为代码中用到了好几个我没接触过的hook,要知道对于同一个组件来讲,不同的需求要使用的hook都是不一样的,如果连组件为什么要使用这个hook都不知道,那么就更不用说去开发什么功能了,顶多写几个bug,所以我也下定决心,这次要把常用的一些勾子都给学会
useState
这个hook应该是用的最多的,基本每个组件里面都能看到useState
的影子,常见的用法就如下所示
每一次点击按钮都会重新给amount
设置新的值,接着组件就会带着这个新的值重新渲染,渲染完成后按钮上显示的也是最新的值
上面我们是通过按钮点击改变一次amount
的更新,并且触发了渲染,如果我们在按钮点击后多设置几次amount
的值,那么按钮上的值会变成多次设置后的最新值吗,我们改下代码试试
代码中,我们希望点击后按钮上的值可以增加3,实际效果如下所示
很遗憾的发现按钮上的值仍旧是每次增加1,并没有达到我们预期的效果,原因就是useState
中set
函数每次设置完值后,最新的值是要通过渲染之后才可以拿到,所以我们如果直接在调用set
函数之后去获取值,仍旧是更新前的值,如果想要达到我们要的效果,我们就要使用另一种方式去设置值,也就是将set
函数的入参从最终状态值改为一个更新函数,代码如下
用这种方式有什么区别呢?用这种方式useState
就会将函数放在一个队列中,到了下一次渲染的时候,队列中的函数就会依次执行,直到队列中没有函数了,那么计算出来的最新值就会渲染到组件上,我们看下
上面举的例子都是更新单个值,但如果我们遇到useState
里面存储的是一个对象,然后想要更改这个对象里面某个元素或者某几个元素的值,我们通常会这么做
这里是新建了一个对象,然后使用set
函数来将旧的对象覆盖掉,这种做法没啥问题,不过我们还有更简单的做法
直接使用...
运算符,然后将需要更改的元素和值写在后面就好了,这种做法不但使代码更加简洁,同时也提升了代码的可读性,同样的做法也适用在给一个数组添加新元素的场景中,比如下面这段代码
同样也是使用...
运算符,将需要插入数组的新元素写在后面,就完成了给数组添加新元素的操作,比起将数组拿出来,push
一遍,然后再set
一下新的数组的这种方式,简便很多
useReducer
在上面的useState
中我们举了一个计数器的例子来说明useState
可以存储状态,但这里的状态还略微简单一些,如果遇到一些复杂的状态,涉及到一定计算和逻辑处理的,虽然也可以用useState
来实现,但不可避免的需要添加一些与业务无关的代码在组件中,破坏了组件的职能单一性,所以为了解决这个问题,我们可以使用useReducer
这个勾子,这个勾子可以理解为复杂一些的useState
的,它的使用方式如下所示
使用useReducer
的时候,接收三个参数,分别代表的意义如下
reducer
:一个用来更新状态的函数,这个函数的入参有俩,一个是当前状态state
,另一个是行为入参数action
,action
是决定state
如何更新的关键initialArg
:状态的初始值,用来作为useReducer
初次加载时候的初始状态init
:初始值的初始化函数,它的入参为initialArg
,这个参数为可选,如果声明了这个参数,通过init(initialArg)
计算出来的值才是useReducer
真正的初始值
useReducer
同样也返回一个数组,数组里面state
作为当前值,dispatch
用来触发reducer
函数更新,这部分跟useState
里面[value,setValue]
很像
下面来写个例子来感受下,还是一个计数器,不过这个计数器我们希望既可以加,也可以减,加减的大小也可以通过入参决定,那么这部分逻辑我们就可以写在reducer
函数里面,看代码
我们将reducer
函数写在组件外层,命名为sum
,组件内部在useReducer
内传入sum
函数以及初始值,另外两个按钮分别用来触发加3以及减2的操作,按钮边上显示当前state
的具体值,这样的做法既可以让state
的状态更新与组件分离,也可以避免在组件重新渲染时候,重复创建sum
函数,效果如下
上面的代码中每一次组件重新挂载,计数器都会从0开始,现在希望组件在挂载后,初始值也可以通过计算得出,毕竟在有的场景中,我们参与计算的初始值可能会从缓存或者数据库中取出来,那么这个时候就要用到useReducer
的第三个参数init
函数,用法也很简单,我们再创建个函数,入参就是initialArg
,这样组件在第一次挂载后初始值就会通过init
函数计算获得,新增init
函数如下
初始化的操作很简单,就是将initialArg
加上一个1到4的随机值作为初始值,然后将这个函数作useReducer
的第三个参数,这样就实现了组件的初始值通过init
函数计算获得
可以看到刚开始我刷新了几次页面,组件上的初始值也发生了变化,这个就是useReducer
的init
函数发挥的作用
useContext
经常会遇到这样的场景,组件之间的数据需要来回通信,比如父组件获取到数据id,然后需要把数据id传递到子组件中获取详情,又或者在子组件中拿到数据后,需要将数据回显至父组件展示,一般像这样的情况,我们会使用props
进行传递,但是用props
的话也是存在一些不足的,比如
props
参数过多容易导致组件业务耦合度过高- 对于层级比较深的组件,
props
会提高代码的维护成本以及降低代码可读性
针对这些情况,我们可以使用useContext
来解决,这个勾子可以允许我们可以从组件读取或者订阅上下文信息,通俗的说就是共享数据,要做到这一点首先需要去创建上下文,我们可以使用下面这段代码来创建
createContext
创建了一个上下文,并且这个上下文有一个默认值abc
,这段代码提供了两个信息,一个是上下文提供的数据类型是string
类型的,另一个是上下文如果没有提供值,那么它内部组件获取到的上下文的数据就是abc
,现在我们看下如何使用上下文
首先使用了CoffeeContext.Provider
创建了一个上下文环境,并且使用value
属性给上下文设置了一个值def
,然后在CoffeeContext.Provider
里面就是我们的组件了,这里的组件为FirstLevel
,在这个组件里面,我们使用useContext
获得了之前在CoffeeContext.Provider
里面设置的值,并且通过点击按钮将这个值显示在页面上,我们看下效果
可以看到我们并没有给组件FirstLevel
传值,但是在FirstLevel
里面已经可以获取到在父组件里面设置的值了,这就是useContext
的基础用法,现在我们来更改一下例子,刚才我们使用的都是静态值,下面我们让上下文的数据可以动态设置,在上面的例子中加点代码
在父组件中通过按钮执行了函数requestCode
,这个函数里面延迟执行了给上下文的数据设置了一个新的值,而在子组件里面,我们取消了刚才的按钮,直接改为显示当前上下文的数据,我们看下现在的效果会是怎么样
可以看到我们改变上下文的数据之后,在子组件里面就算没有按钮去触发更新,它自己就直接将最新的上下文数据渲染出来了,从这里就能看出所有在上下文里面的组件其实都订阅了我们上下文的数据,只要上下文的数据改变了,所有子组件都会得到通知重新渲染,所以这里也是给我们提个醒,上下文数据虽好用,但不能滥用,否则会造成不必要的组件渲染以及性能开销,言归正传,刚才我们举的例子都是子组件里面显示父组件改变的上下文数据,那么如果想在子组件改变上下文数据,并在父组件显示,怎么做呢?我们就需要更改一下上下文的数据类型,看代码
此时上下文提供的数据已经不仅仅是个string
类型了,而是一个string
和(string)=>void
组成的object
,就像是把整个useState
都共享了出去
由于我们的共享数据已经不仅仅是一个string
类型了,所以useContext
得到的ctx
其实是一个object
,如果想要改变共享数据的code
值,必须调用ctx
里面的setCode
函数,效果如下
useRef
我们已经知道useState
可以存储状态,并且触发组件渲染将最新状态拿出来使用,但是如果我们仅仅只是想将某些状态或者数据存储起来,不想渲染组件,那么useState
显然就不是一个最佳方案了,这个时候我们就要使用useRef
,这个勾子可以让我们存储一个值的引用并且不会触发组件渲染,比如我们想要操作页面上的一个dom元素,可以这样写
这里有个输入框和一个按钮,现在想要点击按钮之后让输入框获取焦点,我们就可以先用useRef
获取输入框的引用,然后inputref.current
就代表我们的输入框了,在函数inputFunc
里面就是一个输入框获取焦点的操作,效果如下
刚才我们按钮与输入框在同一个组件内,如果当需要操作的引用在不同的组件内,我们就需要将引用作为参数传到子组件内,看下面这段代码
我们将之前例子中的输入框挪到了子组件ChildInput
里面去,这个时候父组件中的inputref
就需要作为参数传到ChildInput
内,同时ChildInput
还要包在forwardRef
中,这么做的作用就是让父组件可以获得子组件的元素,这段代码的执行效果与第一个例子是一致的,就不演示了,接着看下一个例子,开头我们说了,如果想要存储某些状态,但又不希望渲染组件,我们可以用useRef
这个勾子,现在来写个例子来证明这一点
组件内有个计数器以一秒一次的频率在计数,并且我们将计数的值存在了timeRef
中,timeRef
是用useRef
创建出来的引用,组件内还有一个按钮,当按钮点击后会将当前计数的值展示在页面上,为了证明状态存储在timeRef
后不会触发组件重新渲染,我们还将按钮的背景色用一个随机值来表示,这样当组件重新渲染后,按钮的颜色就会不一样,这段代码的演示效果如下
从效果中也证实了就如我们预期的那样,当timeRef
的值不断被更新的时候,组件是不会被渲染的,只有重新调用了一下setTimeForNow
函数,组件才会被渲染
useEffect
老实说所有勾子里面我第一个会用的并不是useState
,而是useEffect
,因为它的用法跟Compose里面的LaunchedEffect
基本是一样的,我们什么时候需要用到useEffect
呢,当我们的一个组件需要执行一些与组件渲染无关的动作的时候,比如网络请求,将数据渲染到组件上等,我们把这些动作叫做组件的副作用,useEffect
内部有俩参数,一个setup
函数和一个依赖数组
setup
函数:组件副作用真正要执行的操作,它将会在组件渲染完成后或者依赖项发生改变后进行- 依赖项:这个是个可选参数,它是个数组,数组内部的元素只要有其中一个发生变化,就会触发副作用再一次执行它的
setup
函数
下面来写个例子,分别看看useEffect
在有依赖项和没有依赖项的时候的区别
这个例子中分别有三个useEffect
,并且分别没有依赖项目,有一个可变的依赖项和一个不可变的依赖项,三个useEffect
的setup
函数中也用message
的形式给出了对应的提示,有两个按钮,一个按钮是用来更新变量amount
,另一个更新变量name
,现在看下当我们分别刷新组件,点击左边按钮和点击右边按钮时,究竟哪些useEffect
被执行了
我们看到依赖项为不可变的useEffect
只在组件挂载的时候执行了一下,没有任何依赖项的useEffect
在每次组件渲染的时候都会执行,而有依赖项的useEffect
只在组件挂载以及依赖项发生改变的时候进行,好了现在我们知道了useEffect
的执行时机,那么现在来想个问题,如果useEffect
中执行了一个比较耗时的任务,当组件从页面上消失的时候,这个任务还会继续执行吗?来看下面这段代码
有一个按钮,在点击的时候将Timer
组件展示出来,再点一次按钮则卸载Timer
组件,Timer
组件在挂载的时候就会执行一个计时器,这是个简单的计时器,只是在控制台打印出具体执行了多少秒,现在来看下Timer
从挂载到卸载计时器的运行状态
如效果图所示,当Timer
被卸载的时候,内部的计时器却是依然还在进行,这显然是不合理的,所以useEffect
也提供了一个cleanup
函数来处理当组件被卸载时,释放内部资源,我们在代码中增加cleanup
函数
增加cleanup
函数很简单,在useEffect
函数末尾return
一个函数,函数内部执行释放资源的操作就好了,现在再来看下组件卸载时候计时器是否还在运行
增加了cleanup
函数后,useEffect
内部的计时器也停止了
useLayoutEffect
这个勾子相对用的比较少一点,但是我们也要清楚它与useEffect
的区别,以免在错误的时机去使用它引起不必要的问题,useLayoutEffect
与useEffect
在使用上完全一样,内部都是接收一个setup
函数和一个依赖数组,而不一样的地方则是
- 从触发时机上,
useEffect
会在浏览器绘制屏幕之后执行,而useLayoutEffect
是在浏览器绘制屏幕之前执行 - 从使用场景上,
useEffect
是当你需要处理一些与组件无关的操作比如请求接口,访问数据库,而useLayoutEffect
则是当你需要获取DOM信息或者改变DOM的时候去使用
口说无凭,下面用一个例子来加深一下印象
页面上有一个色块,初始状态给它设置个绿色,然后在useEffect
中阻塞一段时间后给色块设置成红色,效果如下
我这里做了刷新操作,我们看到组件刷新后有一个从绿到红的闪烁过程,这也是因为useEffect
是在页面渲染完成后进行的,所以色块才会先展示默认色,然后再变成红色,现在再把useEffect
换成useLayoutEffect
再看看
可以从顶部的刷新图标看到,组件是已经刷新过了,但是我们的色块却没有像上面那样从绿色变成红色,而是一直展示为红色,这也证明了useLayoutEffect
是在浏览器绘制前执行的,在绘制的时候,色块已经用的是更新完的色值,但是后面我们看到反复刷新后,页面有个明显的白屏效果,这里也说明了useLayoutEffect
会阻塞浏览器渲染屏幕,用多了是会影响页面的体验效果的,官方文档也在介绍useLayoutEffect
的时候,第一句就指出了这个勾子会影响性能,尽可能用useEffect
总结
本篇文章介绍的六个勾子算是在日常开发中比较常用的,不过就算是常用也会出现像我这样的新手在某些情况下乱用,不会用这些勾子,不过相信经过这篇文章的学习,在今后的开发过程中对这些勾子会有个新的认识,下一篇文章会介绍另外几个勾子,再见~