hox 状态管理库源码解析
admin
2024-01-29 02:15:45

文章目录

  • hox是什么
  • hox实现状态共享的方式
  • 基本使用
    • 全局状态共享
    • 局部状态共享
  • 源码解析
    • index.js 入口文件
    • coantainer.tsx 管理每个hook
    • 全局状态共享的实现
      • HoxRoot
      • create-global-store.tsx
      • useDataFromContainer
    • 局部状态共享的实现
      • createStore

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(
);
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 (
在这计数:{count}
); }export default App;
  • 局部状态共享

    • 不同 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 (
demo1...{banana}
); }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 (
在这计数:{count}
); }export default App;

源码解析

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'

coantainer.tsx 管理每个hook

  • 用于存储hook状态、以及订阅发布
type Subscriber = (data: T) => voidexport class Container}> {constructor(public hook: (props: P) => T) {}subscribers = new Set>()data!: Tnotify() {for (const subscriber of this.subscribers) {subscriber(this.data)}}
}
  • 这里的hook就是用户自定义的hook,也就是createStore里的内容
  • data 就是自定义hook返回的结果
export const [useAppleStore, AppleStoreProvider] = createStore(() => {const [banana, setBanana] = useState(['苹果'])return {banana,setBanana}
})

全局状态共享的实现

  • 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}>> = props => {// 订阅发布,返回的内容是所有和全局store对应的重新render时能够触发获取新状态的组件const executors = useSyncExternalStore(onStoreChange => {listeners.add(onStoreChange)return () => {listeners.delete(onStoreChange)}},() => {return globalExecutors})return (<>{executors.map((Executor, index) => (index} />))}{props.children})
}
  • 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(hook: () => T) {let container: Container | null = null// 获取传递给createStore自定义的hook对应的containerfunction 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对应的containerconst [innerContainer] = useState(() => new Container(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 {return useDataFromContainer(getContainer(), depsFn)}// 全局store状态的快照function getGlobalStore(): T | undefined {return getContainer().data}return [useGlobalStore, getGlobalStore] as const
}
  • 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(container: Container,depsFn?: DepsFn
): T {const depsFnRef = useRef(depsFn)depsFnRef.current = depsFn// 传入container.data获取老的stateconst depsRef = useRef(depsFnRef.current?.(container.data) || [])//useSyncExternalStore返回statereturn useSyncExternalStore(onStoreChange => {function subscribe() {// 这里做了优化,当只有指定的状态发生变化时,才会触发onStoreChange使得useSyncExternalStore返回新的状态if (!depsFnRef.current) {onStoreChange()} else {const oldDeps = depsRef.currentconst 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
}
  • 这里总结一下流程:当用户传入的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(() => {})export function createStore}>(hook: (props: P) => T,options?: CreateStoreOptions
) {const shouldMemo = options?.memo ?? true// TODO: forwardRefconst StoreContext = createContext>(fallbackContainer)const IsolatorContext = createContext({})const IsolatorOuter: FC}>> = props => {return ({}}>{props.children})}const IsolatorInner = memo}>>(props => {useContext(IsolatorContext)return <>{props.children}},() => true)//和全局store的render组件类似,不过这里是通过context拿到containerconst StoreExecutor = memo>(props => {const { children, ...p } = props// 每次都是重新生成一个container,所以能够做到数据隔离const [container] = useState(() => new Container(hook))container.data = hook(p as P)// 同意是在setState后触发订阅,获取store最新的状态useEffect(() => {container.notify()})return (container}>{props.children})})// 暴露给外部的Providerconst StoreProvider: FC> = props => {return (...props}>{props.children})}function useStore(depsFn?: DepsFn): T {// hook拿到container const container = useContext(StoreContext)if (container === fallbackContainer) {// TODOconsole.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
}
  • 总结一下局部状态共享的流程:获取最新状态的逻辑和全局状态共享一致,都是通过useEffect在setState后发布订阅,在useDataFromContainer中进行订阅。
  • 区别在于:全局状态将container统一使用一个全局数组管理,局部状态使用context传递container,个人觉得这样做的原因是全局store较少通过一个数组一起管理没问题,但一个项目可能有很多局部状态,所以通过一个数组管理每次render都去遍历整个数组消耗太大

相关内容

热门资讯

随笔|我把芫荽炒成菜 文|臧彦钧 馋人多半喜欢自己下厨。除了自己动手不用求人外,更多的是能在烹饪过程中自由发挥,抛开前人的...
4分钱的壳配3毛钱的肉?不少成... 对于热爱美食的当地人来说 在夜市或者美食街 都看到过这种“流量食物” ——蒜蓉粉丝烤扇贝 而且价格还...
全国首个以宋词为核心主题的演艺... 齐鲁晚报·齐鲁壹点 张浩穿越回李清照的时代与其展开时空对话,化身为玩家体验真实宋代生活……近日,在济...
“全球文旅轻创业计划”在京发布... 2025年11月17日上午,“银发文旅项目发布会暨全球文旅轻创业计划启动仪式”在中国传媒大学成功举办...
城事|办理口岸过百,台湾“首来... 据央视新闻消息,19日,国台办举行例行发布会,大陆持续释放旅游福利,首次来大陆的台胞“首来族”可获得...