第九章 案例 3 - Valtio 【上】

Valito是另一个实现全局状态的库。不像 Zustand 和 Jotai,Valito 是 基于可变更新模型的。它主要用于类似 Zustand 的模块状态管理。它利用代理(proxies)来获取不可变的快照,这是与 React 集成所必需的。

这个库的API都是纯JavaScript,而更复杂的事情则发生在这些纯JavaScript之后。Valito借助了proxy的能力来自动优化重新渲染。它并不需要一个selector函数来控制重新渲染。这种 自动的渲染优化,是一基于一种叫 状态使用追踪的 技巧。使用了 状态使用追踪 后,它能够帮助我们检测哪个部分的状态被使用了,并让重新渲染只在被使用的状态发生变化时才发生。如此一来,开发者就可以少写很多代码了。

在这一章,我们会学习Valtio库的基本用法,以及其如何更新状态。Snapshot是创建不可变状态的核心。我们会学习Snapshot和 proxy 是如何帮助我们 优化 重新渲染的。

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

  • 探索另一模块状态库 Valtio
  • 使用proxy来检测状态变动,并创建一个不可变状态
  • 使用proxy来优化重新渲染
  • 创建应用代码
  • 该库的优点与缺点

探索另一模块状态库 Valtio

Valtio 和 Zustand 一样,都是 模块状态 管理库。

在 第七章我们 学到,我们通常这样创建一个仓库:

js 复制代码
const store = create(() => {
    count: 0,
    text: "hello"
})

这个store变量有一个setState属性。我们可以用 setState 来 更新状态。比如,我们可以实现一个递增方法:

js 复制代码
store.setState((prev) => {
    count: prev.count + 1
})

为什么我们需要setState来更新一个状态?因为我们希望以不可变更新的方式来更新一个状态。setState内部,大致是这样实现的:

js 复制代码
moduleState = Object.assign({}, moduleState, {
    count: moduleState.count + 1
})

这是实现不可变更新的方式。

如果我们不遵循不可变更新的模式,可以这样实现自增:

js 复制代码
++moduleState.count;

如果这样直接修改对象的代码可以在React中运行,岂不美哉?事实上,我们可以用proxy来实现。

Proxy是一种特殊的JavaScript对象。我们可以用它来拦截一些对象操作。比如说,你可以用一个 set 处理器来拦截对象的变化:

js 复制代码
const proxyObject = new Proxy({
    count: 0,
    text: "hello",
}, {
    set: (target, prop, value) => {
        conosle.log("start setting", prop);
        target[prop] = value;
        console.log("end setting", prop);
    },
});

我们创建proxyObject时,在 new Proxy 内 传入了两个参数。第一个参数是 对象本身。第二个参数 是 处理各自操作的配置对象。在这个例子中,我们有一个 在对象 发生变动后 打印日志的 set 处理函数。

proxyObject 和 普通的 对象不一样。当你改动对象的值时,它会在改动发生前 和 发生后 打印日志。如果你在 Node.js REPL 运行这段代码,会看到这些:

js 复制代码
> ++proxyObject.count
start setting count
end setting count
1

本质上来说,Valtio是一个用proxy来检测状态变化的库。

在本小节,我们学习到Valito是基于可变更新模型的。接下来,我们要学习Valito是如何使用变更来创建不可变状态。

使用proxy来检测状态变动,并创建一个不可变状态

Valtio 通过代理(proxies)从可变对象创建不可变对象。我们将这个不可变对象称为快照(snapshot)。

我们借助Valtio提供的proxy来创建可变对象。

下面是一个简单的示例:

js 复制代码
import { proxy } from "valtio";

const state = proxy({ count: 0 });

而创建不可变对象,则可以借助Valito所提供的snapshot 函数,如下:

js 复制代码
import { snapshot } from "valito";

const snap1 = snapshot(state);

尽管 state 和 snap1 看起来 很像,但是它们的索引值是不一样的。state 是 包裹 在 proxy 里的 一个对象。而 snap1 是一个被 Object.freeze 所冻结的 不可变对象。

我们通过一个例子,看看 snapshots 是如何 工作的:

js 复制代码
++state.count;

const snap2 = snapshot(state);

state { count: 1 } 变量 的 索引地址没有变,和之前一样。而 snap2 { count: 1 } 则有了一个 新的 索引地址。 因为 snap1 和 snap 2 是不可变的,我们可以 用 snap1 === snap2 来 检查 其 相等性。

proxy 和 snapshot 函数,同样也 适用于 嵌套 对象,并 优化 snapshot 的 创建过程。这意味着,snapshot函数只在必要时创建snapshot,也就是说,在state 的 属性 发生变化时 创建 snapshot。让我们以这个嵌套对象的例子展开:

js 复制代码
const state2 = proxy({
    obj1: { c: 0 }
    obj2: { c: 0 }
});

const snap21 = snapshot(state2)

++state2.obj.c;

const snap22 = snapshot(state2)

在这个例子中 snap21 是 { obj1: { c: 0 }, obj2: { c: 0 } },而snap22 是 { obj1: { c: 1 }, obj2: { c: 0 } }。snap21 和 snap22 的 索引地址不一样,所以 snap21 !== snap22 成立.

