诞生背景
首先我们要确定,一个新的东西的出现,必定是为了弥补旧的东西的不足,如果旧的东西已经可以完美满足人的需求,那么我们也没必要去绞尽脑汁开发一个新的东西,那么,hooks是在一种什么样的背景下诞生的呢?
react的函数式组件与类式组件有哪些问题与缺陷?
react对于函数式组件和类式组件的定义与用法
react的组件分为两大类,函数式组件与类式组件,函数式组件也称无状态组件,类式组件也称有状态组件。在初学的时候,可能两种组件用的频率都差不多,甚至函数式组件更多一些,因为写起来简单,但是在开发中,类式组件绝对是用的最多的。
函数式组件,其实还有一个别称,也叫展示型组件,顾名思义,也就是只能用来展示内部内容的一个组件。因为开发中,我们往往要用到各种各样的状态,所以函数式组件最多也就是在类式组件都写完后,将类式组件都挂载到一个函数式组件作为展示使用,但是一旦我们还需要使用这个父组件对一些状态或者钩子函数,那么也不得不把这个函数式组件改写成类式组件。
看似函数式组件和类式组件满足了我们的开发需求啊,类式组件写功能,函数式组件集中展示,那么为什么还要推出hooks给函数式组件添加状态和钩子函数呢?其根本原因在于使用类式组件时,不知不觉产生的一些副作用。
类式组件的缺陷
1.整个组件过于冗杂,如果说一个页面的一些逻辑比较复杂,一直在这个组件中去添加状态,添加方法,会导致整个组件代码数量越来越多,非常累赘且不利于阅读。
2.一些可能开发中大家已经不知不觉忽略掉的问题,如方法的this指向问题,生命周期钩子的执行顺序逻辑有些复杂,为组件添加状态的写法不够简洁等等,这些问题在hooks进行了一些优化。
那么hook针对这些缺陷做了哪些改进呢?
总的来说,hook实现了两个优化,一个是代码书写的繁琐程度,一个是让我们完成了组件的复用。
用法介绍
hook究竟做了什么?
首先,我们要对hook进行一个定位,就是hooks究竟是什么?或者说hooks做了什么?
我们从类式组件和函数式组件的差异中不难理解出,hooks如果能做到让函数式组件来替代之前类式组件的地位,那么hooks就一定会至少做这两样工作:为组件添加自己的状态,提供类似生命周期钩子的函数。
那么,先从hooks如何给函数式组件添加自己的状态说起。
最基础的函数式组件
首先看下面一段代码
这是一个非常基础的函数式组件,我们知道,函数式组件是没有状态的,那么假设我们想要利用这个函数式组件,做一个点击后实现加减效果的功能,我们只能选择这样做,在外部声明一个看似是状态的变量,那么最终实现效果如何呢?
从控制台的输出我们可以看出,对应的函数确实是执行了,但是页面并没有更新,因为外部声明的变量毕竟不是状态,react是不会进行跟踪的,那么如果我们想要用函数式组件,还想用状态,这时候就需要react-hook了。
用useState与useEffect来改写函数式组件
那么继续看下面的改写代码。
函数式组件和类式组件比,最欠缺的无非是两点,第一点是状态,第二点是生命周期钩子。在hook中,对于状态,用useState补足,对于生命周期钩子,用useEffect替代,那么具体用法是怎么样的呢?
useState的用法
先看useState,这个方法我们可以传入一个值作为这个状态的初始值,这个方法的返回值是个数组,数组中有两个元素,第一个元素是我们初始化的值,第二个元素是一个函数,这个函数可以用来操作我们设置的这个状态,通常是用解构的方法来取得这两个元素。
useEffect的用法
再看useEffect,这个方法我们可以传入一个函数作为参数,这个函数会在页面更新的时候执行,初次加载的时候也一样会执行,是不是和生命周期的一些钩子用法很类似?那么看看具体的效果吧。
上面写的函数式组件,想要实现点击把数字加减的功能,我们在函数外部声明一个count来进行计数。
可以看到,通过点击事件触发了我们绑定的方法,这个方法是从useState的返回值中解构出来的,我们可以思考一下,这段代码如果用class写应该怎样写?首先在construstor中声明一下state,然后写一个方法, 方法中先获取state中的状态,再setState改变数值,这么一比较,class是不是麻烦了许多?
代码升级,为函数式组件继续添加状态
我们可以继续为这个代码升级,为他添加另外的状态,看如下代码:
在新的代码中,为他添加了一个名为style的状态,然后会在鼠标移入时,触发改变背景色的事件,那么实际效果是怎么样的呢?
添加了多个状态后产生了什么问题?
我们可以看到,这个功能确实是实现了,但是是不是又发现了一点小问题?好像useEffect中传入的函数不管是点击的时候,还是鼠标划入的时候,都是完全执行了啊!那么如果这里面的代码写的很多,或者当我们只想执行一部分的时候难道没法处理了吗?当然是能处理的。
针对多个状态,在useEffect用法上的更改
useEffect的第二个参数
这时候useEffect可以传入的第二个参数就派上了用场,我们可以在第二个参数传入一个数组,这个数组中要放入我们想要监控的状态,这样只有在这个状态要发生改变的时候,才会触发对应的函数去执行,我们将代码做如下的修改
这里我将number传入了useEffect,这样我们看看最终的效果
看效果可以看出来了,鼠标划入的时候,颜色改变了并没有触发我们给useEffect传入的函数,只有点击的时候触发了。
为一个组件绑定多个useEffect
同时,一个函数是可以有多个useEffect,可以支撑我们监控多个状态,我们再将代码进行升级。
这样从代码上看,我们对number和style进行了监控,那么来看一下最终的效果。
现在就如我们预期的,在对应的状态发生变化的时候,只执行我们想要的代码了。
将useEffect设置成只执行一次应该怎么做?
也要补充一点,如果想要useEffect达到componentDidMount的效果的话,需要将空数组作为第二个参数传递给useEffect,这样的useEffect是只有在初次加载的时候执行,也就是只会执行一次。
hook的复用功能详解
看到这,可能有的人还会有疑问,目前来看,hook只是让写的代码更简洁了,但是功能好像差不多,如果class类式组件用的很熟的话,可能开发效率上也不会相差太多啊。
也就是说,目前来看,我们只看到了hook对于代码繁琐程度的优化,我们不用去很繁琐的创建状态和去修改状态,去记各种生命周期钩子,但是复用这一点还没有见识到。
class想要复用,应该如何处理?
首先我们考虑这样一种情景,比如我们有一个很大型的组件,这个组件有A,B,C三个功能,这三个功能都依赖这个组件的state,如果我想在另一个组件中用到A功能,但是因为A功能对state有依赖,那么就不得不传递这个state,还要复写功能的代码,是不是很繁琐?
hook想要复用,应该如何处理?
那么如果用hook来实现,应该怎么实现呢?这时候就需要用到自定义hook,来看下面这样的一段代码。
我们声明了一个自定义hook,这个hook有自己的三个状态,isLoading,response,error,我们传入想要请求的地址,和想要建立依赖的数据,当有处理结果的时候,把三个状态值返回,这样在哪里调用了这个hook,只需要看一下返回值的三个状态就可以了,这样我们就完成了功能的完全抽离,在任何地方想要请求数据,都可以直接调用这个hook来实现。
两种方式相互比较有什么优缺点?
反观如果用类式组件,如果状态依赖严重,我们只能传递组件本身的状态,如果依赖不严重,把对应的方法抽离成公共的工具,也需要创建对应的state,可能多个组件里这个类似的属性我们重复创建了几十次,只为了实现一个相同的功能。
hook用法小结
对于hook的用法,总的来说,对于功能抽离成自定义hook,对于组件,用useState与useEffect来为函数式组件添加状态与类似声明周期钩子,这样写法简洁,功能也能复用。
总结
什么是hook?为什么要叫hook?
首先让我们回到初始,hook是什么?或者说,为什么react要给他起这样一个名字?
hook在英语中,是钩子的意思,那么react给他起这个名字,必然不会是乱起的,react是希望这个东西就像一个个小钩子一样,我们在哪里有需要,就把这个钩子钩在哪里。
而且从上面代码中,是不是发现useState,和useEffect这两个方法和我们声明的自定义hook用法很类似?其实useState和useEffect就是react中给我们提供的hook,react还给我们提供了很多的hook,可以自己闲暇的时候去阅读学习一下。
hook的优势
可以说,hook在写法简洁程度和复用代码的程度上,都相对与class有个非常大的提升,但是如果对于习惯了用class类式组件开发的人来说,确实还是很难受的,但是我们要有接受新知识的能力,对于优秀的技术,要能去接纳,才能有一个踏实的立足点。
使用hook的注意事项
适用版本
需要react16.8以上的版本。
tips补充
最后补充两点小tips:
1 不要在循环,条件或嵌套函数中调用Hook,必须始终在React函数的顶层使用Hook。这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
2 只能在React函数式组件或自定义Hook中使用Hook。
对于第一点tip的详解
第二点不难理解,第一点可能理解上可能会有点难以理解,为什么循环和分支会出现问题?这是因为,hook是通过执行的顺序来确定状态值的。我们看下面的代码
这个代码在第一次执行,和第二次执行的时候,执行顺序会有不同,第一次执行的时候,因为showFruit是true,所以三个useState都正常执行了,但是第二次执行的时候,因为shouFruit已经变成了false,所以中间的useState是没有执行的,那么执行结果如何?
我们可以看到,第二次渲染因为执行顺序不对,会直接导致报错。所以我们在使用时,要注意自己的写法问题,避免这种问题的产生。
补充hook
上下文useContext
使用场景简介
为了方便复用和结构清晰,我们会将一个大的模块拆分成数个组件,这样就难以避免有时候可能会有多层级的包裹,而如果要跨层级传值,使用传统的一层一层传值方式就显得非常的不方便,这种情况就非常适用于useContext。
使用方法概述
具体分为三个步骤。
1.创建context对象。
2.使用context对象的provider属性提供value。
3.使用useContext获取传递的value。
三层嵌套的组件关系应用
下面看一个具体的场景实际应用,首先有三个组件,嵌套三层,代码如下:
父组件:
子组件:
孙辈组件:
如果是传统传参,那么首先要在父组件向子组件传递,再由子组件向孙辈组件传递。但是如果用useContext会使得这个传值过程变得简化许多。
对值的提供者和接受者做相应配置
首先是要对信息的提供者进行相应的配置:
如上图代码,首先创建上下文对象,然后使用上下文对象的Provider包裹其中的子组件,value为要传递的值。
子组件首先要引入之前父组件创建的上下文对象,然后使用useContext将上下文对象注入,那么就可以直接跨层级获取到由父辈传递给孙辈的值了。
和redux有些类似的useReducer
使用场景简介
有时候,我们需要对同一个状态有各种各样的处理,而这时候,如果应用useState虽然可以满足我们的开发需求,但是这样会使得state过于臃肿,且不利于阅读。我们作为程序员,一定要记得,代码虽然是写给机器运行的,但更是写给人看的,useReducer可以让代码变得更利于阅读。
具体用法
useReducer这个钩子,对于对redux比较熟悉的人会非常好理解,直接看下面的代码。
useReducer和useState的用法非常类似,需要我们传入两个参数第一个参数为我们定义的reducer,第二个参数为我们为状态设定的初始值。
想要触发对应的action也非常简单,直接调用接收的dispatch,传入对应的参数即可触发相应的事件了,效果也和之前使用的useState创建的计数器类似。
为什么要用useReducer?
那么,看到这里,会不会有一个疑问,这样的功能,用useState依然可以实现,那还用useReducer干嘛?但是如果真要我做比较,在useState和useReducer里比较,我个人更倾向于useReducer。
我认为最直接的一点,就是对这个状态相关逻辑代码集中到了一个函数内,更利于测试,维护,和他人阅读。之前的useState创建的计数器,是把逻辑写传入对应状态的处理函数内,但是一旦情况变得多,代码将难以维护,不利于阅读。
还是那句话,代码是写给人看的,写出来的代码只是能运行别人谁也看不懂,那也含无意义。
小结
在我看来,如果我写的这篇文章可以完全理解,那么将hook应用到开发中,最多是刚开始使用会有些不熟悉,但是基本不会出现有无法实现的功能的情况。同时代码也会相比于原来精简不少,这个精简,并不单纯是行数的减少,而是代码的可阅读性,简洁程度。
hook的应用,在我看来分三步,第一步是会用,第二步是优化,第三步是原理的理解。这篇文章中的代码逻辑都比较简单,实际应用中,会有一些复杂的情况,是我们需要对性能进行优化的,性能的优化,永远是前端的重中之重。
这篇文章仅仅作为介绍hook想应用到开发中的入门,后续我会根据自己的学习理解,写优化篇和原理篇,感谢阅读!