snapshot
snapshot 接受一个代理并返回一个不可变对象,从代理中解包。
不可变性是通过高效地深度复制和冻结对象来实现的(详见 Copy on Write 部分)。
简而言之,在连续的 snapshot 调用中,当代理中的值没有改变时,会返回前一个快照的对象引用。这允许在渲染函数中进行浅比较,防止虚假渲染。
快照还会抛出 promise,使它们能够与 React Suspense 一起工作。
import { proxy, snapshot } from 'valtio'
const store = proxy({ name: 'Mika' })
const snap1 = snapshot(store) // 当前存储值的高效副本,未代理
const snap2 = snapshot(store)
console.log(snap1 === snap2) // true,无需重新渲染
store.name = 'Hanna'
const snap3 = snapshot(store)
console.log(snap1 === snap3) // false,应该重新渲染
写时复制
尽管快照是整个状态的深度副本,但它们使用延迟的写时复制机制进行更新,所以在实践中它们维护起来很快。
例如,如果我们有一个嵌套对象:
const author = proxy({
firstName: 'f',
lastName: 'l',
books: [{ title: 't1' }, { title: 't2' }],
})
const s1 = snapshot(author)
第一个 snapshot 调用创建四个新实例:
- 一个用于作者,
- 一个用于书籍数组,以及
- 两个用于书籍对象。
当我们改变第二本书并拍摄新的 snapshot 时:
author.books[1].title = 't2b'
const s2 = snapshot(author)
然后 s2 将有第二本书的新副本,但重用未改变的第一本书的现有快照。
console.log(s1 === s2) // false
console.log(s1.books === s2.books) // false
console.log(s1.books[0] === s2.books[0]) // true
console.log(s1.books[1] === s2.books[1]) // false
尽管这个例子只重用了四个现有快照实例中的一个,但它表明维护快照的成本基于状态树的深度(通常很低,比如从作者到书籍到书评是三个级别),而不是广度(数千本书)。
类
快照保持原始对象的原型,所以方法和 getter 可以工作,并正确评估快照的冻结状态。
import { proxy, snapshot } from 'valtio'
class Author {
firstName = 'f'
lastName = 'l'
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
const state = proxy(new Author())
const snap = snapshot(state)
// 快照具有 Author 原型
console.log(snap instanceof Author) // true
state.firstName = 'f2'
// 调用使用快照的状态,例如这仍然是 'f',因为
// 在 `fullName` 内部,`this` 将是冻结的快照实例,而不是
// 可变的状态代理
console.log(snap.fullName()) // 'f l'
注意,getter 和方法的结果不会被缓存,每次调用都会重新评估。
这应该没问题,因为期望它们执行得非常快(比缓存它们的开销值得),而且也是确定性的,所以返回值仅基于已经冻结的快照状态。
原生 JavaScript
在原生 JavaScript 中,snapshot 不是访问代理对象值所必需的,无论是在 subscribe 内部还是外部。但是,它很有用,例如,保持未代理对象的可序列化列表或检查对象是否已更改。它还会解析 promise。
💡 提示
如果您在 React 外部使用 valtio,请从
valtio/vanilla导入import { proxy, snapshot } from 'valtio/vanilla'