[译] 深刻 React Hook 系统的原理

React Hook 系统的原理

咱们将会一块儿查看它的实现方法,由内而外地学习 React Hook。html

咱们都已经据说过了:React 16.7 的新特性,hook 系统,并已在社区中激起了热议。咱们都试用过、测试过,对它自己和它的潜力都感到很是兴奋。你必定认为 hook 如魔法般神奇,React 居然能够在不暴露实例的状况下(不须要使用 this 关键字),帮助你管理组件。那么 React 到底是怎么作到的呢?前端

那么今天,让咱们一块儿深刻探究 React Hook 的实现方法,以便更好的理解它。可是,它的各类神奇特性的不足是,一旦出现问题,调试很是困难,这是因为它的背后是由复杂的堆栈追踪(stack trace)支持的。所以,经过深刻学习 React 的新特性:hook 系统,咱们就能比较快地解决遇到的问题,甚至能够直接杜绝问题的发生。react

在开始讲解以前,我先声明我不是 React 的开发者或者维护者,因此个人理解可能也并非彻底正确。我确实很是深刻地研究过了 React 的 hook 系统的实现,可是不管如何我仍没法保证这就是 React 实际的工做方式。话虽如此,我仍是会用 React 源代码中的证据和引用来支持个人文章,使个人论点尽量坚实。android

React hook 系统概要示意图ios


咱们先来了解 hook 的运行机制,并要确保它必定在 React 的做用域内使用,由于若是 hook 不在正确的上下文中被调用,它就是毫无心义的,这一点你或许已经知道了。git

Dispatcher

dispatcher 是一个包含了 hook 函数的共享对象。基于 ReactDOM 的渲染状态,它将会被动态的分配或者清理,而且它可以确保用户不可在 React 组件以外获取 hook(详见源码)。github

在切换到正确的 Dispatcher 以渲染根组件以前,咱们经过一个名为 enableHooks 的标志来启用/禁用 hook。在技术上来讲,这就意味着咱们能够在运行时开启或关闭 hook。React 16.6.X 版本中也有对此的实验性实现,但它实际上处于禁用状态(详见源码json

当咱们完成渲染工做后,咱们将 dispatcher 置空并禁止用户在 ReactDOM 的渲染周期以外使用 hook。这个机制可以保证用户不会作什么蠢事(详见源码)。后端

dispatcher 在每次 hook 的调用中都会被函数 resolveDispatcher() 解析。正如我以前所说,在 React 的渲染周期以外,这些都无心义了,React 将会打印出警告信息:“hook 只能在函数组件内部调用”(详见源码)。设计模式

let currentDispatcher
const dispatcherWithoutHooks = { /* ... */ }
const dispatcherWithHooks = { /* ... */ }

function resolveDispatcher() {
  if (currentDispatcher) return currentDispatcher
  throw Error("Hooks can't be called")
}

function useXXX(...args) {
  const dispatcher = resolveDispatcher()
  return dispatcher.useXXX(...args)
}

function renderRoot() {
  currentDispatcher = enableHooks ? dispatcherWithHooks : dispatcherWithoutHooks
  performWork()
  currentDispatcher = null
}
复制代码

dispatcher 实现方式概览。


如今咱们简单了解了 dispatcher 的封装机制,下面继续回到本文的核心 —— hook。下面我想先给你介绍一个新的概念:

hook 队列

在 React 后台,hook 被表示为以调用顺序链接起来的节点。这样作缘由是 hook 并不能简单的被建立而后丢弃。它们有一套特有的机制,也正是这些机制让它们成为 hook。一个 hook 会有数个属性,在继续学习以前,我但愿你能牢记于心:

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

另外,咱们也须要从新思考看待组件状态的方式。目前,咱们只把它看做一个简单的对象:

{
  foo: 'foo',
  bar: 'bar',
  baz: 'baz',
}
复制代码

旧视角理解 React 的状态

可是当处理 hook 的时候,状态须要被看做是一个队列,每一个节点都表示一个状态模型:

{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'bar',
      next: null
    }
  }
}
复制代码

新视角理解 React 的状态

单个 hook 节点的结构能够在源码中查看。你将会发现,hook 还有一些附加的属性,可是弄明白 hook 是如何运行的关键在于它的 memoizedStatenext 属性。其余的属性会被 useReducer() hook 使用,能够缓存发送过的 action 和一些基本的状态,这样在某些状况下,reduction 过程还能够做为后备被重复一次:

  • baseState —— 传递给 reducer 的状态对象。
  • baseUpdate —— 最近一次建立 baseState 的已发送的 action。
  • queue —— 已发送 action 组成的队列,等待传入 reducer。

