HomeGithub

浅析 React 中的 Hooks

转眼间已经使用 React Hooks 好几年,这期间实践了各种灵活且强大的原生 Hook,这篇文章是我用分类法对 hooks 用法的总结。React 本身足够复杂,所以这篇博客做了以下限制:

3 个状态 Hook

所有现代 UI 框架的理念都是: UI = f(state), 当状态(state)改变时,UI 会自动做相应的调整。React 中控制组件状态的原生 hook 有以下 3 种:

  1. useState
  2. useReducer
  3. useContext

useState 是 React 中最常用的 hook,这里我想突出 3 点:

1. setState 是异步的
点击展开
2. setState 的参数可以是函数(可以保证每次拿到的状态都是最新的)
点击展开
3. React 有批处理(batching)机制
点击展开

useReduceruseState 的高级版本。Reducer 的概念来自于 Redux,它内部封装了动作(action)具体如何更新状态(state)的细节,使用者只需要释放(dispatch)一个动作(action)即可改变状态(state)。对于状态更新来自于多种不同类型事件(比如 Todo List)的场景,useReducer 可以让代码变得简洁优雅。

context 是 React 原生提供的状态容器,可以在跨层级的多个组件间共享状态,解决了著名的属性透传(prop drilling)问题。它使用 Provider 模式,在某一个顶层组件提供 value(通常基于该组件的 state),然后就可以在被它包裹的任意一个子组件(可跨层级)使用 useContext 来消费(Consume)它。

2 个副作用 Hook

effect 是副作用,React 本身只负责 UI=f(state),也就是当状态改变去渲染新 UI。但诸如数据请求,设置定时器,手动操作 DOM 等工作 React 并不关心,React 只提供相关 hook 让用户自己决定做什么,什么时候做,如何做。主要是 2 个 Hook:

  1. useEffect
  2. useLayoutEffect

useEffect 是除 useState 外最常被使用的 hook,它有 2 个参数,第一个是函数也就是 effect,第二个是个数组也就是该 effect 的依赖项。当有任何一个依赖项发生改动时,effect 就会被执行。

使用 useEffect 时要注意下面几点:

useLayoutEffect 一般很少用,它和 useEffect 的区别在于 useEffect 在浏览器绘制(Paint)之后执行,而 useLayoutEffect 在浏览器的布局(Layout)之后就执行。利用这个特性,可以在 UI 被绘制出之前做一些处理。一个典型的例子是 <Tooltip /> 组件,由于不知道 Tooltip 被渲染后会不会因为页面空间不足而被截断,可以先调用 useLayoutEffect 判断下,如果发现确实空间不够可以调整 Tooltip 出现的位置。如果在 useEffect 阶段判断再调整的话,用户会先看到被截断的 Tooltip 一闪而过,造成不好的用户体验。

关于浏览器渲染流程可以参考:浏览器的工作原理

2 个 Ref 相关 Hook

Ref 是组件内部一种引用(Reference),能够在多次 render 之间保持引用有效性。它的使用场景有 2 种:

1. 作为 DOM 节点的引用,从而可以手动操作 DOM 节点
点击展开
2. 想要记录某种状态,但又不想因为该状态的变化触发重渲染(rerender)
点击展开

和 ref 相关的 hook 有 2 个:

  1. useRef
  2. useImperativeHandle

useRef 上面已经介绍过。useImperativeHandle 偶尔会被用到,它在能够接收 ref 的子组件里定义并暴露出该 ref 的一些封装好的方法,这样其父组件就能通过 ref 的方式手动调用这些方法从而达到控制子组件的目的。具体用法可以查看 useImperativeHandle

需要注意的是使用 Ref 作为 DOM 引用然后手动控制 DOM 是一种 Escape Patch,应该尽量少用。这样写出来的代码更加的符合 React 的设计哲学:声明式 & 函数式。

2 个 Memo 相关 Hook

  1. useMemo
  2. useCallback

函数组件本质是个函数,每次组件渲染该函数会直接重新运行一遍。如果组件中含有复杂计算过程,就很可能出现性能问题,造成 UI 卡顿。useMemo 就用于解决这类问题,它的本质是性能优化中常用的技巧之一:使用缓存,也又称记忆化(Memoization)。用法比较简单,也有依赖项,具体可以参考 React 文档 - useMemo

useCallback 是为组件内函数做缓存,为什么函数也要做缓存呢?这是因为该函数可能会作为 useEffect 的依赖项,如果不做缓存将会在每一次渲染过程中都产生一个新函数,而 React 判断依赖项是否改变使用的算法是 Object.is,这样以来依赖该函数的 useEffect 会在每一次渲染的时候都被执行。useCallback 的用法也是基于依赖项的,具体可以参考 React 文档 - useCallback

总结和反思