React必知必会(二)-React Hooks实践

435 阅读9分钟

一、前言

React Hooks从16.8.0版本 正式推出到现在已经两年多了,相信每个开发者都已经入坑,大部分也在广泛使用了。
但单就观察我们公司几个团队中的代码,发现用法千奇百怪,有不少错用、滥用的情况,理解的不是很透彻。我整理了一些供大家鉴赏,避免踩坑;

二、关于React核心概念

React做到了简单易用;它可以让你在不了解其内部概念及原理的情况下就能正常使用。
但在学习过程中,有一些核心概念是要铭记于心的:声明式函数式编程、组件化、数据驱动、React Fiber等;

这些概念看起来很简单,寥寥数语,但却往往隐含着优秀的设计理念和实践精髓;概念理解的越透彻,使用起来越得心应手!
感兴趣的可以查看本人另一篇文章

三、老生常谈:为什么要使用Hooks?

Hook 可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

那么我们为什么选择使用Hooks开发呢?
除了React版本强列推荐之外,支持渐进式引入,还有逃不过真香定律!
image.png

React Hooks引入之前遇到的问题:

1. 在组件之间复用状态逻辑很难
可以通过拆分组件的方式做到复用UI,但却没有一个简洁的方式在组件间复用状态的处理逻辑;
如果我们需要抽离一些重复的状态逻辑处理,就会选择 HOC 或者 render props 的方式。这类方式需要重新组织组件结构,改造麻烦的同时,也使代码难以理解。
2. 组件嵌套地狱问题
在实现复杂业务逻辑时,大量使用HOC、render props等高阶技巧组成的代码充斥着组件结构。打开 React DevTools 就会明显地发现正常组件被各种由 providers、高阶组件、render props 等其他抽象层组成的组件包裹,形成“嵌套地狱”。 带来的问题就是 代码难以理解,也提高了debug的难度;

3. 函数组件的局限性
React16.8之前,函数式组件不能维护内部状态,如果我们需要一个有状态管理的组件,那么就必须转成 class 的方式去创建一个组件。并且一旦 class 组件变得复杂,那么逻辑四散的代码就很不容易维护。
4.另外 class 组件通过 Babel 编译出来的代码也相比函数组件多得多。

Hooks带来的改变:

  1. 自定义Hook组件提取 可以轻松做到组件间的状态逻辑的复用;
  2. 真正全面拥抱函数式编程,Hooks的函数式编程可以减少组件的嵌套;
  3. 可以在不编写 class 的情况下使用state以及其他React 特性;
  4. 组件拆分、组合更加方便,编写的代码更加简洁明了,易于维护;

四、Hooks原理

一位大神说过:

到目前为止,我发现的有关于Hooks的最好的心里规则是“写代码时要认为任何值都可以随时更改”。


不要把React Hook想的太神奇。它没有采用“数据绑定”、“监听器”、“代理”等 高深概念;
函数式组件的底层心智模式与Class组件不同,函数式组件捕获了渲染所使用的值。
**函数组件首先是个普通函数,每一次渲染都是函数执行一遍。**函数每一次执行都会生成本次独有的执行上下文, 相对应的,React重新渲染组件时都有它自己独立的变量及函数,包括Props和State 以及它自己的事件处理函数。
其次React Hooks API 赋予了函数内被HooksAPI包裹的某些变量独特的意义:缓存值和函数、值变更触发重渲染等(通过useMemo、useCallback、useEffect等)。

每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state。

就好比电影中放映的每一帧,电影在放映,但当前每一帧是固定的。它们捕捉UI在特定的时间点的状态,它们永远不会再改变。
**
此外涉及的一些常识:

  • 到目前为止,React Hook是为函数组件量身打造的;
  • 在函数组件渲染时Hooks API是依赖于固定顺序调用的,底层通过单向链表维护队列;
  • 实现的源码在react-reconciler库;

五、Hook 使用规则

使用规则

官方文档对于Hook函数的规则描述:

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中)

(一)、只能在函数最外层调用Hooks API;那么原因呢?

React规则的限制,是为了保证React对函数组件的正确重渲染。而
React源码实现中是通过单向链表维护List队列的方式,存储Hook API的调用顺序。
因为函数式组件在每次更新渲染时,函数会重新执行,这时需要保证每次执行时,Hooks API的调用顺序是保持一致的。