不幸的是,我尚未彻底掌握 reducer 的 hook,由于我没办法复现它任何的边缘状况,因此讲述这部分就很困难。我只能说,reducer 的实现和其余部分相比显得很不一致,甚至它本身源码中的注解都声明“不肯定这些是不是所须要的语义”;因此我怎么可能肯定呢?!

因此咱们仍是回到对 hook 的讨论,在每一个函数组件调用前,一个名为 [prepareHooks()](https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123) 的函数将先被调用,在这个函数中,当前 fiber 和 fiber 的 hook 队列中的第一个 hook 节点将被保存在全局变量中。这样,咱们不管什么时候调用 hook 函数(useXXX()),它都能知道运行上下文。

let currentlyRenderingFiber
let workInProgressQueue
let currentHook

// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123
function prepareHooks(recentFiber) {
  currentlyRenderingFiber = workInProgressFiber
  currentHook = recentFiber.memoizedState
}

// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148
function finishHooks() {
  currentlyRenderingFiber.memoizedState = workInProgressHook
  currentlyRenderingFiber = null
  workInProgressHook = null
  currentHook = null
}

// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:115
function resolveCurrentlyRenderingFiber() {
  if (currentlyRenderingFiber) return currentlyRenderingFiber
  throw Error("Hooks can't be called")
}
// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:267
function createWorkInProgressHook() {
  workInProgressHook = currentHook ? cloneHook(currentHook) : createNewHook()
  currentHook = currentHook.next
  workInProgressHook
}

function useXXX() {
  const fiber = resolveCurrentlyRenderingFiber()
  const hook = createWorkInProgressHook()
  // ...
}

function updateFunctionComponent(recentFiber, workInProgressFiber, Component, props) {
  prepareHooks(recentFiber, workInProgressFiber)
  Component(props)
  finishHooks()
}
复制代码

hook 队列实现的概览。

一旦更新完成,一个名为 [finishHooks()](https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148) 的函数将会被调用,在这个函数中,hook 队列中第一个节点的引用将会被保存在已渲染 fiber 的 memoizedState 属性中。这就意味着,hook 队列和它的状态能够在外部定位到。

const ChildComponent = () => {
  useState('foo')
  useState('bar')
  useState('baz')

  return null
}

const ParentComponent = () => {
  const childFiberRef = useRef()

  useEffect(() => {
    let hookNode = childFiberRef.current.memoizedState

    assert(hookNode.memoizedState, 'foo')
    hookNode = hooksNode.next
    assert(hookNode.memoizedState, 'bar')
    hookNode = hooksNode.next
    assert(hookNode.memoizedState, 'baz')
  })

  return (
    <ChildComponent ref={childFiberRef} /> ) } 复制代码

从外部读取某一组件记忆的状态


下面咱们来分类讨论 hook,首先从使用最普遍的开始 —— state hook:

State hook

你必定会以为很吃惊:useState hook 在后台使用了 useReducer,而且它将 useReducer 做为预约义的 reducer(详见源码)。这意味着,useState 返回的结果实际上已是 reducer 状态,同时也是一个 action dispatcher。请看,以下是 state hook 使用的 reducer 处理器:

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}
复制代码

state hook 的 reducer,又名基础状态 reducer。

因此正如你想象的那样,咱们能够直接将新的状态传入 action dispatcher;可是你看到了吗?!咱们也能够传入 action 函数给 dispatcher,这个 action 函数能够接收旧的状态并返回新的。(在本篇文章写就时,这种方法并无记录在 React 官方文档中,很遗憾的是,它其实很是有用!)这意味着,当你向组件树发送状态设置器的时候,你能够修改父级组件的状态,同时不用将它做为另外一个属性传入,例如:

const ParentComponent = () => {
  const [name, setName] = useState()
  
  return (
    <ChildComponent toUpperCase={setName} /> ) } const ChildComponent = (props) => { useEffect(() => { props.toUpperCase((state) => state.toUpperCase()) }, [true]) return null } 复制代码

根据旧状态返回新状态。


最后,effect hook —— 它对于组件的生命周期影响很大,那么它是如何工做的呢:

effect hook

