Proxy state made simple.
Valtio是一个基于Proxy实现、极简的、灵活的状态管理工具。可以让使用者拥有类mobx的使用体验。如果使用者擅长Vue,将会非常熟悉这一概念。
当下,React所拥有的状态管理工具非常之多,且都兼具着各自不同的理念。Valtio以双向绑定响应式的方式,虽然和React本身理念有所冲突,但是可以将原来例如Redux之类状态管理库它们复杂繁琐的reduce、action等等取代掉,通过两个核心方法proxy与useSnapshot就可以实现状态管理。
import React from 'react';
import { proxy, useSnapshot } from 'valtio';
// 使用 proxy 创建一个可观察的状态对象
const state = proxy({
count: 0,
});
// 计数器组件
function Counter() {
// 使用 useSnapshot 订阅状态更新
const snap = useSnapshot(state);
return (
<div>
<h2>计数:{snap.count}</h2>
<button onClick={() => state.count++}>增加</button>
<button onClick={() => state.count--}>减少</button>
</div>
);
}
// 主组件
function App() {
return (
<div className="App">
<h1>Valtio 示例</h1>
<Counter />
</div>
);
}
export default App;
Valtio源码解析
目录结构
src
|- macro
| |- vite.ts
|- react
| |- utils
| | |- useProxy.ts
| |- utils.ts
|- vanilla
| |- utils
| | ├─ addComputed.ts
│ | ├─ derive.ts
│ | ├─ devtools.ts
│ | ├─ proxyMap.ts
│ | ├─ proxySet.ts
│ | ├─ proxyWithComputed.ts
│ | ├─ proxyWithHistory.ts
│ | ├─ subscribeKey.ts
│ | |─ watch.ts
| |- utils.ts
|- index.ts
|- macro.ts
|- react.ts
|- types.d.ts
|- utils.ts
|- vanilla.ts
其中,index.ts内容如下:
export * from 'valtio/vanilla'
export * from 'valtio/react'
核心逻辑
proxy
创建可观察的状态,实例:
const state = proxy({
count: 0,
});
通过对原对象{count:0}进行代理,当访问该目标对象时,会被handle层拦截,经过handle内置方法操作后,触发notifyUpdate方法,通知更新。 源码地址:src/vanilla.ts。
export function proxy<T extends object>(initialObject: T = {} as T): T {
return defaultProxyFunction(initialObject)
}
其中defaultProxyFunction()来自:
const [defaultProxyFunction] = buildProxyFunction()
其中buildProxyFunction()同样位于src/vanilla.ts,其返回一个数组:
[
// public functions
proxyFunction,
// shared state
proxyStateMap,
refSet,
// internal things
objectIs,
newProxy,
canProxy,
defaultHandlePromise,
snapCache,
createSnapshot,
proxyCache,
versionHolder,
] as const
其中buildProxyFunction()为proxyFunction,代码如下:
proxyFunction = <T extends object>(initialObject: T): T => {
//const isObject = (x: unknown): x is object => typeof x === 'object' && x !== null
if (!isObject(initialObject)) {
throw new Error('object required')
}
//proxyCache = new WeakMap<object, ProxyObject>()
//proxyCache用来存储已经创建的代理对象
//以对象为Key值通过get查找对象的代理器,若已经存在该缓存则直接返回
const found = proxyCache.get(initialObject) as T | undefined
//若proxyCache中存在则直接返回
if (found) {
return found
}
//跟踪代理对象的版本
//通过对versionHolder进行响应式观察,来检测代理对象的状态变化,避免直接观察代理对象本身,从而提高性能
//versionHolder = [1, 1] as [number, number]
//整数值,代表当前代理对象的版本。
let version = versionHolder[0]
//type Listener = (op: Op, nextVersion: number) => void
//当代理对象发生变化时调用
const listeners = new Set<Listener>()
/**
* type Op =
| [op: 'set', path: Path, value: unknown, prevValue: unknown]
| [op: 'delete', path: Path, prevValue: unknown]
| [op: 'resolve', path: Path, value: unknown]
| [op: 'reject', path: Path, error: unknown]
*/
//op表示对代理对象的操作
//通知更新
const notifyUpdate = (op: Op, nextVersion = ++versionHolder[0]) => {
//version不一致,更新,遍历listeners
if (version !== nextVersion) {
//更新version
version = nextVersion
//遍历listeners,进行操作
listeners.forEach((listener) => listener(op, nextVersion))
}
}
let checkVersion = versionHolder[1]
//确保代理对象和其versionHolder的版本匹配,确保依赖关系在需要时得到正确的通知
const ensureVersion = (nextCheckVersion = ++versionHolder[1]) => {
if (checkVersion !== nextCheckVersion && !listeners.size) {
checkVersion = nextCheckVersion
//[propProxyState]: readonly [ProxyState, RemoveListener?] ->propProxyState: ProxyState
propProxyStates.forEach(([propProxyState]) => {
// propProxyState[1]: ensureVersion
const propVersion = propProxyState[1](nextCheckVersion)
if (propVersion > version) {
version = propVersion
}
})
}
return version
}
//创造属性监听器
//propListener属性监听器用来侦听属性变化的,当代理对象的某个属性发生变化时,属性监听器将会触发相应的回调函数,该机制可以在状态发生变化时通知依赖的组件,从而实现响应式
//状态更新时保证只通知到相关组件
const createPropListener =
(prop: string | symbol): Listener =>
(op, nextVersion) => {
const newOp: Op = [...op]
//newOp[1]:Path
newOp[1] = [prop, ...(newOp[1] as Path)]
notifyUpdate(newOp, nextVersion)
}
//存储与每个被代理对象的属性关联的代理状态,这些代理状态对象包含有关属性的依赖关系、更行以及其他元数据信息
//这些信息使得valtio可以在对象的属性被访问或者更改的时候,触发状态更新以及重新渲染
/**
* type ProxyState = readonly [
target: object, //目标对象
ensureVersion: (nextCheckVersion?: number) => number, //版本匹配
createSnapshot: CreateSnapshot, //创建只读的对象,用于组件的渲染
addListener: AddListener //添加监听者
]
*/
//type RemoveListener = () => void
const propProxyStates = new Map<
string | symbol,
readonly [ProxyState, RemoveListener?]
>()
//向代理对象添加属性监听器,用于监听某个属性的变化,并在变化时触发指定的回调函数
const addPropListener = (
prop: string | symbol,
propProxyState: ProxyState
) => {
//已存在
if (import.meta.env?.MODE !== 'production' && propProxyStates.has(prop)) {
throw new Error('prop listener already exists')
}
if (listeners.size) {
//propProxyState[3]:addListener:AddListener
//type RemoveListener = () => void
//type AddListener = (listener: Listener) => RemoveListener
const remove = propProxyState[3](createPropListener(prop))
propProxyStates.set(prop, [propProxyState, remove])
} else {
//没有listener,不设置remove
propProxyStates.set(prop, [propProxyState])
}
}
//移除属性监听器
const removePropListener = (prop: string | symbol) => {
const entry = propProxyStates.get(prop)
if (entry) {
//从propProxyStates中删除
propProxyStates.delete(prop)
//调用remove
entry[1]?.()
}
}
//添加listener
const addListener = (listener: Listener) => {
listeners.add(listener)
//对应addPropListener方法中的if(listener.size)中的propProxyStates.set(prop, [propProxyState])
//当有listener时,遍历propProxyStates,加上remove
if (listeners.size === 1) {
propProxyStates.forEach(([propProxyState, prevRemove], prop) => {
if (import.meta.env?.MODE !== 'production' && prevRemove) {
throw new Error('remove already exists')
}
const remove = propProxyState[3](createPropListener(prop))
propProxyStates.set(prop, [propProxyState, remove])
})
}
//移除listener
const removeListener = () => {
listeners.delete(listener)
//当listeners为空时,遍历删除remove
if (listeners.size === 0) {
propProxyStates.forEach(([propProxyState, remove], prop) => {
if (remove) {
remove()
propProxyStates.set(prop, [propProxyState])
}
})
}
}
return removeListener
}
//若为数组,则返回一个空数组,否则返回一个以initialObject为原型的基本对象
const baseObject = Array.isArray(initialObject)
? []
: Object.create(Object.getPrototypeOf(initialObject))
//制定拦截行为
const handler: ProxyHandler<T> = {
//删除属性
deleteProperty(target: T, prop: string | symbol) {
//获取属性的值
const prevValue = Reflect.get(target, prop)
//调用removePropListener,移除属性监听器
removePropListener(prop)
//执行默认删除操作
const deleted = Reflect.deleteProperty(target, prop)
//如果成功,调用notifyUpdate,发送对象更新通知
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)
//objectIs = Object.is
//如果有原有属性值并且新值和原有属性值相等或者代理对象缓存中存在新值,并且新值在代理对象缓存中对应的值和原有属性值相等,则直接返回true
if (
hasPrevValue &&
(objectIs(prevValue, value) ||
(proxyCache.has(value) &&
objectIs(prevValue, proxyCache.get(value))))
) {
return true
}
//调用removePropListener,移除属性监听器
removePropListener(prop)
//若新值为对象,尝试获取它的未跟踪值
if (isObject(value)) {
//从一个代理对象中提取未跟踪(原始)对象。允许我们在需要获取原始对象而不触发任何代理操作时使用
value = getUntracked(value) || value
}
let nextValue = value
//判断是否为Promis
if (value instanceof Promise) {
//设置对应状态和值,并在Promise状态改变时调用notifyUpdate
value
.then((v) => {
value.status = 'fulfilled'
value.value = v
notifyUpdate(['resolve', [prop], v])
})
.catch((e) => {
value.status = 'rejected'
value.reason = e
notifyUpdate(['reject', [prop], e])
})
} else {
//const proxyStateMap = new WeakMap<ProxyObject, ProxyState>()
//跟踪代理对象与其原始对象之间的映射关系
//canProxy = (x: unknown) =>isObject(x) &&!refSet.has(x) &&(Array.isArray(x) || !(Symbol.iterator in x)) &&!(x instanceof WeakMap) &&!(x instanceof WeakSet) &&!(x instanceof Error) &&!(x instanceof Number) &&!(x instanceof Date) &&!(x instanceof String) &&!(x instanceof RegExp) &&!(x instanceof ArrayBuffer)
//canProxy判断是否为可代理对象
//if ProxyStateMap不存在新值且新值可以为可代理对象
if (!proxyStateMap.has(value) && canProxy(value)) {
//创建新的代理对象
nextValue = proxyFunction(value)
}
//判断是否不是引用对象且是代理对象
const childProxyState =
!refSet.has(nextValue) && proxyStateMap.get(nextValue)
//添加属性监听器
if (childProxyState) {
addPropListener(prop, childProxyState)
}
}
//设置属性值
Reflect.set(target, prop, nextValue, receiver)
//通知相关更新
notifyUpdate(['set', [prop], value, prevValue])
return true
},
}
//newProxy = <T extends object>(target: T, handler: ProxyHandler<T>): T = new Proxy(target, handler),
//代理对象
const proxyObject = newProxy(baseObject, handler)
//设置对象值
proxyCache.set(initialObject, proxyObject)
const proxyState: ProxyState = [
baseObject,
ensureVersion,
createSnapshot,
addListener,
]
//设置代理对象与其原始对象之间的映射关系
proxyStateMap.set(proxyObject, proxyState)
Reflect.ownKeys(initialObject).forEach((key) => {
const desc = Object.getOwnPropertyDescriptor(
initialObject,
key
) as PropertyDescriptor
if ('value' in desc) {
proxyObject[key as keyof T] = initialObject[key as keyof T]
// We need to delete desc.value because we already set it,
// and delete desc.writable because we want to write it again.
delete desc.value
delete desc.writable
}
Object.defineProperty(baseObject, key, desc)
})
return proxyObject
}
其中有两个关键的数据结构proxyStateMap和proxyCache,二者区别如下:
- ‘proxystateMap’:主要用于跟踪和管理代理对象与原始对象之间的映射关系。它在代理对象创建时建立映射,并在需要时提供从代理对象到原始对象或从原始对象到代理对象的查询功能;
- ‘proxyCache’:主要用于缓存已经创建的代理对象,以避免同一个原始对象被多次创建代理对象,目的是提高性能和避免不必要的内存消耗。
subscribe
Subscribe from anywhere,访问状态并订阅更改,执行提供的回调函数。实例demo如下:
subscribe(state, () => console.log('state has changed to', state))
subscribe(state.count, () => console.log('state.count has changed to', state.count))
源码地址:src/vanilla.ts。
//proxyObject:代理对象,proxy实例
//callback:更新回调函数
//notifyInSync:可选boolean,判断是否立即执行回调
export function subscribe<T extends object>(
proxyObject: T,
callback: (ops: Op[]) => void,
notifyInSync?: boolean
): () => void {
//从proxyStateMap中获取proxyState
const proxyState = proxyStateMap.get(proxyObject as object)
if (import.meta.env?.MODE !== 'production' && !proxyState) {
console.warn('Please use proxy object')
}
let promise: Promise<void> | undefined
//Op数组,存储一组操作
const ops: Op[] = []
//定义一个addListener,用来在代理状态中添加监听器,以便在代理对象发生更改时调用listener
const addListener = (proxyState as ProxyState)[3]
let isListenerActive = false
//定义一个listener,当代理对象发生更改的时候进行调用。
//将对应操作添加到ops,根据notifyInSync判断是同步还是异步调用回调函数
const listener: Listener = (op) => {
ops.push(op)
if (notifyInSync) {
callback(ops.splice(0))
return
}
if (!promise) {
promise = Promise.resolve().then(() => {
promise = undefined
if (isListenerActive) {
callback(ops.splice(0))
}
})
}
}
//通过addlistener将listener添加到代理状态中
const removeListener = addListener(listener)
isListenerActive = true
//返回一个函数,其中包含取消订阅
return () => {
isListenerActive = false
removeListener()
}
}
snapshot
快照获取一个代理,并返回一个只读不可变的对象。不可变是通过通过有效地深度复制和冻结对象来实现的。在顺序快照调用中,当代理中的值没有更改时,将返回指向同一先前快照对象的指针。这允许在渲染函数中进行浅层次比较,从而防止虚假渲染。能够与React Suspense一起工作。 实例demo如下:
import { proxy, snapshot } from 'valtio'
const store = proxy({ name: 'Mika' })
const snap1 = snapshot(store) // an efficient copy of the current store values, unproxied
const snap2 = snapshot(store)
console.log(snap1 === snap2) // true, no need to re-render
store.name = 'Hanna'
const snap3 = snapshot(store)
console.log(snap1 === snap3) // false, should re-render
源码地址:src/vanilla.ts。
//proxyObject:代理对象
//handlePromise:可选,若提供该函数,则快照中的Promise将使用此函数进行处理
//type HandlePromise = <P extends Promise<any>>(promise: P) => Awaited<P>
//type Snapshot<T> = T extends SnapshotIgnore? T: T extends Promise<unknown>? Awaited<T>: T extends object? { readonly [K in keyof T]: Snapshot<T[K]> }: T
export function snapshot<T extends object>(
proxyObject: T,
handlePromise?: HandlePromise
): Snapshot<T> {
//获取代理状态
const proxyState = proxyStateMap.get(proxyObject as object)
if (import.meta.env?.MODE !== 'production' && !proxyState) {
console.warn('Please use proxy object')
}
//从代理状态中解构出target, ensureVersion, createSnapshot
const [target, ensureVersion, createSnapshot] = proxyState as ProxyState
//通过createSnapshot函数创建一个快照,并将返回的快照转换为Snapshot<T>类型
return createSnapshot(target, ensureVersion(), handlePromise) as Snapshot<T>
}
useSnapshot
- 创建一个本地快照用来捕获更改(快照钩子)。
- 通常,Valtio的快照(通过snapshot()创建)会在对代理或其任何子代理进行任何更改时重新创建。然而,useSnapshot将Valtio快照封装在访问跟踪代理中,因此您的组件是经过渲染优化的,即只有当它(或其子组件)专门访问的密钥发生更改时,它才会重新渲染,而不是每次更改代理时。 实例demo如下:
const snap = useSnapshot(state);
源码地址:src/react.ts。
首先需要先明白React的一个较新的hook:useSyncExternalStore。
import useSyncExternalStoreExports from 'use-sync-external-store/shim'
const { useSyncExternalStore } = useSyncExternalStoreExports
React18的新特性Concurrent可以让reconcile过程变得可中断,不会因为长时间占用主线程而阻塞渲染进程,从而提升性能。但是,当使用第三方库的时候,该模式会出现状态不一致的情形,所以React18提出useSyncExternalStore,已解决该类问题。
官网对其介绍为:
useSyncExternalStoreis a hook recommended for reading and subscribing from external data sources in a way that’s compatible with concurrent rendering features like selective hydration and time slicing.
作为一个从外部数据源读取和订阅的hook,用法如下:
//subscribe:函数注册一个回调,该回调在存储更改时被调用
//getSnapshot:返回存储的当前值的函数
//getServerSnapshot:回服务器渲染过程中使用的快照的函数
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
因为兼容性的原因,Valtio通过shim引入useSyncExternalStore。
useSnapshot源码如下:
//proxyObject:
//options:可选参数,type Options = {sync?: boolean}
export function useSnapshot<T extends object>(
proxyObject: T,
options?: Options
): Snapshot<T> {
//通过sync设置notifyInSync,是否同步通知
const notifyInSync = options?.sync
//存储上一个快照
const lastSnapshot = useRef<Snapshot<T>>()
//存粗上一个受影响的对象
const lastAffected = useRef<WeakMap<object, unknown>>()
//true表示组件正在渲染
let inRender = true
/使用useSyncExternalStore,
const currSnapshot = useSyncExternalStore(
//定义一个useCallback,在其中调用subScribe
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 &&
//proxy-compare
!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)
)
//false表示渲染结束
inRender = false
//创建一个WeakMap用来存储当前受影响的对象
const currAffected = new WeakMap()
//设置lastSnapshot,lastAffected
useEffect(() => {
lastSnapshot.current = currSnapshot
lastAffected.current = currAffected
})
//非生产环境下,使用useAffectedDebugValue调试受影响的部分
if (import.meta.env?.MODE !== 'production') {
// eslint-disable-next-line react-hooks/rules-of-hooks
useAffectedDebugValue(currSnapshot, currAffected)
}
//通过useMemo创建一个WeakMap,用于每个hook的代理缓存
const proxyCache = useMemo(() => new WeakMap(), []) // per-hook proxyCache
//proxy-compare,使用createProxyToCompare创建一个包裹在代理中的快照,并return出去
return createProxyToCompare(
currSnapshot,
currAffected,
proxyCache,
targetCache
)
}
所以当从快照中读取数据并改变数据源的时候,组件只会在访问的状态部分发生改变时重新渲染。
至此,Valtio核心源码逻辑都被一一分析,当然还会有其他的API,可以在源码中很快了解它们。
以后可能会参考Valtio去做一些事情,敬请期待~
本文可能会有部分个人理解上的错误,欢迎指正~
感谢:
等等...