第十一章 这三个全局状态管理库之间的共性与差异 【下】

113 阅读7分钟

使用 Valtio 和 MobX

虽然 Valito 和 MobX 背后的 工作原理不一样,Valito 经常 被 拿来 和 MobX进行比较。这两者在 绑定 React上,有一定的相似性。它们俩都是基于可变式更新模型,允许开发者之间改变状态的。而JS本身是允许可变式更新的,所以Valtio 和 MobX 对于 开发者来说,是更加自然和好用的。

然而,Valtio 和 MobX 在 优化重新渲染上,是非常不同的。

Valito是 基于 钩子函数进行 重新渲染优化的,而 MobX则是采用了 高阶组件。

在本小节,我们会把基于 MobX 的应用 转化为 给予 Valito 的应用,从而比较这两个库的异同。

使用了 Valito 和 MobX的例子

首先,我们要从 MobX 引入两个基础函数:

import { makeAutoObservable } from "mobx";
import { observable } from "mobx-react";

接下来,我们要实现一个 计时器的逻辑。我们要创建一个相关的类,并将它实体化。

class Timer {
    secondsPassed = 0;
    constructor () {
        makeAutoObservable(this);
    },
    increase() {
        this.secondsPassed +=1;
    }
    reset() {
        this.secondsPassed = 0;
    }
}

const myTimer = new Timer();

这个类有一个字符串属性和两个函数属性。makeAutoObservable可以使myTimer 这个 实例 成为一个可观察对象。

我们可以在任何地方调用这些函数属性。比如,我们可实现一个计时方法:

setInterval(() => {
    myTimer.inrease();
}, 1000)

然后,我们要实现一个使用了该 timer 的组件:

const TimerView = observer(({ timer }: { timer: Timer }) => (
    <button onClick={() => timer.reset()}>
        Seconds passed: {timer.secondsPassed}
    </button>
));

这个observer 函数,是一个高阶组件。它能够读取 在 渲染函数中要使用的 timer.secondsPassed,并确保在 timer.secondsPassed 变动时, 发生 重新 渲染。

最后,我们要实现传入了 Timer 实例 给 TimerViewApp组件:

const App = () => (
    <>
        <TimerView timer={myTimer} />
    </>
)

如下图所示,如果你运行了这个应用,它会展示已经过去的秒数。而点击重置按钮后,数字就会清零。

image.png

那么,用Valito来实现,会所如何?

首先,我们要从 Valtio 库 引入两个 函数:

import { proxy, useSnapshot } from "valtio";

我们使用 proxy 函数来定义 myTimer 实例:

const myTimer = proxy({
    secondesPassed: 0,
    increase: () => {
        myTimer.secondesPassed += 1;
    },
    reset: () => {
        myTimer.secondesPassed = 0;
    }
})

它有一个 数字类型的 secondsPassed 属性,和两个用来更新状态的 函数属性。

然后,我们就可以实现计时功能了:

setInterval(() => {
    myTimer.inrease();
}, 1000)

这个计时函数的用法,和 MobX里的是一样的。

然后,我们要实现的,是使用了 useSnapshotTimerView 组件:

const TimerView = ({ timer }: { timer: typeof myTimer }) => {
    const snap = useSnapshot(timer);
    return (
        <button onClick={() => timer.reset()}>
            Seconds passed: {snap.secondsPassed}
        </button>
    );
};

在Valito里,useSnapshot 用于读取 render 函数中要使用的 状态,并在 这些状态变化时,进行重新渲染。

最后,我们要实现App 组件,它看起来 和 之前的App 组件一样:

const App = () => (
    <>
        <TimerView timer={myTimer} />
    </>
)

然后,这段代码实现的功能和 用 Mobx 实现的 计时功能一样。

接下来,我们讨论这 两个库的不同。

比较 Valito 和 MobX的例子

这两个库大体看起来 差不多,但还是有两个显著的不同点:

  • 第一个不同是更新方法。虽然这两者都是基于 可变式更新模型,但是 MobX 是基于 类 进行 更新的,而 Valito 是 基于 对象 进行更新的。这是最显著的不同,而 Valtio 并不 关心 更新方式。
    Valito 运行 开发者 把 更新 函数 抽离出来。刚刚实现的 myTimer 对象 可以这样实现:
// timer.js
const timer = proxy({ secondesPassed: 0 });

export const increase = () => {
    timer.secondesPassed += 1;
}

export const reset = () => {
    timer.secondesPassed += 0;
}

export useSecondsPassed = () => 
    useSnapshot(timer).secondesPassed;