effect hook 和其余 hook 的行为有一些区别,而且它有一个附加的逻辑层,这点我在后文将会解释。在我分析源码以前,首先我但愿你牢记 effect hook 的一些属性:

  • 它们在渲染时被建立,可是在浏览器绘制运行。
  • 若是给出了销毁指令,它们将在下一次绘制前被销毁。
  • 它们会按照定义的顺序被运行。

注意,我使用了“绘制”而不是“渲染”。它们是不一样的,在最近的 React 会议中,我看到不少发言者错误的使用了这两个词!甚至在官方 React 文档中,也有写“在渲染生效于屏幕以后”,其实这个过程更像是“绘制”。渲染函数只是建立了 fiber 节点,可是并无绘制任何内容。

因而就应该有另外一个队列来保存这些 effect hook,而且还要可以在绘制后被定位到。一般来讲,应该是 fiber 保存包含了 effect 节点的队列。每一个 effect 节点都是一个不一样的类型,并能在适当的状态下被定位到:

  • 在修改以前调用 getSnapshotBeforeUpdate() 实例(详见源码)。

  • 运行全部插入、更新、删除和 ref 的卸载(详见源码)。

  • 运行全部生命周期函数和 ref 回调函数。生命周期函数会在一个独立的通道中运行,因此整个组件树中全部的替换、更新、删除都会被调用。这个过程还会触发任何特定于渲染器的初始 effect hook(详见源码)。

  • useEffect() hook 调度的 effect —— 也被称为“被动 effect”,它基于这部分代码(也许咱们要开始在 React 社区内使用这个术语了?!)。

hook effect 将会被保存在 fiber 一个称为 updateQueue 的属性上,每一个 effect 节点都有以下的结构(详见源码):

  • tag —— 一个二进制数字,它控制了 effect 节点的行为(后文我将详细说明)。
  • create —— 绘制以后运行的回调函数。
  • destroy —— 它是 create() 返回的回调函数,将会在初始渲染运行。
  • inputs —— 一个集合,该集合中的值将会决定一个 effect 节点是否应该被销毁或者从新建立。
  • next —— 它指向下一个定义在函数组件中的 effect 节点。

除了 tag 属性,其余的属性都很简明易懂。若是你对 hook 很了解,你应该知道,React 提供了一些特殊的 effect hook:好比 useMutationEffect()useLayoutEffect()。这两个 effect hook 内部都使用了 useEffect(),实际上这就意味着它们建立了 effect hook,可是却使用了不一样的 tag 属性值。

这个 tag 属性值是由二进制的值组合而成(详见源码):

const NoEffect = /* */ 0b00000000;
const UnmountSnapshot = /* */ 0b00000010;
const UnmountMutation = /* */ 0b00000100;
const MountMutation = /* */ 0b00001000;
const UnmountLayout = /* */ 0b00010000;
const MountLayout = /* */ 0b00100000;
const MountPassive = /* */ 0b01000000;
const UnmountPassive = /* */ 0b10000000;
复制代码

React 支持的 hook effect 类型

这些二进制值中最经常使用的情景是使用管道符号(|)链接,将比特相加到单个某值上。而后咱们就可使用符号(&)检查某个 tag 属性是否能触发一个特定的行为。若是结果是非零的,就表示能够。

const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)
复制代码

如何使用 React 的二进制设计模式的示例

这里是 React 支持的 hook effect,以及它们的 tag 属性(详见源码):

  • Default effect —— UnmountPassive | MountPassive.
  • Mutation effect —— UnmountSnapshot | MountMutation.
  • Layout effect —— UnmountMutation | MountLayout.

以及这里是 React 如何检查行为触发的(详见源码):

if ((effect.tag & unmountTag) !== NoHookEffect) {
  // Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
  // Mount
}
复制代码

React 源码节选

因此,基于咱们刚才学习的关于 effect hook 的知识,咱们能够实际操做,从外部向 fiber 插入一些 effect:

function injectEffect(fiber) {
  const lastEffect = fiber.updateQueue.lastEffect

  const destroyEffect = () => {
    console.log('on destroy')
  }

  const createEffect = () => {
    console.log('on create')

    return destroy
  }

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

  lastEffect.next = injectedEffect
}

const ParentComponent = (
  <ChildComponent ref={injectEffect} /> ) 复制代码

插入 effect 的示例


这就是 hooks 了!阅读本文你最大的收获是什么?你将如何把新学到的知识应用于 React 应用中?但愿看到你留下有趣的评论!

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索