第六章 :介绍全局状态管理库

目前为止,我们已经学习了几个共享状态的模式了。这本书剩下的内容,会介绍使用了这些模式的各种库。在深入这些库之前,我们会回顾全局状态会遇到的挑战,以及两大主题:状态应该存在哪里 和 如何控制 重新 渲染。有了这些知识,我们将能够理解全局状态库的特点。

在这一章,我们会讨论这几个主题:

  • 使用全局状态库遇到的问题
  • 使用 数据中心方法 和 组件中心 方法
  • 优化重新渲染

使用全局状态库遇到的问题

React是基于组件的概念而设计的。在组件模型中,我们希望任何事物都可以复用。而全局状态,是存在于组件之外的。通常情况下,我们确实应该尽可能避免使用全局状态,因为这会使组件额外依赖于某个东西。然而,全局状态有时非常实用,能让我们的工作效率更高。对于某些应用程序的需求而言,全局状态是很适用的。

在设计一个全局状态时,有两个主要挑战:

  • 第一个是如何读取一个全局状态

一般来说,全局状态都有多个值。一般情况下,一个组件只需要只需要全局状态的一个值,并不需要全局状态全部的值。如果一个组件因为无关的全局状态的变化,而真实使用的全局状态没有发生变化,而重新渲染,我们称这为额外的重新渲染。我们不希望遇到额外的重新渲染,而全局状态管理库应该为之提供解决方案。有几种方法可以避免额外的重新渲染。在 优化重新渲染 部分,我们会讨论几个避免 额外重新渲染的方法。

  • 第二个是如何更新一个全局状态

全局状态都有多个值,而且有些值可能嵌套在对象里。拥有一个单一的全局变量并允许任意的变更操作,这可能不是一个好主意。下面的代码块展示了一个全局变量以及一次任意变更操作的示例:

js 复制代码
let globalVariable = {
    a: 1,
    b: {
        c: 2,
        d: 3,
    },
    e: [4, 5, 6],
}

在上述例子中,对全局变量的变更操作 globalVariable.b.d = 9 对于全局状态而言可能不起作用,因为没有办法检测到这种变化并触发 React 组件进行重新渲染。

js 复制代码
const createContainer = () => {
    let state = { a: 1, b: 2 };
    const getState = () => state;
    const setState = (...) => { ... };
    return { getState, setState };
}

const globalContainer = createContainer();
globalContainer.setState(...);

createContainer 生成了globalContainer,而globalContainergetState函数 和 setState函数。getState函数专门读取全局状态,而 setState函数 用于 更新全局状态。有好几个方法可以实现一个setState函数。我们在未来几章节会看到这些方法的具体实现。

使用 数据中心方法 和 组件中心 方法

从技术层面上讲,全局状态可以分为两种类型:以数据为中心的和以组件为中心的。

在接下来的章节中,我们将详细讨论这两种方式。然后,我们也会谈到一些特殊情况。

理解数据中心方法

当你在设计一款应用时,你也许有一些数据模型作为全局的单例而存在。在这种情况下,你需要定义组件,并将数据与这些组件连接起来。数据可能会从外部被修改,例如由其他库进行修改,或者来自其他服务器的修改。

对于 数据中心 方法来说,模块状态会更合适,因为模块状态本质上是处于 React之外的 一段JS内存。模块状态可以先于React启动渲染之前存在,甚至在React组件被卸载后依然存在。

使用了 数据中心 方法的 全局状态管理库,会提供用于创建模块状态的API,和用于连接模块状态和 React 组件的API。模块状态通常被包裹在 store 对象。而 store 对象,有方法用于 访问 和 更改 状态 对象。

理解组件中心方法

与 数据中心方法不同,组件中心方法,允许你先设计组件。在某些时候,一些组件需要访问 一些共享信息。如我们在第二章 讨论的,我们可以传递props的方法来实现 共享。如果不使用 props,我们就需要引入全局状态了。当然了,我们也可以先设计数据模型,但是对于 组件中心方法来说,数据模型与组件是紧密相关的。

