Skip to content

第十二节:React Hooks解析

  • 认识和体验Hooks
  • State/Effect
  • Context/Reducer
  • Callback/Memo
  • Ref/LayoutEffect
  • 自定义Hooks使用

为什么需要Hook?

Hook是React 16.8 的新增特性,它可以让我们在**使用****(比如生命周期)**

我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:

class组件可以,用来

  • 函数式组件不可以,因为函数每次调用都会产生新的临时变量

class组件有,我们可以

  • 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次
  • 在学习hooks之前,如果在函数式中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求

class组件可以以及

  • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次

所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件

Class组件存在的问题

复杂组件变得难以理解

  • 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是,我们的
  • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括(还需要在componentWillUnmount中移除)
  • 而对于这样的class实际上非常难以拆分:因为

难以理解的class

  • 很多人发现
  • 比如在class中,我们,所以需要花很多的精力去学习this
  • 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦

组件复用状态很难

  • 在前面为了一些状态的复用我们需要
  • 像我们之前学习的,这些高阶组件设计的目的就是
  • 或者,但是
  • 这些代码让我们

Hook的出现

Hook的出现,可以解决上面提到的这些问题

简单总结一下hooks

  • 但是我们

Hook的使用场景

  • Hook的出现
  • 但是如果是一个旧的项目,你,因为
  • Hook

在我们继续之前,请记住Hook是

  • :你无需重写任何已有代码就可以在一些组件中尝试Hook。但是如果你不想,你不必现在就去学习或使用Hook
  • :Hook不包含任何破坏性改动
  • :Hook已发布于v16.8.0

Class组件和Functional组件对比

计数器案例对比

我们通过一个计数器案例,来对比一下class组件和函数式组件结合hooks的对比:

你会发现上面的代码差异非常大

  • 函数式组件结合hooks让整个代码变得非常简洁
  • 并且再也不用考虑this相关的问题

useState解析

那么我们来研究一下核心的一段代码代表什么意思:

  • ,需要,它是一个
    • ,如果
    • ,包含
      • 元素一:(第一调用为初始值)
      • 元素二:
    • 点击button按钮后,会完成两件事情:
      • 调用
      • ,并且

相信通过上面的一个简单案例,你已经会喜欢上Hook的使用了

  • Hook就是JavaScript函数,这个函数可以帮助你

但是使用它们会有两个额外的规则

  • 只能在函数。不要
  • 只能中调用Hook,

认识useState

State Hook的API就是useState,我们在前面已经进行了学习

FAQ:为什么叫useState而不叫createState?

  • “create”可能不是很准确,因为
  • 这也是

当然,我们也可以在一个组件中定义多个变量和复杂变量(数组、对象)

认识Effect Hook

目前我们已经通过hook在函数式组件中定义state,那么类似于生命周期这些呢?

  • Effect Hook可以让你来完成一些
  • 事实上,类似于,都是(Side Effects)
  • 所以对于完成这些功能的Hook被称之为Effect Hook

假如我们现在有一个需求:页面的title总是显示counter的数字,分别使用class组件和Hook实现:

  • 案例代码不再贴出

useEffect的解析:

  • 通过useEffect的Hook,可以告诉
  • useEffect,在React,就
  • 默认情况下,,还是每次更新之后,都会

需要清除Effect

在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除

  • 比如我们之前的事
  • 都需要在
  • Effect Hook通过什么方式来模拟componentWillUnmount呢?

useEffect传入的**本身可以有一个返回值,这个返回值是另外一个**

为什么要在effect中返回一个函数?

  • 这是
  • 如此可以
  • 它们

React何时清除effect?

  • React
  • 正如之前学到的,

使用多个Effect

使用Hook的其中一个目的就是解决class中生命周期经常将很多逻辑放在一起的问题

  • 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中

使用Effect Hook,我们可以将它们分离到不同的useEffect中

  • 代码不再给出

Hook允许我们按照代码的用途分离它们,而不是像生命周期函数那样:

  • React将按照effect声明的顺序依次调用组件中的每一个effect

Effect性能优化

默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

  • 某些代码我们只是,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅)
  • 另外,

我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?

  • useEffect实际上有两个参数
  • 参数一:
  • 参数二:该;(受谁的影响)

案例练习:

  • 受count影响的Effect

但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组

  • 那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了

useContext的使用

在之前的开发中,我们要在组件中使用共享的Context有两种方式:

  • 类组件可以通过方式,在类中获取context
  • 多个Context或者在函数式组件中通过方式共享context

但是多个Context共享时的方式会存在大量的嵌套

  • Context Hook

注意事项:

  • 当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重新渲染,并使用最新传递给MyContext provider的context value值。

useReducer

很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是

useReducer仅仅是useState的一种替代方案

  • 在某些场景下,

数据是不会共享的,它们只是使用了相同的counterReducer的函数而已

所以,useReducer只是useState的一种替代品,并不能替代Redux

useCallback

useCallback实际的目的是为了进行性能的优化

如何进行性能的优化呢?

  • useCallback

案例

  • 案例一:使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化
  • 案例二:使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化

