什么是 Valtio?
Valtio makes proxy-state simple for React and Vanilla
翻译成中文,Valtio是一个基于 Proxy 实现,更容易上手的状态管理工具。
Valtio 怎么发音?
[ˈvæltioʊ]
常用的有哪些状态管理工具?
市面上的 React 状态管理工具非常多,例如:Redux、Mobx、Valtio、Recoil。
-
Redux 属于单向数据流方式
-
Mobx 和Valtio 都属于双向绑定的响应式的实现
-
Recoil 以原子化方式对状态进行分离管理
-
也可以直接用 props 或者 context,不需要任何三方状态管理工具
首先,我们分别用 Svelte, Vue, React 实现如下的效果:
Svelte 在线编辑
<script>
let count = 0;
function handleClick() {
count++;
}
</script>
<div>
{count}
<button on:click={handleClick}>+1</button>
</div>
Vue 在线编辑
<template>
{{count}}
<button @click="handleClick">+1</button>
</template>
<script>
export default {
name: 'App',
data() {
return {
count: 0
}
},
methods: {
handleClick() {
this.count++
},
}
}
</script>
React 在线编辑
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount((count) => ++count)
}
return (
<div>
{count}
<button onClick={handleClick}>+1</button>
</div>
)
}
export default App
React (引入 Valtio 后)在线编辑
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0 })
function App() {
const snap = useSnapshot(state)
const handleClick = () => {
++state.count
}
return (
<div>
{snap.count}
<button onClick={handleClick}>+1</button>
</div>
)
}
export default App
可以看出,引入 Valtio 后,React 状态管理变得和 Svelte/Vue 一样简洁
接下来我们一起来看看 Valtio 有什么优势?在什么场景下使用 Valtio?以及Valtio源码分析。
基于 Valtio 做状态管理有什么优势?
上手会更加简单
使用体验和 mobx 基本一致。支持:
-
响应式对象
-
细粒度 render
-
计算属性
适用于需要简单的自动更新的场景
Valtio源码分析
Valtio 的实现就两个核心方法 proxy 与 useSnapshot
proxy 用于包装原始对象( initialObject ),生成可监听修改操作的 observable state。
在组件中,使用 useSnapshot(state) 返回一个不可变的对象 snap - 用于组件渲染,当需要对状态变更时,需操作 state 反映到原始对象上。
Valtio 状态管理本质上是订阅发布模式,如图:
1 创建可观察的状态 ( observable state )
在 Valtio 中封装 observable state 的函数是 proxy,demo 中所示:
const state = proxy({ count: 0 })
外界对目标对象 { count: 0 } 的访问,会通过 handler 层拦截,对于 set/deleteProperty 的操作,会触发 notifyUpdate 函数,最终更新视图。
每个 state 都会有 [PROXY_STATE] 属性,以数组的形式保存着。
[
target, // 目标对象 { count: 0 }
receiver, // Proxy的实例对象,本例中是 state
version, // 版本号,每次set/deleteProperty操作,都会使版本号+1,以此来区分新老数据
createSnapshot, // 创建只读的对象,用于组件的渲染
listeners, // 存储着所有和该state相关的组件的forceUpdate函数调用,用于rerender组件
]
state 属性操作如图所示:
为了便于理解,将 proxy 函数的代码进行简化,点我看源码
const proxy = (initialObject) => {
const PROXY_STATE = Symbol()
const handler: ProxyHandler<T> = {
get(target: T, prop: string | symbol, receiver: object) {
if (prop === PROXY_STATE) {
const state: ProxyState = [
target,
receiver,
version,
createSnapshot,
listeners,
]
return state
}
return Reflect.get(target, prop, receiver)
},
deleteProperty(target: T, prop: string | symbol) {
const deleted = Reflect.deleteProperty(target, prop)
if (deleted) {
notifyUpdate(['delete', [prop], prevValue])
}
return deleted
},
set(target: T, prop: string | symbol, value: any, receiver: object) {
const hasPrevValue = Reflect.has(target, prop)
const prevValue = Reflect.get(target, prop, receiver)
if (hasPrevValue && objectIs(prevValue, value)) {
return true
}
let nextValue = value
Reflect.set(target, prop, nextValue, receiver)
notifyUpdate(['set', [prop], value, prevValue])
return true
},
}
return new Proxy(target, handler)
}
2 订阅 subscribe
该函数有两个重要参数,proxyObject 和 callback
-
proxyObject: 实参为上文中定位的的 Proxy 实例 state
-
callback: React 组件重新渲染函数 forceUpdate
将 callback 函数加入到 proxyObject 对象的 listeners 集合中,proxyObject 对象属性的增删改操作会触发 listeners 函数执行,精准的触发对应组件重新渲染,做到了只 render 真正需要 render 的组件。
简化代码如下:点我看源码
function subscribe<T extends object>(
proxyObject: T,
callback: (ops: Op[]) => void,
) {
let promise: Promise<void> | undefined
const ops: Op[] = []
const listeners = ((proxyObject as any)[PROXY_STATE] as ProxyState)[4]
const listener: Listener = (op) => {
ops.push(op)
if (!promise) {
promise = Promise.resolve().then(() => {
promise = undefined
if (listeners.has(listener)) {
callback(ops.splice(0))
}
})
}
}
listeners.add(listener)
return () => listeners.delete(listener)
}
3 建立关联
React v18.2.0 新增了 useSyncExternalStore hook【用于读取和订阅外部数据源】,通过使用该hook,将定义在 React 外部的可观察的状态 ( observable state ) 和 React Component Render 建立关联,state 上任意属性的变化都会触发组件的重绘。
语法:
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
此方法返回存储的值并接受三个参数:
-
subscribe:用于注册一个回调函数,当存储值发生更改时被调用。
-
getSnapshot: 返回当前存储值的函数。
-
getServerSnapshot:返回服务端渲染期间使用的存储值的函数
由于兼容性的原因,作者引入了 shim
import {useSyncExternalStore} from 'react';
or
// Backwards compatible shim
import {useSyncExternalStore} from 'use-sync-external-store/shim';
基于 useSyncExternalStore 作者封装了 useSnapshot hook,使用方式:
const snap = useSnapshot(state)
函数源码如下:
function useSnapshot<T extends object>(
proxyObject: T,
options?: Options
): Snapshot<T> {
const notifyInSync = options?.sync
const lastSnapshot = useRef<Snapshot<T>>()
const lastAffected = useRef<WeakMap<object, unknown>>()
let inRender = true
const currSnapshot = useSyncExternalStore(
useCallback(
(callback) => {
const unsub = subscribe(proxyObject, callback, notifyInSync)
callback() // Note: do we really need this?
return unsub
},
[proxyObject, notifyInSync]
),
() => {
const nextSnapshot = snapshot(proxyObject, use)
try {
if (
!inRender &&
lastSnapshot.current &&
lastAffected.current &&
!isChanged(
lastSnapshot.current,
nextSnapshot,
lastAffected.current,
new WeakMap()
)
) {
// not changed
return lastSnapshot.current
}
} catch (e) {
// ignore if a promise or something is thrown
}
return nextSnapshot
},
() => snapshot(proxyObject, use)
)
inRender = false
const currAffected = new WeakMap()
useEffect(() => {
lastSnapshot.current = currSnapshot
lastAffected.current = currAffected
})
const proxyCache = useMemo(() => new WeakMap(), []) // per-hook proxyCache
return createProxyToCompare(currSnapshot, currAffected, proxyCache)
}
4 通知 notify
versionHolder = [1] as [number]
let version = versionHolder[0]
const notifyUpdate = (op: Op, nextVersion = ++versionHolder[0]) => {
if (version !== nextVersion) {
version = nextVersion
listeners.forEach((listener) => listener(op, nextVersion))
}
}
整个流程描述为:
onDataChange -> notifyUpdata -> callListeners -> recreateSnap -> forceUpdate
更多API
除了 proxy 和 useSnapshot 外,还会有比如 ref、watch、proxyWithHistory、proxySet 等等,感兴趣的同学可以在官网查看使用。
参考:
Meet the new hook useSyncExternalStore, introduced in React 18 for external stores