因为proxy 赋予了我们进行 可变式 更新的能力,我们可以把 这两个 更新函数 从 timer 对象里 抽离出来。这样做的好处是,可以进行代码拆分、降低包提及,并删除一些无用的 代码。

  • 第二个不同是优化渲染的方式。MobX 采用的是 观察者模式,而 Valtio 采用的 是 钩子模式。这两种模式各有其利弊。观察者模式的可预测性更强,而 钩子模式 对于并发渲染更加友好。实现这些模式也是非常不同的。当然,采用哪种模式,也是开发者个人偏好的问题。

在这个小节,我们比较了 MobX 和 Valito的 异同。接下来,我们要比较 Zustand, Jotai 和 Valito。

比较 Zustand,Jotai 和 MobX

在本章中,我们已经对比了以下几对状态管理库:

  • 在 "Zustand 与 Redux 的区别" 一节中对比了 Zustand 和 Redux
  • 在 "理解何时使用 Jotai 和 Recoil" 一节中对比了 Jotai 和 Recoil
  • 在 "使用 Valtio 和 MobX" 一节中对比了 Valtio 和 MobX

我们之所以对比这些组合,是因为它们之间存在一些相似性。在本节中,我们将对 Zustand、Jotai 和 Valtio 进行对比。

首先,这三个库均由 Poimandres GitHub 组织(github.com/pmndrs)提供。该… GitHub 组织推出三个微型状态管理库,这听起来可能有违直觉,但它们的风格各不相同。这三个库还共享一个核心理念:它们的 API 表面都非常精简。三者都尽力提供极简的 API 接口,并让开发者能够根据需求自由组合这些 API。 不过,这三个库之间究竟有哪些区别呢?

有两个方面:

*状态驻留在哪里?在 React 中存在两种方式。一种是模块状态,另一种是组件状态。模块状态是在模块级别创建且不属于 React 的状态。组件状态是在 React 组件生命周期中创建并由 React 控制的状态。Zustand 和 Valtio 是为模块状态设计的。而 Jotai 则是为组件状态设计的。例如,以 Jotai 的 atoms 为例,下面是 countAtom 的定义:

const countAtom = atom (0); 

这个 countAtom 变量保存的是一个配置对象,并不保存值。atom 的值存储在 Provider 组件中。因此,countAtom 可以在多个组件中重复使用。而用模块状态实现相同的行为则较为复杂。使用 Zustand 和 Valtio 时,我们最终可能需要借助 React Context。另一方面,从 React 外部访问组件状态在技术上是不可行的,我们可能需要某种模块状态来连接组件状态。 选择使用模块状态还是组件状态取决于应用程序的需求。通常,使用模块状态或组件状态来管理全局状态即可满足应用需求,但在少数情况下,同时使用这两种状态可能更为合理。

  • 状态更新风格是怎样的?Zustand 和 Valtio 之间存在一个主要区别。Zustand 基于不可变状态模型,而 Valtio 基于可变状态模型。不可变状态模型的约定是对象一旦创建就不能更改。假设你有一个状态变量,如 state = { count: 0 }。如果想在不可变状态模型中更新 count,需要创建一个新对象。因此,将 count 加 1 应该是 state = { count: state.count + 1 }。而在可变状态模型中,可以直接 ++state.count。这是因为 JavaScript 对象本质上是可变的。 不可变模型的优势在于可以通过比较对象引用判断是否有变化,这对大型嵌套对象的性能优化很有帮助。由于 React 大多基于不可变模型,因此采用相同模型的 Zustand 具有良好的兼容性,这也使得 Zustand 成为一个非常轻量级的库。另一方面,使用可变状态模型的 Valtio 需要填补两种模型之间的差异。 总之,Zustand 和 Valtio 采用了不同的状态更新风格。可变更新风格非常便捷,尤其是在处理深度嵌套对象时。可以回顾第 9 章 “用例场景 3 - Valtio” 中 “此方法的优缺点” 一节的示例。

这三个库之间存在一些细微差别,但重要的是它们基于不同的原则。如果我们要从中选择一个,需要看哪一个原则更符合我们的应用需求和思维模式。

概要

在本章中,我们总结了本书所介绍的三种全局状态管理库之间的差异。它们之所以不同,是因为基于不同的模型。

从本质上讲,微型状态管理涉及为特定问题选择合适的解决方案和库。微型状态管理要求你了解自己面临的问题是什么,以及有哪些可用的解决方案。我们希望本书涵盖的一些主题能帮助开发者找到合适的解决方案。