那,嵌套在里面的对象的相等性如何?snap21.obj1 和 snap22.obj1 的 索引地址是不一样的,但 snap21.obj2 和 snap22.obj2 的 索引地址是一样的。这是因为 obj2 的 c 属性并没有发生 变化,所以 obj2 不需要改变其索引地址,因此 snap21.obj2 === snap22.obj2 成立。

这个快照优化是一项重要的特性。snap21.obj2 和 snap22.obj2 有相同的索引,也就意味着 它们 共享同一段内存地址。Valtio之会在 有必要时创建快照。

在这个小节,我们学习了 Valtio 是 如何 自动 创建 不可变状态,也就是 快照的。接下来,我们要学习 Valtio 为 React 提供的 钩子函数。

使用proxy来优化重新渲染

除了检测变动外,Valtio 还 使用 proxy 来 优化 重新 渲染。这是我们在第六章学习到的 优化重新渲染的一种模式。

让我们以一个 计数app,切入Valtio的钩子。这个钩子叫 useSnapshotuseSnapshot 的实现 是基于 snapshot 函数 和 另一个 包裹它的 proxy 对象。这个 snapshot 的 proxy 的目的 与 proxy 函数 不一样。这个 snapshot 的 proxy 用于 检测 一个 snapshot 对象的 属性可 访问性。多亏了 snapshot 的 proxy,我们可以看到 渲染优化 是如何 运作的。

我们先从引入相关函数开始:

js 复制代码
import { proxy, useSnapshot } from "valito"

proxyuseSnapshot 是 Valito提供的 两个 常用的 函数。这个两个 方法足以应付 最大多数 场景了。

之后,我们用proxy来创建一个类型为对象的状态:

js 复制代码
const state = proxy({
    count1: 0,
    count2: 0,
})

这个proxy函数接受了原始对象后,会返回一个 proxy 对象。如此一来,我们就可以随心改动这个对象了。

之后,我们可以定义使用了count1 属性 的Counter1 组件:

js 复制代码
const Counter1 = () => {
    const snap = useSnapshot(state);
    const inc = () => ++state.count1;
    
    return (
        <>
            {snap.count1} <button onClick={inc}> +1 </button>
        </>
    )
}

之后,我们可以定义Counter2 组件:

js 复制代码
const Counter2 = () => {
    const snap = useSnapshot(state);
    const inc = () => ++state.count2;
    
    return (
        <>
            {snap.count2} <button onClick={inc}> +1 </button>
        </>
    )
}

这个两个组件的不同,就在于 Counter1 组件 使用了 counter1属性,而Counter2 组件 使用了 counter2属性

之后,我们可以定义 App 组件。因为我们没有 使用 Context,就不用写 providers了:

js 复制代码
const App = () => {
    <>
        <div><Counter1 /></div>
        <div><Counter2 /></div>
    </>
}

这个计数应用是如何运作的?在第一次渲染时,这个状态对象是 { count1: 0, count2: 0 },且它为 快照对象。Counter1 组件 会 访问 快照对象的 count1 属性,而 Counter2 组件 会 访问 快照对象的 count2 属性.每一个useSnapshot钩子知道并记得这些追踪信息。而追踪信息代表着哪一个属性被访问了。

当我们点击了Counter1 组件的 按钮,它会改变状态对象的 count1 属性

因此,这个状态对象会变成 { count1: 1, count2: 0 }.此时 Counter1 组件 会 把 状态更新为 1。与此同时, Counter2 组件 并不会 重新渲染,因为 count2 并没有变化。

而这个重新渲染的过程,是基于追踪信息来实现的。

在这个例子中,状态对象还只是简单的双属性对象。而Valito是支持嵌套对象与嵌套数组结构的:

js 复制代码
const contrivedState = proxy({
    num: 123,
    str: "hello",
    arr: [1, 2, 3],
    nestedObject: { foo: "bar" },
    objectArray: [ { a: 1 }, { b: 2 }]
})

无论嵌套的数组、对象有多深,Valtio都能支持。

在这个小节,我们学习了Valtio是如何通过 snapshot 和 proxy 来 优化 重新渲染。在下一个部分,我们会结合一个例子来讲解如何构建一个应用。

相关推荐
北辰alk5 分钟前
package.json 中模块入口字段详解
前端
xu__yanfeng7 分钟前
绘制平滑的曲线
前端
懒猫爱上鱼7 分钟前
Jetpack Compose 中的 MVVM 架构解析
前端
马克凤梨8 分钟前
低代码平台中的拖拽设计:从原理到实践
前端·低代码
北辰alk11 分钟前
加速 npm install 的全面指南
前端
susnm15 分钟前
classnames-rs 库
前端·rust
市民中心的蟋蟀15 分钟前
第十章 案例 4 - React Tracked 【上】
前端·javascript·react.js
前端小巷子16 分钟前
JS 函数柯里化
前端·javascript·面试
工呈士18 分钟前
React Hooks 与异步数据管理
前端·react.js·面试
JustHappy21 分钟前
「Versakit攻略」或许我们还是需要一份SEO优化攻略🔥🔥
前端·seo