在上一篇文章中,已经相继介绍了几个React里面常用的hook,这篇文章中,我们继续介绍剩下的几个hook以及它们的特性与用法。
useMemo
这个勾子从名字上就能看出它是与缓存有关的,那么究竟是缓存的什么?什么时候去缓存?我们又该在什么时候去用这个勾子呢?我们一个个来讲,首先看下useMemo
的使用方式
useMemo
接收两个参数,其中calculateValue
是一个函数,这个函数是带有返回值的,组件首次加载的时候,会去执行calculateValue
函数,并且将执行结果赋给cachedValue
,而等到组件下一次加载的时候,useMemo
直接会去取上一次calculateValue
执行的结果给到cachedValue
,而不会再去执行一遍这个函数,既然这样,那是不是除了首次,cachedValue
一直取得都是缓存值呢,这里就要说到第二个参数dependencies
了,它是一个依赖数组,这个数组里面的所有元素都是calculateValue
的依赖项,组件在渲染的时候,只要发现依赖元素里面存在某一个或几个值上一次渲染时候不同,那么就会触发calculateValue
重新执行,重新计算出新的cachedValue
。
啰哩啰嗦讲了一堆不知道有没有说明白,我们还是用一个实际例子来演示下,这个例子就是来做一个简易的通过工号搜索员工的功能,首先页面上有个Form
表单,表单里面有两个输入框,分别输入最小工号以及最大工号,最后我们加载出在这个工号区间范围内的员工,代码如下
现在是数据部分,首先是创建总的员工数据,由于一般情况下员工总数不会变的,而且查询员工数据计算量比较庞大,所以这部分我们就需要用到useMemo
,只在初次加载页面的时候加载员工数据,其余情况下我们取缓存数据,只有当员工总数发生变化的时候,我们才需要重新计算员工数据,所以依赖项我们就定为员工总数,代码如下
为了展示效果,我这里的员工数目小一点,默认为20个,然后在useMemo内部的calculateValue
函数中加上一条打印日志,来观察什么时候useMemo
重新执行了calculateValue
函数,我们再增加一个按钮用来改变员工总数,用来验证改变依赖项会不会让useMemo
重新执行calculateValue
函数
然后我们还需要计算在最小最大工号区间内的员工,这部分我们同样放在useMemo
里面,依赖项分别是Form
表单中两个Input
的值以及员工总数totalCount
现在这个根据工号区间搜索员工的功能就做好了,我们看下效果以及控制台日志打印情况
可以看到除了在第一次加载页面以及改变总数大小的时候,计算总数的useMemo
才会去执行一遍内部计算操作,其他情况都是拿的缓存数据,另外,我们还注意到在更改员工最大最小工号区间,以及在更改员工总数大小的时候,都会触发获取filterList
的useMemo
去执行一遍内部计算函数,看起来貌似这里的useMemo
并没有存在的价值,其实主要是我们改变的都是过滤员工总数的useMemo
的依赖项,才会让这个useMemo
每次都重新去执行一遍内部函数,但是要知道实际项目中触发组件渲染的条件有很多,如果我们不用useMemo
,必然会让一些渲染导致某些函数经历没有必要的重新创建,这些都是会影响性能的,我们可以来试一下,在目前的页面中加入一个按钮,让它来触发页面重新渲染,我们观察下useMemo
还会不会执行内部函数
新增的按钮功能是改变自己的背景颜色,它的色值存储在useState
里面,当设置一个不同的色值的时候,页面就会重新渲染,我们看下效果
可以看到页面在重新渲染的时候,两个useMemo
都没有重新执行内部函数,也就是渲染过程中都是取的缓存值,好了,目前来讲数据部分我们算是能缓存的都缓存起来了,但是组件部分呢,我们看到当改变按钮颜色导致页面重新渲染的时候,我们的Form
表单以及员工List
也发生了没有必要的渲染,应该是当filterList
有改变的时候再去渲染员工List
才是合理的,所以还要进一步优化,将Form
表单以及员工List
也缓存起来,方法就是将它们提取出来作为一个新的组件,然后用memo
包起来,被memo
包起来的组件只有当props
发生改变的时候,才会重新渲染,提取出来的组件代码如下所示
在父组件内,代码更改为
可以看到TagList
的入参之一就是filterList
,只有当过滤出来的员工数据发生改变才会重新渲染TagList
,我们还在TagList
中加入了一条打印日志,可以观察到TagList
什么时候发生了渲染,看下效果
可以看到效果跟预期的一样,TagList
只在入参filterList
发生改变的时候重新渲染,而其他情况下,就算父组件重新渲染了,TagList
展示的是缓存内容,并没有重新渲染
useCallback
不得不说useCallback
和useMemo
基本属于同一种类型的hook
,有的人更是把useCallback
作为专门缓存函数的useMemo
,所以这俩勾子在使用以及思想上是可以相提并论的,那么我们什么时候需要用到useCallback
呢?试想一下我们经常需要在子组件中回调一些值到父组件,这个时候我们通常会在父组件里面创建一个函数,然后作为子组件的props
传进去,子组件的任何操作都会通过props
里面的函数回调给父组件的函数,然后在父组件的函数里面更新一些值,触发一些组件渲染,我们将这个过程用代码写下来
很正常的操作,我也经常这么写,不过现在才知道这样做的话这里的子组件OperateHouse
就已经陷入了无止境的无意义的渲染中,因为我们父组件的函数每次渲染都会创建一个新的onValueSent
实例,然后OperateHouse
的props
得到这个新的实例后就会触发子组件的渲染,尽管对于子组件来讲前后两次渲染都跟自己没关系,我们看下执行结果
我们看到每次点击更新时间按钮的时候,组件OperateHouse
里面的日志都会被打印出来,说明它也渲染了,因为它的props
里面的函数每次都是一个新的实例,那么优化的方向就是别让onValueSent
函数每次都新建实例,这个时候就用到useCallback
了,它保证除了第一次组件渲染,其余的时候函数都是拿的缓存,更改后的函数如下所示
由于我们的子组件是用memo
函数包起来的,所以当onValueSent
函数不再被创建而是从缓存里面取出的时候,props
每次传入的值都是相同的,那么这样子组件就不会被渲染了,我么看下现在的效果
可以看到子组件OperateHouse
除了在初次页面加载时候渲染了一次,后面无论父组件怎么渲染,都不会影响到子组件
注:无论是useMemo还是useCallback,虽然好用但不能滥用,如果只顾着一味的追求缓存数据,在组件中大量使用这俩hook,那么势必会造成页面首次加载时候的负担,具体该不该用,哪里该使用,哪里可用可不用,还是需要一定的分析与测试再做结论
useTransition
从React18开始,React就默认开启了同步模式,能够确保页面上的元素或者状态更新完后再执行下一个,但是难免会有些场景涉及到比较复杂的计算或者耗时的操作,影响到其他元素正常渲染,也就是阻塞UI,这个时候页面就会出现卡顿现象,正因为如此,React18推出了两个hook来处理这个问题,useTransition
和useDeferredValue
,我们先来说前者,useTransition
的使用方式如下
可以看到useTransition
与其他勾子有点不一样,不接受任何参数,返回一个数组,里面有两个值,startTransition
可以设置某些状态更新为过渡状态,设置为过渡状态的更新操作,它的优先级就会降低,会让优先级比较高的操作先执行,比如输入框内的输入操作,tab切换等,isPending
是个布尔值,它表示设置成过渡状态的操作是否在执行,我们可以利用这个值来显示一些loading效果,或者实现防抖节流操作,下面来看下如何在实际场景中使用useTransition
这里有个输入框,会将每次输入的内容拿出来渲染出20000条数据,由于这边的渲染操作涉及的数据量比较大,很可能会带来操作上的卡顿,我们看下实际效果
在我不断输入字母的时候,我们看到输入框内的内容并不是连续的显示的,卡顿现象很严重,主要原因就是底下列表渲染太耗时了,所以要优化这一点的话,只需要使用startTransition
将列表设置为过渡状态,让它的优先级降低,这样就能解决这个卡顿问题
使用了useTransition
后,我们在看下现在输入还卡不卡
稍微还有一点点卡,不过比之前的好多了,然后我们再看下如何使用isPending
,刚才说了这个值表示过渡状态正在执行,所以我们可以在isPending
为true的时候,给页面增加一个正在加载的文案,等到过渡状态结束,再把列表展示出来,代码如下
没几句代码,就实现了给页面增加了一个正在加载的loading效果,所以以后在开发过程中如果遇到了ui阻塞或者需要添加loading效果的时候,可以尝试下useTransition
这个hook
useDeferredValue
讲完了useTransition
我们在说说useDeferredValue
,上面说了这两个hook都是用来解决同步更新时候ui阻塞问题,它俩的区别也很明显
useTransition
:延迟更新的是一个状态的过渡,会有一个过渡的标识useDeferredValue
:延迟更新的是一个值,没有任何过渡的标识
useDeferredValue
的使用方式如下所示
当组件第一次初始化的时候,deferredValue
与value
是一样的,但是在接下来的渲染中,deferredValue
会使用前一次渲染的值,而当前最新值会在后台进行渲染,这个渲染的过程是可以被中断的,也就是如果有新的值过来了,正在后台进行的渲染操作会重新开始,直到没有了更新的值,才会把当前最新值给到deferredValue
,相当于就是做了一个延迟更新,而有更高优先级的任务则会优先执行,我们依然用上面的例子,改一下就是如下所示
在useTransition
例子中我们是降低了列表渲染的优先级来防止阻塞输入框的输入,而这里是降低了输入内容更新的优先级,在不停输入内容也就是value
值一直在更新的时候,defered
的值是不变的,从而也不会触发列表重新渲染,直到value
值停止更新了,后台的渲染工作完成了,defered
才会拿到最新的值并触发列表重新渲染,整个过程效果如下
可以看到,还是比较明显的,输入框在输入内容的时候,底下列表没有更新数据,而输入框一旦停止更新内容,底下列表数据就改变了,为了形成对比,下面再看看不用useDeferredValue
的时候,效果如何
可以看到卡顿现象还是很明显的,所以当遇到ui更新发生阻塞的时候,同时也不关心过渡状态,那么useDeferredValue
会是一个比较好的解决这种问题的方案
useImperativeHandle
头一次看见这个勾子还是有点懵圈的,完全没看懂啥意思,后来翻了下文档后大致也清楚了,总体来说就是一句话,子组件通过一个ref,自定义自己想要暴露什么给父组件,useImperativeHandle
的使用方式如下所示
这个hook接收三个参数,它们分别代表
ref
:forwardRef
里面接收的第二个参数,也就是从外界传进来的createHandle
:这是个函数,函数内部可以定义具体需要暴露的内容dependencies
:可选项,一个依赖项的数组,如果定义了这个参数,当依赖项改变的时候,createHandle
将会重新创建
现在我们来尝试几个例子来看看useImperativeHandle
如何使用,有这么个场景,子组件里面有个Form
表单,表单项是两个输入框,而父组件里面有俩按钮,点击按钮分别需要让两个输入框获取焦点,但是又不想让父组件直接操作子组件的Dom元素,该怎么做呢?首先来创建子组件
子组件内分别为两个输入框创建了引用nameRef
和jobRef
,如果不想让这两个引用对外暴露的话只能在组件内部定义两个获取焦点的函数,然后将函数暴露出去,这两个函数就需要定义在useImperativeHandle
的createHandle
里面,代码如下
现在当外界需要获取这两个输入框的焦点的时候,只需要调用nameFocus
跟jobFocus
这两个函数就可以了,我们看父组件的代码
formRef
就是传入到子组件的ref
,我们看到只需要在formRef
上分别调用nameFocus
和jobFocus
这两个函数,就可以让子组件的两个输入框获取焦点了,看下效果
就这样我们就实现了不直接访问子组件Dom元素,也可以实现获取焦点的效果,还是蛮容易的,现在在这个基础上再加点功能,一般有Form
表单的场景,我们想要获取表单内的数据的时候,肯定要先校验一下,校验通过了再拿数据,而这些操作我们不能在父组件内直接执行,除非将子组件内的Form实例挪到父组件内,但是这样就破坏了子组件本身的功能,那么我们就可以利用useImperativeHandle
将校验以及获取数据的操作包在一个函数内暴露出去,外界只需要调用这个函数即可,现在我们在上面的createHandle
中新增一个这样的函数
新增了一个yanzheng
函数,函数名随意取的,大家能看懂就好,函数内部就是我们熟悉的Form
表单的验证过程,现在在父组件内新增一个按钮用来调用这个函数,代码如下
为了方便实际结果就以message
的方式展示了,现在看下我们要的验证与获取数据的功能有没有实现
从展示效果中可以看到,无论是校验还是获取数据都可以正常执行,而这种方式除了这里举的表单例子之外,还可以用在比如组件内有请求接口,刷新数据等场景,无需通过props
的方式,直接调用暴露出来的函数就好了。
useSyncExternalStore
组件之间共享数据以及数据通信是我们一直要面对的问题,至今也有很多种方式来处理这些问题,比如props
传值,使用useContext
来共享数据,或者使用useImperativeHandle
在组件中暴露出一个获取或者处理数据的函数来让其他组件来调用,但是这些有个通病,都增加了代码的耦合,或者在组件中添加了太多与组件无关的操作,比如数据处理,那么有没有一种方案可以让ui视图与数据处理完全分离,通过一种方式将两者建立一个通道来进行数据通信呢?这个时候useSyncExternalStore
就可以来了解下
useSyncExternalStore
的使用方式就如上述代码所示,返回值snapshot
就是当前需要使用的数据快照,它的参数分别代表的意义是
subscribe
:一个订阅函数,它内部提供了一个callback
的回调,使用这个订阅函数的组件都表示对数据源store
发起了订阅,只要数据源里面的数据发生变化,那么都会调用callback
并触发组件重新渲染,注意的是subscribe里面也必须提供一个cleanup
函数来取消订阅getSnapshot
:获取数据源里面的数据,如果获得的数据同前一次的值不同,那么就会触发组件渲染getServerSnapshot
:可选参数,同样是获取数据快照,不同的是这个数据快照是只用在服务端渲染时
现在用一个例子来演示下如何使用这个hook,既然要订阅数据源的数据,那么首先就需要创建一个这样的数据源
articleStore
就是我们的数据源,并且提供了一个订阅函数subscribe
,下面再来定义数据模型
这里定义了一个文章的数据模型,里面有标题,作者,创建时间属性,数据模型有了之后,就可以在数据源里面增加获取数据快照的函数
上面说到了当数据快照发生改变的时候,就会调用callback
函数来让所有发起订阅的组件重新渲染,所以再增加一个函数用来批量调用所有callback
函数
现在再来想一下我们需要实现什么功能,有两个文章列表的组件分别用来展示当前最新的文章数据,有一个添加按钮用来添加文章数据,我们点击某一条文章时候,这条数据就会被删除,上述功能里面,对数据源的操作有增加和删除,所以我们需要在数据源里面增加相关的实现函数,代码如下
数据源的功能都完成了,组件部分的代码就简单了,我们这里将展示数据和添加数据都各自放在不同的组件内,将它们绑定在一起的就是我们的数据源articleStore
,代码如下
ListPage
使用了useSyncExternalStore
对数据源发起了订阅,这样无论是在哪里操作了数据源,发起订阅的组件都可以实时响应数据,看下效果
总结
用了两篇文章将React18中的12个hook的使用与特性逐一介绍了一下,另外三个hookuseDebugValue
,useId
,useInsertionEffect
没有讲的原因主要是个人还没遇到相关使用场景所以暂时先不写,其实在实际项目中我们用到的勾子不仅仅局限在官方提供的这十几个,还有很多勾子来自一些三方库,或者自己团队封装的库里面,但是这些自定义的勾子的实现方式也基本上用的都是react库里面的,所以只要了解了目前react提供的那十几个勾子,基础打好了,无论是看其他人写的勾子还是自己去自定义勾子,也都不会变的很难。