简单构建
createStore创建共享状态的引用,更新函数和订阅者set,更新函数告知所有订阅者进行更新.
useStore保存共享状态的某个版本并提供一个proxy用来监听组件访问的属性.如果这些属性变化或者组件访问了其他属性,立刻更新本地state版本同时触发组件更新
const createStore(initState){
const sharedStateRef = { current:initState }
const listeners = new Set()
const update = newState => {
sharedStateRef.current = newState
listeners.forEach(fn=>fn())
}
const useStore = ()=>{
const [state,setState] = useState(sharedRef.current)
const proxy = useMemo(()=>{
return createProxy(state)
},[state])
const updateState = useCallback(()=>{
if(state!==sharedRef.current && isChange(state,proxy)){
setState(sharedRef.current)
}
},[setState])
useEffect(()=>{
listeners.add(updateState)
return ()=>{
listeners.delete(updateState)
}
},[updateState])
return [proxy,update]
}
return useStore
}
发现热重载情况下react会多次调用createStore,这会导致出现多个listeners,组件无法正常订阅共享状态(具体发生了我也没捋顺,反正数据放在闭包里面就是不行).所以改为放在context里面
const createStore(initState){
const contextValue = getContextValue(initState)
const Context = createContext(contextValue)
const Provider = props=>{
return createElement(
Context.Provider,
{ value: contextValue },
props.children
)
}
const useStore = ()=>{
const { sharedRef,listeners,update } = useContext(Context)
// 其余都一样
}
return useStore
}
增加形态
.受到use-context-selector启发,再加两种形态.
常规形态
对应useStore.使用setState更新组件,会以常规优先级参与react并发,按照资源情况灵活调度
同步形态
使用useSyncExternalStore进行更新.使用这个hook,在更新时会额外进行一次更新.此次更新脱离react并发,以同步方式进行
异步形态
使用Promise进行更新.在更新结束前,距离最近的Suspense会展示fallback
最终结果
再加亿点点细节,immer便于更新,proxy-compare捕获属性访问行为
import { Draft, produce } from 'immer'
import { createContext, createElement, FC, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import { createProxy, isChanged } from 'proxy-compare'
/**
* 创建共享存储
* @param initState 初始值
* @example
* const { Privoider, useStore } = createStore({ text: 'text', num: 1 })
*
* export default const App = () => {
* return <Privoider><Comp/></Privoider>
* }
*
* const Comp = () => {
* const [sharedState,setSharedState] = useStore()
* const increase = () => {
* setSharedState(state => {
* state.count += 1
* })
* }
* return <button onClick={increase}>{state.count}</button>
* }
*/
export const createStore = <T>(initState: T) => {
const contextValue = getContextValue(initState)
const Context = createContext(contextValue)
// 热重载会重新调用createStore 通过闭包存储共享状态会导致监听器无法正常绑定
const Privoider: FC<PropsWithChildren> = props => {
return createElement(
Context.Provider,
{ value: contextValue },
props.children
)
}
/** 三种更新方式的共通行为 */
const useCommon = () => {
const { initState, sharedStateRef, updateSharedState, sharedStateListeners } = useContext(Context)
// 本地状态.共享状态的某个版本 确保组件引用的属性与最新版本相同
const stateRef = useRef<T>(sharedStateRef.current)
// 使用proxy-compare获得一个代理 可以记录组件访问的属性
const affected = useMemo(() => new WeakMap(), [])
const proxyCache = useMemo(() => new WeakMap(), [])
const getStateProxyRef = useRef(() => {
return createProxy(stateRef.current, affected, proxyCache)
})
/** 存储用于更新组件的回调函数 */
const callbackRef = useRef<() => void>()
/** 共享状态监听函数 */
const listenerRef = useRef(() => {
if (
callbackRef.current
&& stateRef.current !== sharedStateRef.current
&& isChanged(stateRef.current, sharedStateRef.current, affected)
) {
stateRef.current = sharedStateRef.current
callbackRef.current()
}
})
const addListenerRef = useRef((callback: () => void) => {
if (!callbackRef.current) {
sharedStateListeners.add(listenerRef.current)
}
callbackRef.current = callback
})
const removeListenerRef = useRef(() => {
sharedStateListeners.delete(listenerRef.current)
callbackRef.current = undefined
})
return {
initState,
getStateProxy: getStateProxyRef.current,
updateSharedState,
addListener: addListenerRef.current,
removeListener: removeListenerRef.current,
}
}
/** 以常规方式更新组件 使用setState进行更新 */
const useStore = () => {
const { getStateProxy, updateSharedState, addListener, removeListener } = useCommon()
const [stateProxy, setStateProxy] = useState(getStateProxy)
const subscribe = useCallback(() => {
addListener(() => setStateProxy(getStateProxy))
}, [addListener, setStateProxy, getStateProxy])
subscribe()
useEffect(() => {
subscribe()
return removeListener
}, [removeListener, subscribe])
return [stateProxy, updateSharedState] as const
}
/** 以同步方式更新组件 用useSyncExternalStore进行更新 */
const useSyncStore = () => {
const { initState, getStateProxy, updateSharedState, addListener, removeListener } = useCommon()
const subscribe = useCallback((callback: () => void) => {
addListener(callback)
return removeListener
}, [addListener, removeListener])
const stateProxy = useSyncExternalStore(subscribe, getStateProxy, () => initState)
return [stateProxy, updateSharedState] as const
}
/**
* 以异步方式进行更新 更新期间Suspense会切换至fallback
* @param delay 在至少delay毫秒后更新才会结束
*/
const useSuspenseStore = (delay: number = 0) => {
const { getStateProxy, updateSharedState, addListener, removeListener } = useCommon()
const [stateProxy, setStateProxy] = useState(getStateProxy)
const [updatePromise, setUpdatePromise] = useState<Promise<void>>()
if (updatePromise) {
throw updatePromise
}
const subscribe = useCallback(() => {
addListener(() => {
setUpdatePromise(
new Promise<void>(resolve => setTimeout(() => {
setStateProxy(getStateProxy)
setUpdatePromise(undefined)
resolve()
}, delay))
)
})
}, [addListener, delay, setUpdatePromise, setStateProxy, getStateProxy])
subscribe()
useEffect(() => {
subscribe()
return removeListener
}, [removeListener, subscribe])
return [stateProxy, updateSharedState] as const
}
return {
/** 共享状态上下文 */
Privoider,
/** 以常规方式更新组件 */
useStore,
/** 以同步方式更新组件 */
useSyncStore,
/** 更新时Suspense切换至fallback */
useSuspenseStore
}
}
const getContextValue = <T>(initState: T) => {
const sharedStateRef = { current: initState }
const sharedStateListeners = new Set<() => void>()
const updateSharedState = (mutation: (draft: Draft<T>) => void) => {
sharedStateRef.current = produce(sharedStateRef.current, mutation)
sharedStateListeners.forEach(fn => fn())
}
const contextValue = {
initState,
sharedStateRef,
updateSharedState,
sharedStateListeners
}
return contextValue
}
使用例
import { createStore } from "@/utils/shared-state"
import { Suspense, useEffect } from "react"
const {
Privoider,
useStore,
useSyncStore,
useSuspenseStore
} = createStore({ text: 'text', num: 1 })
const Page = () => {
return (
<Privoider>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<Text></Text>
<Num></Num>
<Suspense fallback={'loading num...'}>
<NumSuspense></NumSuspense>
</Suspense>
</div>
</Privoider>
)
}
export default Page
const Text = () => {
const [sharedState, setSharedState] = useStore()
console.log('text')
useEffect(() => {
console.log('text effect')
})
return <input onChange={e => setSharedState(draft => { draft.text = e.target.value })} value={sharedState.text}></input>
}
const Num = () => {
const [sharedState, setSharedState] = useStore()
const increase = () => setSharedState(state => { state.num += 1 })
return <button onClick={increase}>num {sharedState.num}</button>
}
const NumSuspense = () => {
const [sharedState, setSharedState] = useSuspenseStore(1000)
console.log('num')
useEffect(() => {
console.log('num effect')
})
const increase = () => setSharedState(state => { state.num += 1 })
return <button onClick={increase}>num {sharedState.num}</button>
}
效果