数据重用与生命周期
上期我们封装了一个 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。