Hook 为王

233 阅读7分钟

⚡️ Why we use Hook

很简单,如果你更偏爱函数式编程,那么 hook 可以完美的解决你的函数式编程洁癖,当前版本的 hook 基本可以完成所有 class 组件的任务,而这带来的优点不仅仅是函数式编程的政治正确。

你不再需要去维护冗长的 class 组件,不用再去关心 this 到底指向哪里,不用再去手动绑定函数的执行上下文,不用手动去控制复杂的生命周期,class 的减少也会大大减少项目打包时候的处理时间以及最终打包大小。最重要的是 hook 彻底改变了以往的编程思维,函数式组件不再是只负责数据展示的展示组件,他也能担当起复杂逻辑处理的任务。

而且 hook 可以很好的解决组件之间状态逻辑的复用,这在多数后台项目中是十分常见的功能,因为很可能不同的业务组件他们拥有着相同的状态逻辑,这有一点类似于面向对象语言中的继承。之前我们的解决方案可能就是通过高阶组件,也就是使用组合来实现继承,这在逻辑上并没有太大的问题,问题在于组件的“嵌套地狱”,会让你的调试过程十分头痛。而现在你可以通过自定义 hook 来完成逻辑状态的提取和复用,这在很多流行的库( Material UI , React Hook Form )中都已经有了很完美的解决方案。

📚 How Hook work

关于 hook 的使用,官网有这样一个规定

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

关于这样规定的原因,官网也给出了解释。 React 通过 hook 的调用顺序来保证多个 hook 的对应关系一致,也就是说如果想要保证 hook 按照预期的顺序执行,你必须保证 hook 的每一次执行顺序不会发生改变,和初始化的顺序一致。读到这里你会不会意识到什么呢,为什么同样是函数,同样有自己的标示,为什么顺序一旦改变 hook 就不能找到自己定义的 state 了呢。

hook 的这个特点非常符合线性存储结构的特点,而且不支持随机访问,只能够从头走到尾。那么我们就可以猜到了,对于 fiber 架构下的 hook 实现,也是基于线性数据结构保存的。有很多文章通过数组和下标来解释 hook ,但是其实 React 的内部实现是通过单链表的,我觉得理解由于单链表数据结构而带来的 hook 规则要比数组更易懂,而且你在 hook 的数据结构体系中也会看见我们的老朋友 fiber。

开始之前,你听过 Dispatcher 么

没错,就是那个你在 Redux 中随处可见的 Dispatcher ,这可能是个很明显的社区反哺官方的例子,React hook 中也采用了 reducer + Dispatcher 的方案来围护 hook 。 它的第一个作用就对应着上边的第二条规则,不要在非 React 组件中使用 hook,Dispatcher 会很及时的给出一个警告。

而他的最主要功能也和 Redux 中的相同,那就是分发 reducer 来反馈 action。在 hook 里是当 hook 被调用的时候通过他来分发 hook 队列。而这些 hook 队列一般都保存在哪里呢,他们要和组件捆绑在一起,你想到了什么,没错,那就是 fiber。

VrdfOolqFWbH5JG.png

先来看看 State hook

我们要知道,一个合格的 hook 应该有以下特点:

  • 它的初始状态会在初次渲染的时候被创建。
  • 它的状态可以在运行时更新。
  • React 可以在后续渲染中记住 hook 的状态。
  • React 能根据调用顺序提供给你正确的状态。
  • React 知道当前 hook 属于哪个 fiber。

来看 hook 的真实面纱:

{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'bar',
      next: null
    }
  }
}

没错,关键的两个属性使得 hook 以单链表的形式存储。 而这个 hook 队列怎么连接到 fiber 上呢,只需要把链表的头交给 fiber 管理就好了。

当一个函数组件被调用的时候,当前 fiber 和 fiber 保存的 hook 队列的头节点会保存在执行上下文中,没错,就是你常见的闭包。

这样保证了你的函数组件拥有了保存 state 的能力。也就意味着,如果你要把 hook 和传统的 class 组件拉出来 pk 一下的话,那就应该是这样的。

class 组件在创建后就一直存在,因此你的 state 和生命周期管理都在内部完成。

hook 为函数组件赋予了保存 state 的能力,利用的机制是闭包,保证了每一次运行的时候都重新生成的函数还能找到自己的 state 。

充满魔法的 Effect hook

首先来了解一下 Effect hook 具有的特点:

  • 执行副作用操作,在每次生命周期更新的时候都触发
  • 可以清除 effect 操作,如在组件渲染完成或卸载的时候移除订阅源
  • 清除操作通过 return 一个函数来执行,React 会在应当执行他的时候执行他。(这也太智能了)
  • 通过对比第二个参数(是个数组)来跳过 Effect 进行性能优化,如果数组内容不变,那就不执行这次 Effect。

这里有个小技巧,如果你需要一个只运行一次的 effect ,仅在组件挂载和卸载时执行,那就传递一个空数组给第二个参数,它会让 effect 跳过除了第一次的所有 effect 。 如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入[]作为第二个参数更接近大家更熟悉的componentDidMountcomponentWillUnmount思维模式,但我们有 更好的 方式 来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用useEffect,因此会使得额外操作很方便。

而这些特点与我们要讲的有关的是这三个:

  • 它们在渲染(Fiber 节点建立)时被创建,但是在浏览器绘制(Fiber 执行 commit 反馈到浏览器绘制)运行。
  • 如果给出了销毁指令,它们将在下一次绘制前被销毁。
  • 它们会按照定义的顺序被运行。

如果你了解 React 的更新机制的话,你也会很熟悉这些东西,这些东西定义在 fiber 的 updateQuene 上面。和生命周期那个是同一个数据结构。长成这个样子:

{
	tag: 0b11000000, 
	next: lastEffect.next, 
	create: createEffect, 
	destroy: destroyEffect, 
	inputs: [createEffect],
}

没错,这也是个链表(人家都写着叫 Quene 了),create 和 destroy 就定义了特定生命周期应当执行的操作,而 Effect hook 的实现原理就是把保存 Effect hook 的那个队列合并到 fiber 上来,这样 fiber 在更新的时候就会去调用指定的方法。 实际跟 class 中的生命周期钩子函数是一个道理,而 hook 的那些魔法功能实际上是来自于他自身的优化。也就是 useEffect 才是真正的实现清除 effect 和跳过 effect 的地方。

关于怎么实现的,源码里头有,自己看。🤣

Reference

Hook 简介 – React

译 深入 React Hook 系统的原理 - 掘金

译 React hooks: 不是魔法,只是数组 - 知乎