上下文存储【下】

数据重用与生命周期

上期我们封装了一个 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。

完整代码

相关推荐
Hi_kenyon10 小时前
小白理解main.js
前端·javascript·vue.js
ID_1800790547310 小时前
淘宝平台商品详情API(item_get)深度解析
java·服务器·前端
毕设源码-郭学长10 小时前
【开题答辩全过程】以 基于Web的文档管理系统的设计与实现为例,包含答辩的问题和答案
前端
Rhys..10 小时前
Playwright + JS 进行页面跳转测试
开发语言·前端·javascript
We་ct10 小时前
LeetCode 135. 分发糖果:双向约束下的最小糖果分配方案
前端·算法·leetcode·typescript
Yan.love10 小时前
【CSS-核心属性】“高频词”速查清单
前端·css
广州华水科技10 小时前
如何通过GNSS位移监测提升单北斗变形监测系统的精度与应用效果?
前端
郭优秀的笔记10 小时前
html鼠标悬浮提示功能
android·javascript·html
慧一居士10 小时前
npm install 各参数使用说明, 和使用场景说明
前端
冰暮流星10 小时前
if与switch的区分
javascript