对于以组件为中心的方法,在组件生命周期中保存全局状态的组件状态更为适用。这是因为当所有相应的组件都被卸载时,全局状态也会随之消失。这种特性使我们能够在 JavaScript 内存中拥有两个或更多的全局状态,因为它们存在于不同的组件子树(或不同的传送门)中。

使用了 组件中心 方法 的 库,会提供一个 给React 组件使用的 用于 创建初始时的全局状态的 工厂函数。这个工厂函数并不会 直接 创建 一个 全局 状态。但使用其 所生成 的 函数,我们让 React可以在其生命 周期中 处理一个全局状态。

探索这两个方法的例外情况

我们刚刚讨论的,都是典型用例,这两种方法都会有例外情况。数据中心 方法 和 组件中心 方法 并不是 一个 硬币的 两面。在实际开放场景中,我们可以使用 其在一个方法, 或者 组合使用 这 两个 方法。

组件状态通常被用于为一个子树提供状态,但是,如果你把provider函数放在了组件的根部,那它在JS的记忆中只有一个内存,那其实这和单例模式一样了。

组件状态通常由 useState 钩子来实现,但是如果我们需要 有一个 可变 变量 或者 store,那用 useRef钩子来实现也是可以的。这种实现也许比用 useState 更复杂,但是它依然 处于 组件的 生命周期 中。

在这个部分,我们学习了两个用于实现 全局状态的方法。模块状态通常用于 数据中心方法,而组件状态通常 用于 组件中心方法。接下来,我们要讨论几个 用于 优化 重新 渲染的 方法。

优化重新渲染

在处理全局状态时,避免 额外重新渲染 是一个 主要挑战。这是设计 React 全局状态管理库时,要考虑的问题。

一般来说,全局状态有多个值,有的值还会嵌套在对象里。看看下面这个例子:

js 复制代码
let state = {
    a: 1,
    b: { c: 2, d: 3 },
    e: { f: 4, g: 5}
}

我们在用为代码实现一下ComponentAComponentB

js 复制代码
const ComponentA = () => {
    return <>value: {state.b.c}</>
}

const ComponentB = () => {
    return <>value: {state.e.g}</>
}

现在,假设我们改变了状态:

js 复制代码
++state.a;

我们只改变了a,没有改变state.b.c 或者 state.e.g。在这种情况下,这两个组件都不需要重新渲染。

优化 组件重新渲染的目的,就是明晰 哪部分的 state在 一个组件被使用了。我们有几个方法可以 明晰 哪部分的 状态。这个部分会讲这三个方法:

  • 使用selector函数
  • 检测属性访问
  • 使用atoms

我们会逐个讨论这些方法。

使用 selector函数

selector方法,以一个状态为参数,并返回这个状态的某个部分。

比如说,假设我们有一个useSelector钩子,这个钩子以一个selector函数为参数,并返回这个状态的某部分:

js 复制代码
const Component = () => {
    const value = useSelector((state) => state.b.c);
    return <>{value}</>;
}

如果state.b.c为2,这个组件会展示2.现在,我们知道这个组件只关心state.b.c,这样我们就可以避免因其他状态变化而引起的重新渲染。

每次状态发生变化时,useSelector 都会用于比较选择器函数的返回结果。因此,当选择器函数接收到相同的输入时,返回引用相等的结果至关重要。

selector函数非常灵活,我们甚至可以用它获取衍生的值:

js 复制代码
const Component = () => {
    const value = useSelector((state) => state.b.c * 2);
    return <>{value}</>;
}

由于选择器函数是一种明确指定组件将使用状态中哪一部分的方式,所以我们将其称为一种手动优化方式。

检测属性访问

我们可以 不使用selector 函数 声明 要获取哪个部分 的状态,自动地优化重新渲染吗?有一种被称为状态使用情况跟踪的机制,它用于检测属性的访问情况,并利用检测到的信息来进行渲染优化。 例如,假设我们有一个 useTrackedState 钩子函数,它具备状态使用情况跟踪的能力:

js 复制代码
const Component = () => {
    const trackedState = useTrackedState();
    return <p>{trackedState.b.c}</p>
}

