Skip to content
大纲

hooks

整理 React 所有 hooks 相关问题

  1. Hooks 的由来是什么?
  2. React Hooks 有哪些,都是做什么的?
  3. useRef 的高级用法是什么?
  4. useMemouseCallback 什么区别?是怎么做优化的?从本质上说,useCallback(fn, deps) 就是useMemo(() => fn, deps) 的语法糖:
  5. 一个好的自定义 Hooks 该如何设计?
  6. 如何做一个不需要 useState 就可以直接修改属性并刷新视图的自定义 Hooks?
  7. 如何做一个可以监听任何事件的自定义 Hooks?
  8. useEffectuseLayoutEffect,怎么做到的?

Hooks 的由来是什么?

react-hooks 是 React16.8 以后新增的钩子 API,目的是增加代码的可复用性、逻辑性,最主要的是解决了函数式组件无状态的问题,这样既保留了函数式的简单,又解决了没有数据管理状态的缺陷。

React hooks 有哪些?

截止到现在,已经有 22 个 Hooks 了。

  • react 包导出了 21 个
  • react-dom 包导出了 1 个 (useFormStatus)

react

截止当前,React 的发展主要经历了 3 个时期 👇

  1. CSR 时期(客户端渲染时期)
  2. 并发时期
  3. RSC 时期(服务端组件时期)

当前的 hooks 也都是这 3 个时期的产物

CSR 时期

与状态的流转相关的👇
  1. useState

  2. useReducer

  3. useContext

    与处理副作用相关的 👇

  4. useEffect

  5. useLayoutEffect

    与提高操作自由度相关的 👇

  6. useRef

    与性能优化相关的 👇

  7. useMemo

  8. useCallback

    与调试相关的 👇

  9. useDebugValue

    为了完善 CSR 的并发模式,对现有 hooks 能力进行补充或约束

  10. useImperativeHandle(控制 useRef 防止其丢失)

  11. useEffectEvent(对 useEffect 能力的补充)

  12. useInsertionEffect(对 useEffect 场景的补充)

  13. useMemoCache(减少性能优化心智负担)👇

    FC 中的两个性能优化的 hook,存在比较重的心智负担,比如

    1. 开发者需要考虑是否需要性能优化
    2. 开发者需要考虑何时使用 useMemo, useCallback

    为了解决这个问题,在 2021 年的 React Conf,黄玄带来了「能够通过编译器生成等效于 useMemouseCallback 代码」的方案 —— React Forget。

    useMemoCache 就是 React 内部为 React Forget 提供缓存支持的 hook。

    所以这个 hook 是给编译器用的,而不是我们普通开发者。

    并发时期,随着并发特性落地,首先推出的是两个并发相关的 hook

  14. useTransition

  15. useDeferredValue

    👆 这两个 hook,本质都是降低更新的优先级,「更新」意味着「视图渲染」,所以当更新拥有不同优先级后,这意味着「视图渲染」拥有不同优先级。

    这就是并发更新的理论基础。

    但是,并发更新的出现,打破了 React 沿袭多年的「一次更新对应一次渲染」的模式。

    为了让现有的库兼容并发模式,推出了如下 hook:

  16. useMutableSource

  17. useSyncExternalStore

    所以,👆 上述 2 个 hook 主要是面向开源库作者。

    RSC 时期 RSC(服务端组件)是一个浩大的工程

  18. useId

    在并发时期,由于引入了「渲染优先级」的概念,那势必存在一些由于优先级不足,而处于 pending 中的渲染。

    如何展示「渲染的 pending 状态」呢?React 引入了 <Suspense> 组件。

    到了 RSC 时期,React 团队发现,「渲染的 pending 状态」是 pending,「数据请求的 pending 状态」不也是 pending 吗?

    换言之,任何需要中间 pending 状态的流程,不都可以纳入 <Suspense> 的管理范围?

    那该怎么标记一个流程可以被纳入 <Suspense> 的管理呢?于是有了:

  19. use

    👆 通过这个 hook 声明的流程中的 pending 状态都会被纳入 <Suspense> 的管理。

    既然 <Suspense> 越来越重要,那我们是不是要针对他做些优化?既然 <Suspense> 可以在不同视图之间切换,那为他增加缓存显然是种不错的优化方式,于是有了:

  20. useCacheRefresh(用于建立 <Suspense> 缓存)

    到这一步,RSC 的基础设施算是搭好了,下一步该构建上层应用了。

    在浏览器端,与 RSC 理念最契合的便是 form 标签,围绕 form 标签的 action 属性,React 推出了如下 hook:

  21. useOptimistic

  22. useFormStatus

    👆 这 2 个 hook 都是为了优化「表单提交」这一场景(也可以说是 RSC 与客户端的交互场景)。

  23. use-sync-external-store

    👆 官方还使用独立包提供了这个 hook, 使用同步的外部存储,这是向后兼容的垫片 React.useSyncExternalStore。适用于任何支持 Hooks 的 React。

    zustand 就是基于此来实现的

