第三章 钩入React 【上】

在前面的章节,我们学习了在函数组件内通过自定义构建的状态来执行操作。在这一章,在本章中,我们将深入探讨在构建优质状态解决方案时所面临的挑战,随后将了解 React 是如何借助底层的 Hook 来构建这一解决方案的。之后,我们要引入钩子的概念,理解其调用顺序的重要性,并理解为什么要避免条件性钩子函数。在本章的附录部分,会讨论两个额外主题: React Fiber 与 Current , 和 WorkInProgress 场景

本章会讲解这些主题

  • 创造一个好的状态方案
  • 介绍一个React钩子
  • 什么是钩子?
  • 答疑环节
  • 附录

创造一个好的状态方案

状态(States)太他妈重要了。一个没有状态(states)组件,就好比一个 没有了变量的函数。这样的组件会失去推理、运算的能力。一个UI组件正是因为有了状态(states),才能够与用户的行为产生交互。

在上一章中,我们构建了一个自定义状态(方案),代码如下:

js 复制代码
let states = {}
function _getM2(initialValue, key) {
    if (states[key] === undefined) {
        states[key] = initialValue
    }
    return states[key]
}

function _setM2(v, key) {
    states[key] = v
    ReactDOM.render(<Title />, rootEl)
}

虽然这个方案可以跑起来,但是有几个问题我们要先抛出来。

首先,这个 states 对象 要放在哪里是一个 重要的问题:

js 复制代码
let states = {}

在先前的代码中,states 对象是作为一个全局对象而存在的,但是我们通常更关心的是状态(states) 如何 与 组件 发生关联。换言之,我们需要发现一个地方来定义 本地状态(states)。

第二个问题是,我们如何得到每一个状态的唯一索引:

js 复制代码
const a = _getM2(0, 'comp_a');

正如在之前的状态使用方式中那样,当我们将状态命名为 comp_a 后,只要是涉及该状态的任何操作,都必须携带这个键(comp_a)。在一个典型的应用中,我们可能会有大量此类状态;如果每个状态都必须用一个唯一的字符串来定义,我们就得想出很多不重复的名称。要记录所有已使用过的名称会是相当繁琐的工作,更不用说函数组件内部存储该状态的变量本身已经有了一个名称(比如这里的 a)。同时存在变量名和键字符串,不免有些累赘。

除了这两个 大问题外,还有一个小问题需要关注。在刚刚的事例中,每当我们更新状态时,这个Title组件就要渲染。

对于开发者而言,知道每一次操作会更新哪些组件,是有一定难度的。如果React 引擎能自动帮我们识别要更新的组件,这是最好的。而这,正是React 做的最好的;我们应该借助引擎的这一能力来写代码,以更好地更新组件。最后但同样重要的是,我们知道状态(states)可用于实现不同的功能 ------ 其底层核心逻辑本就是一种 "数据持久化机制"。如果设计得当,我们完全可以基于这一机制搭建某种基础架构,进而在其上扩展更多附加功能。

介绍一个React 钩子

状态对于一个组件来说,是本地的。把状态放在组件的实例上,是符合直觉的,因为一个 React 组件 是 一个 UI的定义。那么,对于一个函数组件来说,其实例应该被存储在哪里。

实际上,组件并不是React的 最小单元。有一个更具颗粒度的架构------fiber。fiber 是用来代表一个元素(element)的。一个 fiber 会执行 与之对应的 元素(element)的所有任务。这个 元素 可以只是简单的原生 h1,div 元素,也可以是自定义组件。比如说,"fragment"元素可以下含一系列子元素,而不展示 "fragment"自身;一个memo元素,可以存储上一次更新 该 element的结果。

事实上,一个函数组件正是一个 fiber 所代表的自定义元素。一个 函数组件做的,不过是允许我们 定义什么元素可以被展示,所以,无论何时这个 函数被调用,它可以知道哪些 DOM 元素需要展示在 屏幕上。更多的细节,你可以在附录 A ------ React Fiber部分了解到。

现在,我们知道了一个组件实例所对应的单元:而这个单元,正是React 存储状态的地方。React 会把这些状态 存储在 名为 memoizedState 的属性上, 而这个 属性又具有 Hook 结构:

我们在这里引入的 Hook 是一个 用于 存储 状态的 结构(你要说是 class 也行)。这个 Hook 并不是我们后面要介绍的 React hook(函数)。但麻烦的是,React在这两个地方 用了同一个名字。为了方便区分,我们会用 Hook 来代表一个 数据结构,而hook 来代表 函数。

ts 复制代码
interface Hook {
    memoizedState: any;
    updateQueue: unknown;
    baseState: any;
    baseQueue: Update<any> | null;
    next: Hook | null;
}

Hook结构的主要功能,是通过 state 属性来存储当个状态。React不再是将多个状态存放在一个数组(或对象)中,而是通过链表(linked list)将这些状态相互关联起来,如下图。一个 Hook 对象 通过 next指向另一个 Hook 对象。当到最后一个Hook 对象时,其 next指向 为 null。

