上下文存储【下】

数据重用与生命周期

上期我们封装了一个 zustand-utils 的上下文存储,这期我们来扩展这个函数的功能,使得它更好用。

数据位于上下文中,则受到了 React 组件位置的限制,没了全局数据的优势, 如果这套 api 不仅能让 React 组件无感实现局部上下文,又能将数据当作普通对象那样随意取用传递,那在面对更多业务场景的时候才能游刃有余。

参考 react-form 的想法,通过 useForm 创建一个表单,绑定到组件后无需关心数据更新问题,最后提交时只需 form.getFieldsValue 即可,至于用去干嘛。。和库没关系。

上一次,我们将 useSelector 的 selector 参数包装为必传参数,防止有人不传使得组件频繁刷新。本着奥卡姆剃刀的原则,我们可以用上这个无参的同名函数

javascript 复制代码
	function useFactoryStore(): StoreApi<StoreType>;
	function useFactoryStore<T>(selector: (store: StoreType) => T): T;
	function useFactoryStore<T>(selector?: (store: StoreType) => T): T | StoreApi<StoreType>
    const store = useContext(StoreContext);
    if (!store) {
        throw new Error(`Missing Provider!`);
    }
    if(!selector) {
        return store;
    }
    return useStore(store, selector)
}

注意这里类型也扩展了

当不传递 selector 的时候,直接返回 store 实体,而不是 store 的数据内容

这时,我们想要两个组件公用一个上下文就很方便了,在移动端做数据编辑操作的时候,一屏没有办法装下全部操作项的时候,我们一般用弹窗作为载体展示部分子表单。弹窗的上下文一般就不在原页面的范围下。这时候可以

tsx 复制代码
const FormPage = () => {
    return <FormProvider init={{}}>
        <FullForm />
    </FormProvider>
}

const FullForm = () => {
    const form = useFormStore()
    return <Form>
        <FormItem onClick={() => {
            openEditorModal(form) // 封装某个form的挂载点
        }>
            <FormPreviewer data={form}>
        </FormItem>
    <Form>
}

const EditFormModal = ({ formStore }:{ formStore: FormStore }) => {
    // 传递到另一个Provider中使用
    return <FormProvider store={formStore}>
        <FormEditor>
    </FormProvider>
}

这样 Form 的 Previewer 和 Editor 就能共享同一份数据了,通过上下文又能让页面上可以存在多个 form,通过存储有能让数据跨多层组件管理而不用通过 props 传递,减少刷新,集成了多种优势。

上面没提到 Provider 也要相应更新另一种初始化方式,代码变更如下

javascript 复制代码
interface ProviderProps {
    children: ReactNode;
    /** 传入初始化该store的内容,结构与store一致 */
    init?: Partial<StoreType>;
    /** 传入StoreApi创建上下文的引用,需要通过不传selector的useStore获取 */
    reference?: StoreInstance;
}
const StoreProvider = ({ children, init, store }: ProviderProps) => {
    const storeRef = useRef<StoreInstance | null>(null);
    if (!storeRef.current) {
        if(init) storeRef.current = StoreBuilder(init);
        else if(store) storeRef.current = store
        else throw new Error("StoreProvider 没有任何一种初始化方式!")
    }
    return <StoreContext.Provider value={storeRef.current!}>{children}</StoreContext.Provider>;
};

通过 ID 共享

跨组件传递 store 都能把整个 store 实体扔过去了,如果能建立一个 id,直接从全局取货就好了。例如下面这种调用方法

javascript 复制代码
const Page1 = () => {
  return (
    <FileProvider init={{ file: readFile(path) }} id={`file:${path}`}>
      <Content />
    </FileProvider>
  );
};

const Page2 = () => {
  return (
    // 这里的 init 不会被调用!
    <TestColorProvider init={{ file: readFile(path) }} id={`file:${path}`}>
      <Content />
    </TestColorProvider>
  );
};

如果你在做一个类似 Vscode 这样的编辑器,用户对一个文件打开两个编辑窗(Tab)是很有可能的,这时候我们就没有必要传递第一个 tab 的 store 到第二个 tab,他们本身没有优先级关系,反而是文件路径应该作为他们的唯一 id,让他们表面上处于不同的组件树中,但实际上共用的是同一份数据,还能省去文件内容重复解析的性能消耗。

实现如下

javascript 复制代码
interface ProviderProps {
    children: ReactNode;
    /** 传入初始化该store的内容,结构与store一致 */
    init?: Partial<StoreType>;
    /** 传入StoreApi创建上下文的引用,需要通过不传selector的useStore获取 */
    store?: StoreInstance;
    /** 根据id自动重用Store*/
    id?: string;
}
// 全部上下文store会存一份在这里
const SharedStore = new Map<string,object>();

const StoreProvider = ({ children, init, store, id }: ProviderProps) => {
    const storeRef = useRef<StoreInstance | null>(null);
    if (!storeRef.current) {
        if(init) {
            if(id && SharedStore.has(id)) {
                storeRef.current = SharedStore.get(id);
            } else {
                storeRef.current = StoreBuilder(init);
                if(id) SharedStore.set(id, storeRef.current)
            }
        }
        else if(store) {
            storeRef.current = store
        }
        else throw new Error("StoreProvider 没有任何一种初始化方式!")
    }
    return <StoreContext.Provider value={storeRef.current!}>{children}</StoreContext.Provider>;
};

嗯,功能是实现了,但是 SharedStore 绝对是一个无限增长的内存实体,一定要在 useEffect 中做一个手动的引用计数去清理缓存的 Store

tsx 复制代码
const SharedStoreCount = new Map<string, number>();

useEffect(() => {
  if (!id) return;
  SharedStoreCount.set(id, (SharedStoreCount.get(id) || 0) + 1);
  return () => {
    SharedStoreCount.set(id, (SharedStoreCount.get(id) || 0) - 1);
    const count = SharedStoreCount.get(id);
    if (count <= 0) SharedStore.delete(id);
  };
}, []);

结尾

当我们允许外部共享存储的时候,存储的生命周期就变为了 既不是全局变量,又不随组件卸载的一个对象。 数据的存活周期内,如果有一些关联的监听器怎么办?例如我们需要监听系统外部的更新 api,确保存储内的数据不是因为一直存活而无法更新。

如果我们挂在第一个创建该 store 的组件中,那它卸载后可能连带着监听器一起卸载掉,如果挂在每一个有 provider 组件的组件中,那遇到一些不可重复监听的事件(例如补发 diff 数据的事件)会带来麻烦。

最好的方法就是把 effect 和 store 本身绑定,而非和组件绑定,effect 随 store 创建和消亡,同样通过一个引用计数来管理。

实现方式大差不差,这里留作课后作业吧,需要注意的是 id 不是必选参数,你可能需要用 store 对象本身作为存储 Map 的 key。

完整代码

相关推荐
吃杠碰小鸡16 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone22 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_090141 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝3 小时前
RBAC前端架构-01:项目初始化
前端·架构