总结

如果说 CSR 时期的 hook 都是面向开发者直接使用的。那么并发时期最初的 2 个 hook(useTransitionuseDeferredValue)已经鲜有开发者使用了,而后期类似 useMutableSource 这样的 hook,普通开发者则根本用不到。

同样的,再往后的 RSC 时期的所有 hook,普通开发者都用不到。他们都是为其他库、框架(比如 Next.js)提供的。

这标志着 React 发展方向的不断变化:

  • 早期,定位是前端框架,主要为了解决 facebook 自身问题,顺便开源,受众是开发者
  • 中期,定位是底层 UI 库,受众是开源库作者
  • 当前,定位是 web 底层操作系统,受众是上层全栈框架

深入理解

class 组件的不足

  • 难以复用组件间状态逻辑
  • 难以维护复杂组件
  • this 指向问题 (class 的方法默认不会绑定 this)
  • 难以对 class 进行编译优化

Hook 的优势

  • 无需改变组件结构的情况下复用状态逻辑(自定义 Hook)
  • 将组件中互相关联的部分拆分成更小的函数(比如设置订阅或请求数据)
  • 在非 class 的情况下可以使用更多的 React 特性

Hook 使用规则

Hook 就是 Javascript 函数,使用它们时有两个额外的规则:

  • 只能在函数外层调用 Hook,不要在循环、条件判断或者子函数中调用
  • 只能在 React 的函数组件和自定义 Hook 中调用 Hook。不要在其他 JavaScript 函数中调用

在组件中 React 是通过判断 Hook 调用的顺序来判断某个 state 对应的 useState 的,所以必须保证 Hook 的调用顺序在多次渲染之间保持一致,React 才能正确地将内部 state 和对应的 Hook 进行关联

useState vs useReducer

useState 用于在函数组件中调用给组件添加一些内部状态 state,唯一的参数就是初始 state,会返回当前状态和一个状态更新函数

useReducer 作为 useState 的代替方案,在某些场景下使用更加适合,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。

使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为父组件可以向子组件传递 dispatch 而不是回调函数

useEffect vs useLayoutEffect