要让(React)引擎知晓界面是否存在变化,就需要对 Fiber 节点进行更新。而在 update,正是 Hooks 初始化的地方。接下来,我们看看更新函数。

更新一个函数组件

React 是 通过 updateFunctionComponent 函数 来更新 函数组件的。这个函数有两个参数 Componentprops

js 复制代码
let updatingFiber = ...
function updateFunctionComponent(Component, props) {
    prevHook = null
    let children = Component(props)
}

这个更新函数的 主要作用是 调用Component(props) 来获得 children 元素。以 Title 组件为例,当它被更新时,会 调用 函数 Title()。得到了 children 元素后,React 引擎会把这个元素 和 屏幕上的元素进行对比,并渲染 需要更新的部分。

在刚刚的更新函数中,有两个全局变量。它们是 updatingFiberprevHookupdatingFiber代表的是React 引擎正在处理的 当前 fiber,而 prevHook 代表的是 这个 fiber 之前在 使用的 Hook 对象。在这个组件被调用前, updatingFiber 已经被引擎生成了。

组件首次更新时(比如在挂载阶段),就是该 Fiber 节点的第一个 Hook 被创建的时刻。

在挂载时创建一个 Hook 对象

为了在当前fiber 上 生成一个 Hook对象,React 会 生成 一个 新的 Hook 对象,并把它 插入到 链表里:

js 复制代码
function mountHook() {
    const Hook = {
        state: null,
        next: null
    }
    if (prevHook === null) {
        updatingFiber.memoizedState = Hook;
        prevHook = Hook
    } else {
        prevHook.next = Hook;
        prevHook = prevHook.next;
    }
    
    return Hook;
}

在前面提到的 mountHook 函数中,首先会分配一个空的 Hook 对象,其 statenext 属性都被设为 null。如果这是该 Fiber 节点的第一个 Hook(此时 preHooknull),它会被存储在 Fiber 的 memoizedState 属性下;否则,它会被附加到前一个 Hook 的 next 属性上。之后,这个被分配的 Hook 会被返回。

在更新时获取一个Hook对象

当挂载完成后,我们可以访问React 在挂载时所生成的 Hook对象们。

js 复制代码
function updateHook() {
    var Hook;
    if (prevHook === null) {
        Hook = updatingFiber.memoizedState;
    } else {
        Hook = prevHook.next;
    }
    prevHook = Hook;
    return Hook;
}

在当前的 updateHook 函数里,Hook 对象 是通过 查找updatingFibermemoizedStatenext属性来实现的。而 prevHook也会实时更新。这就是 Hook的获取方式。

使用一个钩子

现在,我们已经构建了一个 与所有更新 同步的 钩子,我们可以在一个 函数组件里调用它,就像先前调用_getM_getM2 那样。

现在,我们来实现一个 可以 接收 初始值的 _useHook 函数:

js 复制代码
function _useHook(initialState) {
    let Hook;
    if (isFiberMounting) {
        Hook = mountHook();
        Hook.state = initialState
    } else {
        Hook = updateHook()
    }
    return Hook.state
}

通过 isFiberMounting 标志判断组件是否处于挂载阶段后,前面提到的 _useHook 函数会获取一个持久化的 Hook。若处于挂载阶段,React 会将 initialState(初始状态)赋值给该 Hook;对于其他任何更新阶段,该 Hook 不会被修改。在所有情况下,最终都会返回该 Hook 中存储的状态。

你可能会好奇 React 是如何确定 isFiberMounting 标志的 ------ 由于这部分逻辑与引擎(React 核心机制)的关联更为深入,我们将相关内容放在了本章末尾的附录 B《Current 与 WorkInProgress 场景》中。

到目前为止,我们已经梳理了 React 引擎底层是如何实现 Hook 的。我们刚刚攻克了最复杂的部分,现在就让我们来看看如何使用它。

相关推荐
Holin_浩霖2 小时前
为什么typeof null 返回 "object" ?
前端
PanZonghui3 小时前
Zustand 实战指南:从基础到高级,构建类型安全的状态管理
前端·react.js
PanZonghui3 小时前
Vite 构建优化实战:从配置到落地的全方位性能提升指南
前端·react.js·vite
_extraordinary_3 小时前
Java Linux --- 基本命令,部署Java web程序到线上访问
java·linux·前端
用户1456775610373 小时前
推荐一个我私藏的电脑神器:小巧、无广、功能强到离谱
前端
用户1456775610373 小时前
终于找到了!一个文件搞定PDF阅读
前端
liangshanbo12153 小时前
React 18 的自动批处理
前端·javascript·react.js
一位搞嵌入式的 genius3 小时前
前端实战开发(二):React + Canvas 网络拓扑图开发:6 大核心问题与完整解决方案
前端·前端框架
da_vinci_x3 小时前
设计稿秒出“热力图”:AI预测式可用性测试工作流,上线前洞察用户行为
前端·人工智能·ui·设计模式·可用性测试·ux·设计师