• hox 状态管理库源码解析


    hox是什么

    hox文档

    • hox 想解决的问题,不是如何组织和操作数据,不是数据流的分层、异步、细粒度,我们希望 Hox 只聚焦于一个痛点:在多个组件间共享状态

    hox实现状态共享的方式

    • 首先通过自定义hook返回要共享的状态
      • 全局状态:全局数组+useSyncExternalStore + 用于render后重新发布订阅状态的组件
      • 局部状态:context + useSyncExternalStore + 用于render后重新发布订阅状态的组件
    • hox特别新颖的点:通过渲染一个空组件,利用组件的useEffect在每次渲染后进行判断数据是否更改

    基本使用

    • 全局状态共享

    import { HoxRoot } from './hox';
    
    const root = ReactDOM.createRoot(
      document.getElementById('root') as HTMLElement
    );
    root.render(
      <React.StrictMode>
        <HoxRoot>
          <App />
        </HoxRoot>
      </React.StrictMode>
    );
    reportWebVitals();
    =============================================
    import { createGlobalStore } from '../hox';
    import {useState} from 'react';
    
    function useStoreDemo(){
        const [count,setCount]=useState(0);
    
        return {
            count,
            setCount
        }
    }
    
    export const [useCountDemo] = createGlobalStore(useStoreDemo);
    ==========================================
    import React from 'react';
    import { useEffect } from 'react';
    import { useCountDemo } from './hooks/demo'
    
    function App() {
      
      const {count,setCount}=useCountDemo()
        
        return (
          <div className="App">
            <div>
              在这计数:{count}
            </div>
          </div>
        );
    }
    
    export default App;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 局部状态共享

      • 不同 StoreProvider 实例之间,数据是完全独立和隔离的
    import { useState } from 'react'
    import { createStore } from '../hox'
    
    export const [useAppleStore, AppleStoreProvider] = createStore(() => {
      const [banana, setBanana] = useState(['苹果'])
    
      return {
        banana,
        setBanana
      }
    })
    
    ===================================
    import { useEffect } from 'react';
    import { useCountDemo } from '../hooks/demo'
    import {useAppleStore} from '../hooks/appleDemo';
    // import {useBeerStore} from '../hooks/beerDemo';
    
    function App() {
      
      const {count,setCount}=useCountDemo()
      const {banana,setBanana} =useAppleStore()
    
      function changeCount(){
        setCount(count+1)
      }
      function changeApple(){
        setBanana(v=>[...v,'葡萄'])
      }
      
      return (
        <div className="demo1">
            demo1...
            <button onClick={changeApple}>水果计数</button>
            <span>{banana}</span>
        </div>
      );
    }
    
    export default App;
    ==================================================
    import React from 'react';
    import { useEffect } from 'react';
    import { useCountDemo } from './hooks/demo'
    import Demo1 from './components/demo1';
    import Demo2 from './components/demo2'
    import {AppleStoreProvider} from './hooks/appleDemo';
    
    function App() {
      
      const {count,setCount}=useCountDemo()
    
      
        return (
          <div className="App">
            <AppleStoreProvider>
              <Demo1></Demo1>
            </AppleStoreProvider>
      
            <AppleStoreProvider>
              <Demo2></Demo2>
            </AppleStoreProvider>
    
            <div>
              在这计数:{count}
            </div>
          </div>
        );
    }
    
    export default App;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    源码解析

    index.js 入口文件

    // 创建局部状态共享
    export { createStore } from './create-store'
    // 创建全局状态共享
    export { createGlobalStore } from './create-global-store'
    export { HoxRoot } from './hox-root'
    // 兼容类组件,这里暂不做分析
    export { withStore } from './with-store'
    
    export type { CreateStoreOptions } from './create-store'
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    coantainer.tsx 管理每个hook

    • 用于存储hook状态、以及订阅发布
    type Subscriber<T> = (data: T) => void
    
    export class Container<T = unknown, P = {}> {
      constructor(public hook: (props: P) => T) {}
      subscribers = new Set<Subscriber<T>>()
      data!: T
    
      notify() {
        for (const subscriber of this.subscribers) {
          subscriber(this.data)
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 这里的hook就是用户自定义的hook,也就是createStore里的内容
    • data 就是自定义hook返回的结果
    export const [useAppleStore, AppleStoreProvider] = createStore(() => {
      const [banana, setBanana] = useState(['苹果'])
      return {
        banana,
        setBanana
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    全局状态共享的实现

    • HoxRoot

    //create-global-store.tsx
    import React, { ComponentType, FC, PropsWithChildren } from 'react'
    import { useSyncExternalStore } from 'use-sync-external-store/shim'
    
    let globalExecutors: ComponentType[] = []
    
    const listeners = new Set<() => void>()
    
    // 每创建一个全局store,就会调用一次该方法
    export function registerGlobalExecutor(executor: ComponentType) {
      //用于收集重新render时能够触发获取新状态的组件
      globalExecutors = [...globalExecutors, executor]
      //当添加新的全局store时,调用收集到的onStoreChange
      //意味着可以创建多个全局store
      //通过useSyncExternalStore重新计算返回添加后的重新render时能够触发获取新状态的组件
      listeners.forEach(listener => listener())
    }
    
    export const HoxRoot: FC<PropsWithChildren<{}>> = props => {
     // 订阅发布,返回的内容是所有和全局store对应的重新render时能够触发获取新状态的组件
      const executors = useSyncExternalStore(
        onStoreChange => {
          listeners.add(onStoreChange)
          return () => {
            listeners.delete(onStoreChange)
          }
        },
        () => {
          return globalExecutors
        }
      )
    
      return (
        <>
          {executors.map((Executor, index) => (
            <Executor key={index} />
          ))}
          {props.children}
        </>
      )
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • create-global-store.tsx

    import { Container } from './container'
    import { registerGlobalExecutor } from './hox-root'
    import { useDataFromContainer } from './use-data-from-container'
    import { DepsFn } from './types'
    import { memo, useEffect, useState } from 'react'
    
    export function createGlobalStore<T>(hook: () => T) {
      let container: Container<T> | null = null
      // 获取传递给createStore自定义的hook对应的container
      function getContainer() {
        if (!container) {
          throw new Error(
            'Failed to retrieve data from global container. Please make sure you have rendered HoxRoot.'
          )
        }
        return container
      }
      
      // 重新render时能够触发获取新状态的组件,传递给registerGlobalExecutor
      // 通过useEffect在setState触发render后,通知重新计算状态
      const GlobalStoreExecutor = memo(() => {
        // 构建传入的hook对应的container
        const [innerContainer] = useState(() => new Container<T>(hook))
        container = innerContainer
        // 保存hook返回的状态
        innerContainer.data = hook()
        // 通过useEffect在setState触发render后,通知重新计算状态
        useEffect(() => {
          // 收集发生在use-datat-from-container中
          innerContainer.notify()
        })
        return null
      })
      
      // 将组件传递给HooxRoot进行创建
      registerGlobalExecutor(GlobalStoreExecutor)
      
      // useDataFromContainer进行收集订阅
      // depsFn是指定要获取的状态内容,不传返回全部
      function useGlobalStore(depsFn?: DepsFn<T>): T {
        return useDataFromContainer(getContainer(), depsFn)
      }
      // 全局store状态的快照
      function getGlobalStore(): T | undefined {
        return getContainer().data
      }
    
      return [useGlobalStore, getGlobalStore] as const
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • useDataFromContainer

      • 将传入createStore的hook,通过useSyncExternalStore订阅起来
      • 发生在用户使用 const {count,setCount}=useCountDemo();时
    //use-data-from-container.ts
    import { useRef } from 'react'
    import { Container } from './container'
    import { DepsFn } from './types'
    import { useSyncExternalStore } from 'use-sync-external-store/shim'
    
    export function useDataFromContainer<T, P>(
      container: Container<T, P>,
      depsFn?: DepsFn<T>
    ): T {
      const depsFnRef = useRef(depsFn)
      depsFnRef.current = depsFn
      // 传入container.data获取老的state
      const depsRef = useRef<unknown[]>(depsFnRef.current?.(container.data) || [])
      
      //useSyncExternalStore返回state
      return useSyncExternalStore(
        onStoreChange => {
          function subscribe() {
            // 这里做了优化,当只有指定的状态发生变化时,才会触发onStoreChange使得useSyncExternalStore返回新的状态
            if (!depsFnRef.current) {
              onStoreChange()
            } else {
              const oldDeps = depsRef.current
              const newDeps = depsFnRef.current(container.data)
              if (compare(oldDeps, newDeps)) {
                onStoreChange()
              }
              depsRef.current = newDeps
            }
          }
    
          container.subscribers.add(subscribe)
          return () => {
            container.subscribers.delete(subscribe)
          }
        },
        () => container.data
      )
    }
    
    function compare(oldDeps: unknown[], newDeps: unknown[]) {
      if (oldDeps.length !== newDeps.length) {
        return true
      }
      for (const index in newDeps) {
        if (oldDeps[index] !== newDeps[index]) {
          return true
        }
      }
      return false
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 这里总结一下流程:当用户传入的hook被createStore处理后,再调用hook对应的setState方法时,会触发重新render,又因为GlobalStoreExecutor通过registerGlobalExecutor将GlobalStoreExecutor组件渲染在了页面上
    • GlobalStoreExecutor中的useEffect会在每次渲染后触发,从而触发订阅的发布流程,最终useDataFromContainer里的useSyncExternalStore会被重新触发,进行判断后,返回新的store状态
    • 对于全局状态共享并没有使用context,直接通过将container保存进全局变量来实现

    局部状态共享的实现

    • createStore

    // create-store.tsx
    import React, {
      createContext,
      FC,
      memo,
      PropsWithChildren,
      useContext,
      useEffect,
      useState,
    } from 'react'
    import { Container } from './container'
    import { DepsFn } from './types'
    import { useDataFromContainer } from './use-data-from-container'
    
    export type CreateStoreOptions = {
      memo?: boolean
    }
    
    const fallbackContainer = new Container<any>(() => {})
    
    export function createStore<T, P = {}>(
      hook: (props: P) => T,
      options?: CreateStoreOptions
    ) {
      const shouldMemo = options?.memo ?? true
      // TODO: forwardRef
      const StoreContext = createContext<Container<T, P>>(fallbackContainer)
    
      const IsolatorContext = createContext({})
    
      const IsolatorOuter: FC<PropsWithChildren<{}>> = props => {
        return (
          <IsolatorContext.Provider value={{}}>
            {props.children}
          </IsolatorContext.Provider>
        )
      }
    
      const IsolatorInner = memo<PropsWithChildren<{}>>(
        props => {
          useContext(IsolatorContext)
          return <>{props.children}</>
        },
        () => true
      )
      //和全局store的render组件类似,不过这里是通过context拿到container
      const StoreExecutor = memo<PropsWithChildren<P>>(props => {
        const { children, ...p } = props
        // 每次都是重新生成一个container,所以能够做到数据隔离
        const [container] = useState(() => new Container<T, P>(hook))
        container.data = hook(p as P)
        // 同意是在setState后触发订阅,获取store最新的状态
        useEffect(() => {
          container.notify()
        })
        return (
          <StoreContext.Provider value={container}>
            {props.children}
          </StoreContext.Provider>
        )
      })
      // 暴露给外部的Provider
      const StoreProvider: FC<PropsWithChildren<P>> = props => {
        return (
          <IsolatorOuter>
            <StoreExecutor {...props}>
              <IsolatorInner>{props.children}</IsolatorInner>
            </StoreExecutor>
          </IsolatorOuter>
        )
      }
      
      function useStore(depsFn?: DepsFn<T>): T {
        // hook拿到container 
        const container = useContext(StoreContext)
        if (container === fallbackContainer) {
          // TODO
          console.error(
            "Failed to retrieve the store data from context. Seems like you didn't render a outer StoreProvider."
          )
        }
        // 这里的逻辑就和全局 store 一致
        return useDataFromContainer(container, depsFn)
      }
    
      return [useStore, shouldMemo ? memo(StoreProvider) : StoreProvider] as const
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 总结一下局部状态共享的流程:获取最新状态的逻辑和全局状态共享一致,都是通过useEffect在setState后发布订阅,在useDataFromContainer中进行订阅。
    • 区别在于:全局状态将container统一使用一个全局数组管理,局部状态使用context传递container,个人觉得这样做的原因是全局store较少通过一个数组一起管理没问题,但一个项目可能有很多局部状态,所以通过一个数组管理每次render都去遍历整个数组消耗太大
  • 相关阅读:
    【八大经典排序算法】快速排序
    测试用例的设计方法(全):错误推测方法及因果图方法
    苹果电脑mac系统运行卡顿 反应慢怎么办?
    我在Vscode学OpenCV 图像运算(权重、逻辑运算、掩码、位分解、数字水印)
    关于 国产系统UOS系统Qt开发Tcp服务器外部连接无法连接上USO系统 的解决方法
    Spring Security认证流程分
    python之xml文件的读取介绍
    看门狗 WDG
    社区动态——恭喜海豚调度中国区用户组新晋 9 枚“社群管理员”
    sqlserver配置管理器无法启动解决方案
  • 原文地址:https://blog.csdn.net/weixin_43294560/article/details/127904018