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

文章目录

  • 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都去遍历整个数组消耗太大

相关内容

热门资讯

宁波一游乐园内装饰树起火,工作... 7月6日,多位网友在社交平台发视频称,浙江宁波罗蒙环球乐园一棵圣诞树突然起火。 现场视频显示,一处游...
快乐不放假 公园玩一夏 全市4... 深圳新闻网2025年7月7日讯(深圳商报记者 文灿)7月5日,深圳公园“快乐不放假,公园玩一夏”夏日...
为了帮女儿照顾孩子,我和儿媳妇... 为了帮女儿照顾孩子,我和儿媳妇闹翻了01 “带着你的外孙走。” 一声尖锐的叫喊声过后,传来门被重...
孩子课上听不懂老师讲的怎么办? 孩子课上听不懂老师讲的怎么办?到外面培训机构培训一下,比如新东方教育之类的孩子刚上一年级,课上听不懂...
安徽一爸爸火车上睡着,儿子走丢... 安徽一爸爸火车上睡着,儿子走丢了都不知道,爸爸带娃到底有多靠谱?实在是太不靠谱了,这样的行为实在是太...
大家支持小学生向老师举报、告状... 大家支持小学生向老师举报、告状、告密吗?我个人不支持这样的做法,这样有个弊端,可能把孩子引向一个背后...
麻烦各位推荐几本好看的穿越小说 麻烦各位推荐几本好看的穿越小说绾青丝 独步天下 醉玲珑 丑颜倾城芊泽花,醉玲珑,跨过千年来爱你,暴...
星际战甲怎么一键分解所有重复m... 星际战甲怎么一键分解所有重复mod对着mod,按鼠标中键,即可全选该mod。按重复数量排序,即可方便...
电视剧《五星大饭店》有续集吗?... 电视剧《五星大饭店》有续集吗?是什么结局啊?没有续集。话说没有拍续集的打算。一般现在看的续集都是网友...
高中会考没过就一定没高中毕业证... 高中会考没过就一定没高中毕业证吗?高中会考没过就一定没高中毕业证吗不一定那,没过也可以领的当然不是,...
为什么我的仙剑奇侠传四玩道慕容... 为什么我的仙剑奇侠传四玩道慕容紫英那就卡掉,不能继续啊?我仙四一到紫英那放动画那就卡住,然后关闭了啊...
YES!光之美少女5GOGO有... YES!光之美少女5GOGO有哪些剧场版有YES,光之美少女剧场版1、光之美少女剧场版2、光之美少女...
住家保姆的服务内容有哪些 住家保姆的服务内容有哪些这个要跟保姆自己谈了要看你给的价格了
钓鱼坠子的制作方法有什么? 钓鱼坠子的制作方法有什么?进行海竿抛钩时,由于水底不平或水草、树根过多,挂钩失坠时有发生。因此在垂钓...
问下,图里的那个动漫人物是出自... 问下,图里的那个动漫人物是出自哪个动漫薄樱鬼斋藤一出自动漫《薄樱鬼》人物名为:斋藤一这是原图我觉得像...
死神黑崎一护是不是灭却师?第二... 死神黑崎一护是不是灭却师?第二部什么时候出?一护妈妈是灭却师,爸爸是死神,所以算个混血儿→ →也就是...
点到为止的意思 点到为止的意思点到为止:说话时只是轻轻触及话题的边缘,而不必深入谈论,但是已经让对方明了意图即可到了...
《余生请多指教》热播,顾医生身... 《余生请多指教》热播,顾医生身上有哪些特质?特别温柔,特别专一,会安慰人,会讲大道理,特别暖心。顾医...
浩态狂香昔未逢,红灯烁烁绿盘龙... 浩态狂香昔未逢,红灯烁烁绿盘龙,觉来独对情惊恐,身在仙宫第九重。韩愈的哪一首诗。名字啊什麼?顺便求诗...