上下文存储【下】

数据重用与生命周期

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

完整代码

相关推荐
Pluto_CRown2 小时前
H5 开发的各类小知识点
前端·javascript
AAA阿giao2 小时前
JavaScript 中基于原型和原型链的继承方式详解
前端·javascript·面试
用户600071819102 小时前
【翻译】如何在Vue中使用Suspense处理异步渲染?
前端
acaiEncode2 小时前
nvm use xxx 报错: exit status 145: The directory is not empty.
前端·node.js
三秦赵哥2 小时前
Prompt 优化教程
前端
光影少年2 小时前
react怎么实现响应式?
前端·react.js·前端框架
奋斗吧程序媛2 小时前
Vue Router的路由模式
前端·javascript·vue.js
by__csdn2 小时前
Vue.js 生命周期全解析:从创建到销毁的完整指南
前端·javascript·vue.js·前端框架·ecmascript·css3·html5
m0_471199632 小时前
【JavaScript】前端如何处理服务端部分接口加解密
开发语言·前端·javascript