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的钩子。这个钩子叫 useSnapshot
。useSnapshot
的实现 是基于 snapshot 函数 和 另一个 包裹它的 proxy 对象。这个 snapshot 的 proxy 的目的 与 proxy 函数 不一样。这个 snapshot 的 proxy 用于 检测 一个 snapshot 对象的 属性可 访问性。多亏了 snapshot 的 proxy,我们可以看到 渲染优化 是如何 运作的。
我们先从引入相关函数开始:
js
import { proxy, useSnapshot } from "valito"
proxy
和 useSnapshot
是 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 来 优化 重新渲染。在下一个部分,我们会结合一个例子来讲解如何构建一个应用。