通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存

useMemo

useMemo实际的目的也是为了进行性能的优化

如何进行性能的优化呢?

  • useMemo返回的也是一个memoized(记忆的)值
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的

案例

  • 案例一:进行大量的计算操作,是否有必须要每次渲染时都重新计算
  • 案例二:对子组件传递相同内容的对象时,使用useMemo进行性能的优化

useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变

最常用的ref是两种用法

  • 用法一:引入DOM(或者组件,但是需要是class组件)元素
  • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变

案例

  • 案例一:引用DOM
  • 案例二:使用ref保存上一次的某一个值

useImperativeHandle

useImperativeHandle并不是特别好理解,我们一点点来学习

我们先来回顾一下ref和forwardRef结合使用

  • 通过forwardRef可以将ref转发到子组件
  • 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中

forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件

  • 直接暴露给父组件带来的问题是某些情况的不可控
  • 父组件可以拿到DOM后进行任意的操作
  • 但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他的并不希望它随意操作

通过useImperativeHandle可以只暴露固定的操作:

  • 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
  • 所以在父组件中,使用inputRef.current时,实际上使用的是返回的对象
  • 比如我调用了focus函数,甚至可以调用printHello函数

useLayoutEffect

useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新

如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect

案例:useEffect和useLayoutEffect的对比

自定义Hook

自定义Hook本质上是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性

自定义 Hook函数名需要以use开头

需求:所有的组件在创建和销毁时都进行打印

  • 组件被创建:打印 组件被创建了
  • 组件被销毁:打印 组件被销毁了

自定义Hook练习

需求一:Context的共享

需求二:获取鼠标滚动位置

需求三:localStorage数据存储

redux hooks

在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect

  • 但是这种方式必须使用高阶函数结合返回的高阶组件
  • 并且必须编写:mapStateToProps和mapDispatchToProps映射的函数

在Redux7.1开始,提供了Hook的方式,我们再也不需要编写connect以及对应的映射函数了

****的作用是将state映射到组件中

  • 参数一:将state映射到需要的数据中
  • 参数二:可以进行比较来决定是否组件重新渲染(后续讲解)

****默认会比较我们返回的两个对象是否相等

  • 如何比较呢? const refEquality = (a,b) => a === b
  • 也就是我们必须返回两个完全相等的对象才可以不引起重新渲染

useDispatch非常简单,就是直接获取dispatch函数,之后在组件中直接使用即可

我们还可以通过useStore来获取当前的store对象

useId

官方的解释:useId是一个用于生成横跨服务器和客户端的稳定的唯一ID的同时避免你不匹配的hook。

这里有一个词叫hydration,要想理解这个词,我们需要理解一些服务器渲染(SSR)的概念

什么是SSR?

  • SSR**(Server Side Rendering,服务端渲染)**,指的是页面在服务器端已经生成了完成的HTML页面结构,不需要浏览器通过执行JS代码,创建页面结构
  • 对应的是CSR**(Client Side Rendering,客户端渲染)**,我们开发的SPA页面通常依赖的就是客户端渲染

早期的服务端渲染包括PHP,JSP,ASP等方式,但是在目前前后端分离的开发模式下,前端开发人员不太可能再去学习PHP,JSP等技术来开发网页

不过我们可以借助Node来帮助我们执行JavaScript代码,提前完成页面的结构

SSR同构应用

什么是同构?

  • 一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。

同构是一种SSR的形态,是现代SSR的一种表现形式

  • 当用户发出请求时,先是服务器通过SSR渲染出首页的内容
  • 但是对应的代码同样可以在客户端被执行
  • 执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染

Hydration

什么是Hydration?这里我引入vite-plugin-ssr插件的官方解释

在进行SSR时,我们的页面会呈现为HTML

  • 但仅HTML不足以使页面具有交互性。例如,浏览器端JavaScript为零的页面不能是交互式的(没有JavaScript事件处理程序来响应用户操作,例如单击按钮)
  • 为了使我们的页面具有交互性,除了在Node.js中将页面呈现为HTML之外,我们的UI框架(Vue/React/...)还在浏览器中加载和呈现页面。(它创建页面的内部表示,然后将内部表示映射到我们在Node.js中呈现的HTML的DOM元素。)

这个过程称为hydration

useId的作用

我们再来看一遍:useId是一个用于生成横跨服务端和客户端的稳定的唯一ID的同时避免hydration不匹配的hook

所以我们可以得出如下结论

  • useId是用于react的同构应用开发的,前端是SPA页面并不需要使用它
  • useId可以保证应用程序在客户端和服务器端生成唯一的ID,这样可以有效的避免通过一些手段生成的id不一致,造成hydration mismatch

useTransition

官方解释:返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数

  • 事实上官方的说法,还是让人云里雾里,不知所云

useTransition到底是干嘛的呢?它其实在告诉react对于某部分任务的更新优先级较低,可以稍后进行更新

useDeferredValue

官方解释:useDeferredValue接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。

在明白了useTransition之后,我们就会发现useDeferredValue的作用是一样的效果,可以让我们的更新延迟