写一个状态管理工具

89 阅读3分钟

查看仓库

简单构建

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>
}

效果 test1.gif