在函数组件主体内(React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性

useEffect Hook 的使用则是用于完成此类副作用操作。useEffect 接收一个包含命令式、且可能有副作用代码的函数

useEffect 函数会在浏览器完成布局和绘制之后,下一次重新渲染之前执行,保证不会阻塞浏览器对屏幕的更新

useEffect Hook 函数执行时机类似于 class 组件的 componentDidMountcomponentDidUpdate 生命周期,不同的是传给 useEffect 的函数会在浏览器完成布局和绘制之后,下一次重新渲染之前,进行异步执行,保证不会阻塞浏览器对屏幕的更新。useEffect 返回一个清除函数,类似 componentDidUnmount,会在组件卸载前执行。

useEffect 可以接收第二个参数,它是 effect 所依赖的值数组,这样就只有当数组值发生变化才会重新创建订阅。但需要注意的是:

  • 确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量
  • 传递一个空数组作为第二个参数可以使 effect 只会在初始渲染完成后执行一次

useLayoutEffect 在浏览器重新绘制屏幕之前触发。

useContext

Context 提供了一个无需为每层组件手动添加 props ,就能在组件树间进行数据传递的方法,useContext 用于函数组件中订阅上层 context 的变更,可以获取上层 context 传递的 value prop 值

useContext 接收一个 context 对象(React.createContext 的返回值)并返回 context 的当前值,当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定

useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>

调用了 useContext 的组件都会在 context 值变化时重新渲染,为了减少重新渲染组件的较大开销,可以通过使用 memoization 来优化

useRef vs createRef

  • createRef 每次渲染都会返回一个新的引用
  • useRef 每次都会返回相同的引用。

useRef 用于返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)

useRef 创建的 ref 对象就是一个普通的 JavaScript 对象,而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象

使用场景(useRef 高级用法)

  • 用 ref 引用一个值(改变 ref 不会触发重新渲染,可用于缓存数据,获取最新值)
  • 通过 ref 操作 DOM(获取对应元素的相关属性)
  • 避免重复创建 ref 的内容(高昂开销的初始化)

无法获取自定义组件的 ref?

将其包装在 forwardRef 里,父级组件就可以得到它的 ref。

jsx
import { forwardRef } from 'react'

const MyInput = forwardRef(({ value, onChange }, ref) => {
  return <input value={value} onChange={onChange} ref={ref} />
})

export default MyInput

注意

不要在渲染期间写入 或者读取 ref.current

你可以在 事件处理程序或者 effects 中读取和写入 ref。

如果 不得不 在渲染期间读取 或者写入,使用 state 代替。

React.memo vs useMemo vs useCallback

React.memo() 随 React v16.6 一起发布。它是一个 HOC 高阶组件,其作用是结合了 PureComponent 纯组件和 componentShouldUpdate 功能,会对传入的 props 进行一次对比,然后根据第二个函数返回值来进一步判断哪些 props 需要更新

jsx
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

使用 memo 将组件包装起来,以获得该组件的一个 记忆化 版本。通常情况下,只要该组件的 props 没有改变(默认情况下,React 将使用 Object.is 比较每个 prop),这个记忆化版本就不会在其父组件重新渲染时重新渲染。但 React 仍可能会重新渲染它:记忆化是一种性能优化,而非保证。

useCallbackuseMemo 结合 React.Memo 方法的使用是常见的性能优化方式,可以避免由于父组件状态变更导致不必要的子组件进行重新渲染

  • useMemo 返回的是函数运行的结果
  • useCallback 返回的是函数

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

使用注意:

  • 传入 useMemo 的函数会在渲染期间执行,不要在这个函数内部执行与渲染无关的操作
  • 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值

自定义 hooks 该如何设计?

hooks 本质上是一个函数,而这个函数主要就是逻辑复用

自定义 hooks 的名称是以 use 开头

jsx
const [xxx, setXXX] = useXXX(x1, x2, ...)

hooks 实现原理

手写实现 useState

其他 APIs

  • react
    • Built-in React APIs
      • createContext
      • forwardRef
      • lazy
      • memo
      • startTransition
  • Legacy React APIs
    • children
    • cloneElement
    • Component
    • createElement
    • createRef
    • isValidElement
    • PureComponent
  • react-dom
    • APIs
      • createPortal
      • flushSync
      • findDOMNode
      • hydrate
      • render
      • unmountComponentAtNode
    • client APIs
      • createRoot
      • hydrateRoot

关于 hooks 还是要熟读官方文档,然后结合三方 hooks 库加深理解,下面是社区比较优秀的 React Hooks utils 库:

参考文 ahooks 常见场景的封装,该文是全网最全 ahooks 源码分析篇之一,该系列已整理成文档-地址

参考