轻巧快艇 Hooks

背景

在 React 16.8 之前 函数式组件又被称为无状态组件

因为函数在每次执行执行的时候 都会重新执行内部的代码 导致函数无法维护自身内部的状态 例如

function add(n) {
  const result = 0;
  return result + 1;
}

add(1); //1
add(1); //1

我们无法在函数中保存 result 的状态 因为每一次执行函数时 都会重新初始化 result

我们来看看 16.8 之前 类组件和函数式组件有哪些差别

  • 类组件需要继承 class,函数组件不需要

  • 类组件可以访问生命周期方法,函数组件不能

  • 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以

  • 类组件中可以定义并维护 state(状态),而函数组件不可以

  • ......

类组件的状态 一般由组件内部维护 这样就会造成组件的复用性很差

前几章中 我提到了 HOC 和 Render Props

究其根本 它们都是尽可能的为了优雅的实现代码的复用

但是 这些名词的背后无疑是繁重的学习成本

而函数式组件 又无法保存状态

这个时候 hook 就横空出世了 它想要解决的问题就是

让函数式组件拥有类似类组件的功能

我们来看一个简单的 demo

代码有点长 但是它做的事情很简单

不管是类组件还是函数式组件都只是输出父组件给它的 props

但是我们通过 setTimeout 将预期中的渲染推迟了 3s,打破了 this.props 和渲染动作之间的这种时机上的关联

进而导致类组件在渲染时捕获到的是一个错误的、修改后的 this.props

因为虽然 props 本身是不可变的,但 this 却是可变的,this 上的数据是可以被修改的

this.props 的调用每次都会获取最新的 props 确保数据实时性

而函数式组件可以确保在任何时机下读取到的 props,都是最初捕获到的那个 props

所以函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式

了解了为什么需要 hook 后 我们来看看 👀 有哪些 React 已经帮我们封装好的 hook 吧

useState

  • useState 填补了函数式组件内部不能保存状态的空白

  • 更新 useState 中保存的状态时 都是走的异步更新 对同一个属性的多次更新 会被合并 只取最新的一次

  • 使用回调函数更新状态时 可以拿到最新的 state

我们来对比一下类组件中的 setState

假如我们执行了一次 this.setState 那么组件大概会走如下流程

this.setState --> shouldComponentUpdate --> componentWillUpdate --> render --> componentDidUpdate

设想一下 如果我们每次执行 this.setState React 都要做出上述响应

那么 大概没有几次 你的页面就会出现卡顿了 所以 React 做了批量更新

但是在 hook 中 所有的更新都是异步的

然而在 类组件中 如果你使用 setTimeout/Promise 这些异步的方法包裹 setState 的话 它会让这些更新摆脱 React 的控制 从而看上去表现出了同步的样子

但是请注意 ⚠️ 无论是在函数式组件还是类组件中 更新状态永远是异步的

这意味着你无法马上获取到最新的状态 如果需要的话 请使用回调函数的方式

关于批量更新 可以参考 github 上的discussions

文中提到了在即将到来的 React18 中将会默认开始批量更新

包括在类组件中 如果我们使用 setTimeout/Promise 的这些情况 现在 React 也都能对它们进行控制 从而使类组件和函数式组件的表现趋于一致

上述的代码表示 不同的 state 之间互不影响 但是对同一个 state 的多次操作会导致批量更新

useReducer

说完 useState 再来看看 useReducer

两者都是函数式组件中对状态管理的手段

前者可看成是后者的一种实现

  • 和 redux 没有关系 组件之间不共享数据

  • 是 useState 的一种替代方案

  • 对于复杂的业务 使用 useReducer 比 useState 会有更好的可读性

执行代码后 你会发现 不同组件之间的 state 互不影响

useEffect

  • 允许函数组件执行副作用操作 在一定程度上弥补了生命周期的缺席

  • 可以接收两个参数,分别是回调函数与依赖数组 useEffect(callBack, [])

  • 第一个参数 函数形式 可实现等同于 componentDidMount shouldComponentUpdate componentWillUnmount

  • 并且可以返回一个函数 用来消除副作用 类似 componentWillUnmount 可以做一些事件的解绑 定时器的关闭等

  • 第二个参数 数组 状态依赖项 实现性能优化 如果传[] 则等同于不开启 shouldComponentUpdate

useContext

  • 可以在子组件之间共享数据

  • 在组件外创建 context 对象 const AppContext = React.createContext({})

  • 父组件内使用 context 对象下的 Provider 并赋值

  • 在子组件内过去 context 对象 React.useContext(AppContext)

useRef

  • 获取元素结点

  • 保存数据 (永远指向最初的那个值)

useCallback && useMemo

在函数式组件中使用 memo 包裹我们的组件 可以帮助我们对前后的 props 进行一个浅比较

但是如果 props 是引用类型 那么此时 memo 就会失效

我们可以使用 useCallBack 和下面提到 useMemo 来使我们的引用类型变成可记忆的

两者的关系如下 useMemo 的能力边界大于 useCallBack 但是两者在某些情况下可以相互转换

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps) -- React 官网

useCallback(() => {}, [])

useCallback 接受两个如参

第一个参数 是一个函数

第二个参数 是一个数组 是这个函数的依赖项 只有依赖项更新 函数才会重新执行

自定义 hook

自定义 hook 的函数名 必须以 use 开头 比如下面是一个打印组件创建/销毁的 useLogger hook

Hooks 使用注意点

hook 的本质是链表 React 在执行 hook 的时候 是按顺序执行的

如果将 hook 放在条件/判断语句中 那么就会打破它的执行顺序 产生意想不到的结果

所以 请将你的 hook 放在最顶层进行使用

总结

  • 告别难以理解的 Class

    • this

    • 生命周期

  • 解决业务逻辑难以拆分的问题

    • 逻辑一度与生命周期耦合在一起

  • 使状态逻辑复用变得简单可行

    • HOC(高阶组件)

    • Render Props

  • 函数组件从设计思想上来看,更加契合 React 的理念

Last updated

Was this helpful?