这个trackedState可以访问.b.c属性,而useTrackedState只会在.b.c属性发生变化时,才触发重新渲染。我们称呼这种方式为自动化渲染优化,而useSelector则是手动渲染优化。

为简单起见,前面代码块的示例是人为设计的。这个示例可以很容易地通过 useSelector(手动渲染优化方式)来实现。让我们来看另一个使用两个值的示例:

js 复制代码
const Component = () => {
    const trackedState = useTrackedState();
    return (
        <>
            <p>{trackedState.b.c}</p>
            <p>{trackedState.e.g}</p>
        </>
    );
};

与使用useSelector相比,useTrackedState使用起来就简单多了。我们不用再去写一个能获取准确值的selector函数。

useTrackedState是基于Proxy来实现的(developer.mozilla.org/enUS/docs/W...%25E3%2580%2582%25E4%25BD%25BF%25E7%2594%25A8Proxy%25EF%25BC%258C%25E5%258F%25AF%25E4%25BB%25A5%25E6%258B%25A6%25E6%2588%25AA%25E9%2592%2588%25E5%25AF%25B9%25E9%2592%2588%25E5%25AF%25B9%25E7%258A%25B6%25E6%2580%2581%25E5%25AF%25B9%25E8%25B1%25A1%25E7%259A%2584%25E5%25B1%259E%25E6%2580%25A7%25E8%25AE%25BF%25E9%2597%25AE%25E3%2580%2582%25E5%25A6%2582%25E6%259E%259C%25E8%25BF%2599%25E4%25B8%25AA%25E9%2592%25A9%25E5%25AD%2590%25E8%25A2%25AB%25E5%2590%2588%25E7%2590%2586%25E5%259C%25B0%25E5%25AE%259E%25E7%258E%25B0%25EF%25BC%258C%25E5%25AE%2583%25E5%258F%25AF%25E4%25BB%25A5%25E4%25BB%25A3%25E6%259B%25BF%25E5%25A4%25A7%25E9%2583%25A8%25E5%2588%2586%2560useSelector%2560%25E7%259A%2584%25E4%25BD%25BF%25E7%2594%25A8%25E5%259C%25BA%25E6%2599%25AF%25E4%25BA%2586%25E3%2580%2582%25E7%2584%25B6%25E8%2580%258C%25EF%25BC%258C%25E5%259C%25A8%25E6%259F%2590%25E4%25BA%259B%25E6%2583%2585%25E5%2586%25B5%25E4%25B8%258B%25EF%25BC%258C%2560useTrackedState%2560%25E8%25BF%2590%25E8%25A1%258C%25E5%259C%25B0%25E5%25B9%25B6%25E4%25B8%258D%25E6%2598%25AF%25E9%2582%25A3%25E4%25B9%2588%25E8%2589%25AF%25E5%25A5%25BD%25E3%2580%2582 "https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Global/Objects/Proxy)%E3%80%82%E4%BD%BF%E7%94%A8Proxy%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8B%A6%E6%88%AA%E9%92%88%E5%AF%B9%E9%92%88%E5%AF%B9%E7%8A%B6%E6%80%81%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%B1%9E%E6%80%A7%E8%AE%BF%E9%97%AE%E3%80%82%E5%A6%82%E6%9E%9C%E8%BF%99%E4%B8%AA%E9%92%A9%E5%AD%90%E8%A2%AB%E5%90%88%E7%90%86%E5%9C%B0%E5%AE%9E%E7%8E%B0%EF%BC%8C%E5%AE%83%E5%8F%AF%E4%BB%A5%E4%BB%A3%E6%9B%BF%E5%A4%A7%E9%83%A8%E5%88%86%60useSelector%60%E7%9A%84%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E4%BA%86%E3%80%82%E7%84%B6%E8%80%8C%EF%BC%8C%E5%9C%A8%E6%9F%90%E4%BA%9B%E6%83%85%E5%86%B5%E4%B8%8B%EF%BC%8C%60useTrackedState%60%E8%BF%90%E8%A1%8C%E5%9C%B0%E5%B9%B6%E4%B8%8D%E6%98%AF%E9%82%A3%E4%B9%88%E8%89%AF%E5%A5%BD%E3%80%82")