在组件首次渲染时,hook依次插入链表之中;
再次渲染时,执行中Hook API,则从之前的链表队列中一一关联对照;
而如果此时在条件判断等逻辑中调用了Hook,当条件不满足时,Hook调用的对应关系就会不一致,从而产生Bug。在下一次函数组件更新,Hooks链表结构将会被破坏。

Hook的操作基本可以分为mount(首次挂载)阶段和update(更新)阶段。在mount阶段,初始化新的Hook队列,添加到fiber流中;在update阶段,则将当前fiber的Hook队列克隆到workInProgressHook。
以下为mount阶段-mountWorkInProgressHook函数生成hook的逻辑:

// React 部分源码
let currentlyRenderingFiber: Fiber = (null: any); // 当前Fiber
let workInProgressHook = null; // 指向当前hook,存储当前hook相关信息

// 初次渲染,每一个自定义hook都会调用 mountWorkInProgressHook 函数
function mountWorkInProgressHook(){
  const hook = {
    memoizedState: null, // 记忆存储的state,
    baseState: null, // 缓存的基准state
    baseQueue: null, // 缓存的基准队列
    queue: null, // 调度的操作队列
    next: null, // 指向下一个hook对象
  };

  if (workInProgressHook === null) {
    // 如果链表为空,则是list队列中的第一个hook,直接添加
    // 且添加到当前fiber的memoizedState中;
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 否则,则向链表尾部增加hook
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;  // 返回当前hook
}

(二)、只能在React的函数组件中调用。

这里问三个问题:

  1. 为什么不能在React的Class组件中使用?
  2. 为什么在其他JS函数中调用无效?
  3. 为什么自定义的 Hook 中又可以使用?

1. 为什么不能在React的Class组件中使用?

React渲染时,判断一个组件是Class组合和函数组件后,是两套不同的处理逻辑,Hooks API只支持函数组件。
Class组件中通过声明周期函数完成,也无法达成第一条准则-只能在最外层函数中调用Hooks API;

2. 为什么在其他JS函数中调用无效?

在 另一篇简单概念 中 提到过:React库只是定义React语法的Api封装,绝大多数的实现都存在于React-Dom等“渲染器”中。实际功能是渲染时通过“依赖注入”的方式加载的。
在React组件之外的其他普通JS函数中,只是引入React.useState()等API 直接调用是无效的。它不是一个React组件,就不会被渲染器识别,未被依赖注入React-DOM渲染器上的dispatcher,自然无法被正常执行。

3. 为什么自定义的 Hook 中又可以使用?

首先,自定义Hook 在函数组件中使用,且跟官方Hook API一样,也必须遵循第一条规则--只能在最外层函数中调用Hooks API; 如此一来,自定义Hook执行后,里面调用的
Hook API 平铺添加到当前函数组件中。再次渲染时,可以满足调用顺序是保持一致。

此外,需要遵从的实践准则有:

  • 你可能不需要派生state,任何数据,都要保证只有一个数据来源,而且避免直接复制它;
  • useMemo, useCallback是作为性能优化的方式存在,不要作为阻止渲染的语义化保证;
  • 一个Hooks函数尽量只做一件事;每个effect内功能不能过于耦合,尽量控制一个effect只做一件事;
  • 代码结构: 功能划分优于结构化划分;逻辑聚合,获取更高的代码可读性;
  • 尽量避免过早地增加抽象逻辑;
  • 善用自定义组件,这是Hook的利器之一;

六、自定义Hooks

自定义Hooks其实就是借助useState、useEffect等基础Hook 封装一些通用组件逻辑并提取到可重用的函数中。
组件解决的是UI复用,自定义组件则是解决状态逻辑复用的目的。

自定义的Hook是如何影响使用它的函数组件的。

自定义组件在执行时与函数式组件 共享hook数组顺序;

Q&A

React是如何区分Class 和Function的呢?

React在Component类的原型上增加了一个标记。这样在继承Component的子类组件,可以通过原型查找的方式判断。

后语

前言要搭后语  


概念理解的越透彻,使用就越顺畅。

我目前在做的就是在持续学习当中总结自己在实践React过程中的所见所学。

本篇文章主要是React Hooks相关知识,涉及的代码优劣在下一篇会专项探讨;