为什么需要Hook?
Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。
首先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
- class组件可以定义自己的state,用来保存组件自己内部的状态;函数式组件不可以,因为函数每次调用都会产生新的临时变量;
- class组件有自己的生命周期,可以在对应的生命周期中完成自己的逻辑;比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;在hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
- class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
所以,在Hook出现之前,对于上面这些情况通常都会编写class组件。
Class组件存在的问题
- 复杂组件变得难以理解:
我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除);而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
- 难以理解的class
很多人发现学习ES6的class是学习React的一个障碍。比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;虽然前端开发人员必须掌握this,但是依然处理起来非常麻烦;
- 组件复用状态很难
在前面为了一些状态的复用我们需要通过高阶组件或render props;像redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,代码就会存在很多嵌套;这些代码不管是编写和设计上来说,都变得非常困难;
Hook的出现
Hook的出现,可以解决上面提到的这些问题;
简单总结一下hooks:
- 它可以让我们在不编写class的情况下使用state以及其他的React特性;
- 但是我们可以由此延伸出非常多的用法,比如自定义hook;
Hook的使用场景:
Hook的出现基本可以代替我们之前所有使用class组件的地方(除了一些非常不常用的场景);但是如果是一个旧的项目,并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,可以渐进式的来使用它;Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用
Hook 规则
Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则。
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的useState和 useEffect调用之间保持 hook 状态的正确。
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook
Class组件和Functional组件对比
通过一个计数器案例,来对比一下class组件和函数式组件结合hooks的对比:
上面的代码差异非常大:函数式组件结合hooks让整个代码变得非常简洁,并且再也不用考虑this相关的问题
useState
State Hook的API就是 useState,useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为undefined)。useState是一个数组,可以通过数组的解构,来完成赋值会非常方便。
通过传入 useState 参数后返回一个带有默认状态和改变状态函数的数组。通过传入新状态给函数来改变原本的状态值。值得注意的是 useState 不帮助你处理状态,相较于 setState 非覆盖式更新状态,useState 覆盖式更新状态,需要开发者自己处理逻辑。
FAQ:为什么叫 useState 而不叫 createState?
- “Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。
- 在下一次重新渲染时,useState 返回给我们当前的 state。
- 如果每次都创建新的变量,它就不是 “state”了。
- 这也是 Hook 的名字总是以 use 开头的一个原因。
当然,我们也可以在一个组件中定义多个变量和复杂变量(数组、对象)
多个状态使用的情况
复杂状态的使用情况
Effect Hook
目前已经通过hook在函数式组件中定义state,那么类似于生命周期这些呢?Effect Hook 可以让你来完成一些类似于class中生命周期的功能;事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);所以对于完成这些功能的Hook被称之为 Effect Hook。
现在有一个需求:页面的title总是显示counter的数字,分别使用class组件和Hook实现:
class实现
Hook实现
useEffect的解析:
- 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
- useEffect要求传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
- 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;
清除Effect
在class组件的编写过程中,某些副作用的代码,需要在componentWillUnmount中进行清除:比如事件总线或Redux中手动调用subscribe;都需要在componentWillUnmount有对应的取消订阅;
- Effect Hook通过什么方式来模拟componentWillUnmount呢?
useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:
type EffectCallback = () => (void | (() => void | undefined))
- 为什么要在 effect 中返回一个函数?
这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;如此可以将添加和移除订阅的逻辑放在一起;它们都属于 effect 的一部分;
- React 何时清除 effect?
React 会在组件更新和卸载的时候执行清除操作;effect 在每次渲染的时候都会执行;
使用多个Effect
使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;使用Effect Hook,可以将它们分离到不同的useEffect中:Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
Effect性能优化
默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
某些代码只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;另外,多次执行也会导致一定的性能问题;
如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?useEffect实际上有两个参数:
- 参数一:执行的回调函数;
- 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:
那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数
useContext
在之前的开发中,我们要在组件中使用共享的Context有两种方式:
- 类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
- 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;
但是多个Context共享时的方式会存在大量的嵌套:
Context Hook允许我们通过Hook来直接获取某个Context的值;
注意事项:
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider的 context value 值
useCallback
在类组件中,我们经常犯下面这样的错误:
这样写有什么坏处呢?一旦 App 组件的 props 或者状态改变了就会触发重渲染,即使跟 SomeComponent 组件不相关,由于每次 render 都会产生新的 style 和 doSomething(因为重新render前后, style 和 doSomething分别指向了不同的引用),所以会导致 SomeComponent 重新渲染,倘若 SomeComponent 是一个大型的组件树,这样的 Virtual Dom 的比较显然是很浪费的,解决的办法也很简单,将参数抽离成变量。
在类组件中,我们还可以通过 this 这个对象来存储函数,而在函数组件中没办法进行挂载了。所以函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。
函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。 而有了 useCallback 就不一样了,可以通过 useCallback 获得一个记忆后的函数。
第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCallback 就会重新返回一个新的记忆函数提供给后面进行渲染。 这样只要子组件继承了 PureComponent 或者使用 React.memo 就可以有效避免不必要的 VDOM 渲染。
useMemo
useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。useMemo实际的目的也是为了进行性能的优化。
所以前面使用 useCallback 的例子可以使用 useMemo 进行改写:
唯一的区别是:**useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你。**所以在前面的例子中,可以返回 handleClick 来达到存储函数的目的。 所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。
当 a/b 改变时,child1/child2 才会重新渲染。从例子可以看出来,只有在第二个参数数组的值发生变化时,才会触发子组件的更新。
自定义Hook
自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。