useSelectoruseTrackedState的不同

在某些场景下,使用useSelector 比 使用useTrackedState 好。因为 useSelector 可以 生成 衍生值。衍生值,可以理解为 基于 state 而变化的值。

下面是一个例子,用于说明这两个钩子的不同:

js 复制代码
const Component = () => {
    const isSmall = useSelector((state) => state.a < 10);
    return <>{isSmall ? 'small' : 'big' }</>
}

同样的逻辑,用 useTrackedState 来实现:

js 复制代码
const Component = () => {
    const isSmall = useTrackedState().a < 10;
    return <>{isSmall ? 'small' : 'big' }</>
}

从功能上来说,这个使用 useTrackedState 的组件运行良好,但每当状态 state.a 发生变化时,它都会触发重新渲染。相反,使用 useSelector 时,只有当 isSmall 发生变化时才会触发重新渲染,这意味着它在渲染优化方面表现得更好。

使用atoms

还有一个方法,就是使用atoms。atom,就是触发重新渲染的最小状态单元。atom给予了我们更加颗粒化地订阅状态的能力,而不是订阅整个store对象。

比如说,我们有一个只订阅一个atom的useAtom钩子。而一个atom函数会产生一个state对象的atom单元:

js 复制代码
const globalState = {
    a: atom(1),
    b: atom(2),
    e: atom(3),
}

const Component = () => {
    const value = useAtom(globalState.a);
    return <>{value}</>;
}

如果atom们被有效分割,就等于有多个独立的全局状态。然而,我们可以用atoms创建衍生值。比如说,我们要用globalState创建一个sum:

js 复制代码
const sum = globalState.a + globalState.b + globalState.c;

为了实现这一点,我们需要跟踪依赖关系,并在某个依赖atom被更新时重新评估派生值。在第八章的用例场景二 "Jotai" 中,我们将仔细研究这样一个 API 是如何实现的。

使用atom的这种方法可以被看作介于手动方法和自动方法之间。虽然atom和派生值的定义是明确指定的(手动的),但依赖关系的跟踪却是自动进行的。

在本节中,我们了解了用于优化重新渲染的各种模式。对于一个全局状态管理库而言,设计如何优化重新渲染是很重要的。这往往会影响到库的 API,并且对于库的使用者来说,了解如何优化重新渲染也是很有价值的。

概要

在本章中,在深入探讨全局状态管理库的实际实现之前,我们了解了与之相关的一些基本挑战,以及区分不同全局状态管理库的一些类别。在选择全局状态管理库时,我们可以考察该库如何让我们读取和写入全局状态,它将全局状态存储在何处,以及它如何优化重新渲染。理解这些方面对于判断哪些库适合特定用例非常重要,它们能帮助你选择符合自身需求的库。

在下一章中,我们将学习 Zustand 库,这是一个采用以数据为中心的方法,并通过选择器函数来优化重新渲染的库。

相关推荐
回村中年3 分钟前
浏览器 路由详解
前端
Mcband6 分钟前
主流程发起,去除子流程的时长计算问题
java·前端·算法
晓得迷路了6 分钟前
栗子前端技术周刊第 75 期 - Rspack 1.3、React 19.1、Astro 5.6...
前端·javascript·react.js
Kagol7 分钟前
TinyPro 后台管理系统从启动 ➡️ 使用 ➡️ 二开,看这一篇就够了!点赞、收藏⭐,不迷路!
前端·vue.js·nestjs
QTX187308 分钟前
常见的 JavaScript 框架和库
开发语言·javascript·ecmascript
xiezhr19 分钟前
程序员为什么总是加班?
前端·后端·程序员
好_快22 分钟前
Lodash源码阅读-baseIsMatch
前端·javascript·源码阅读
excel22 分钟前
webpack 格式化模块工厂 第 一 节
前端
九筠25 分钟前
python网络爬虫开发实战之Ajax数据提取
前端·爬虫·ajax·网络爬虫
excel38 分钟前
webpack 核心编译器 十七 节
前端