技术咨询、项目合作、广告投放、简历咨询、技术文档下载
点击这里 联系博主
# 基于 React.Context 的轻量化状态管理 umi useModel 分析
# 前言
在阅读 umi 文档的时候发现一个 useModel
(opens new window) 获取数据流的方式;其基于 约束 + namespace
的方式 令人耳目一新。为了一探究竟,看看 useModel
的魔法,遂翻看了其源码;发现 其 核心 基于 React.Context + useContext + 自定义hooks
的方式 着实轻量 且 实用。
在日常的开发中做状态管理,较多使用 reducer
/ React 自带的 useReducer
/ mobx
; 其 reducer
/useReducer
需要新同学有一定的前置知识才可以(比如 dispatch/action/store 等); mobx
对于 从 vue 转过来的同学较为友好。
而 useModel
是一个更加轻量的,不需要较多的学习成本即可学会状态管理(会 hooks 就行),能够快速的让新同学上手,且没有额外的依赖库引入。
聊聊源码前 先看看 useModel
的使用方式吧
定义
// src/models/userModel.ts
import { useState } from "react";
import { getUser } from "@/services/user";
export default () => {
const [user, setUser] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
getUser().then((res) => {
setUser(res);
setLoading(false);
});
}, []);
return {
user,
loading,
};
};
使用
// src/components/Username/index.tsx
import { useModel } from 'umi';
export default () => {
const { user, loading } = useModel('userModel');
return (
{loading ? <></>: <div>{user.username}</div>}
);
}
# 源码分析
那么 useModel
是如何实现 基于 namespace
的 跨组件 通信方式呢?
有兴趣的同学可以点击这里查看详细的源码 (opens new window)
其核心思路 大致如下:
- 基于
React.Context
创建一个全局的Context
;
const Context = React.createContext<{ dispatcher: Dispatcher }>(null);
class Dispatcher {
// 由于同一个 namespace 可以 在多处调用,所以意味着 针对单个namespace 存在多个依赖,在数据更新时需要更新多个依赖
callbacks: Record<Namespaces, Set<Function>> = {};
// 某个namespace 具体的model 值
data: Record<Namespaces, unknown> = {};
// 某一个namespace 的数据更新,需要同步所有的依赖地方
update = (namespace: Namespaces) => {
if (this.callbacks[namespace]) {
this.callbacks[namespace].forEach((cb) => {
try {
const data = this.data[namespace];
cb(data);
} catch (e) {
cb(undefined);
}
});
}
};
}
- 使用
Context.Provider
包裹跟组件,并执行所有的useModel
对应的hooks
// 初始化一个具体的 Dispatcher 实例
const dispatcher = new Dispatcher();
export function Provider(props: {
models: Record<string, any>;
children: React.ReactNode;
}) {
return (
// 这样在 react 的所有子组件中 可以使用 useContext 获取到全局 dispatcher
<Context.Provider value={{ dispatcher }}>
{Object.keys(props.models).map((namespace) => {
return (
<Executor
key={namespace}
hook={props.models[namespace]}
namespace={namespace}
onUpdate={(val) => {
dispatcher.data[namespace] = val;
dispatcher.update(namespace);
}}
/>
);
})}
{props.children}
</Context.Provider>
);
}
interface ExecutorProps {
hook: () => any;
onUpdate: (val: any) => void;
namespace: string;
}
function Executor(props: ExecutorProps) {
const { hook, onUpdate, namespace } = props;
const updateRef = useRef(onUpdate);
const initialLoad = useRef(false);
let data: any;
try {
// 执行 对应namespace 的 hooks 函数
data = hook();
} catch (e) {
console.error(
`plugin-model: Invoking '${namespace || "unknown"}' model failed:`,
e
);
}
// 首次执行时立刻返回初始值
useMemo(() => {
updateRef.current(data);
}, []);
// React 16.13 后 update 函数用 useEffect 包裹
useEffect(() => {
// 保证初始化 之后 一直更新数据,那么可以通过 onUpdate 获取到 对应 model 的 最新数据
if (initialLoad.current) {
updateRef.current(data);
} else {
initialLoad.current = true;
}
});
return null;
}
- 在
useModel
函数中通过useContext
获取到dispatcher
, 并更新数据
tips:
useModel
本身其实就是一个 Hooks 函数
- step1: 使用第一步创建的
Context
获取到dispatcher
; - step2: 通过
dispatcher
初始化当前的namespace
的 值,并使用state
记录; - step3: 由于将当前组件使用到了 某个
namespace
的值,所以需要 将当前组件 依赖记录到 对应namespace
下面的 依赖列表中(callbacks
, 其实我觉得叫deps
似乎更合适) - step4: 通过触发
dispatcher
的update
方法,给对应namespace
下面的 所有依赖更新数据 - step5: 通过
useRef
记录之前的值和当前的值,如果有变化则setState
; 最后 返回state
export function useModel<N extends Namespaces, S>(
namespace: N,
selector?: Selector<N, S>
): SelectedModel<N, typeof selector> {
// 使用第一步创建的Context 获取到 dispatcher
const { dispatcher } = useContext<{ dispatcher: Dispatcher }>(Context);
const selectorRef = useRef(selector);
selectorRef.current = selector;
// 通过dispatcher 初始化当前的 namespace 的 值
const [state, setState] = useState(() =>
selectorRef.current
? selectorRef.current(dispatcher.data[namespace])
: dispatcher.data[namespace]
);
const stateRef = useRef<any>(state);
stateRef.current = state;
// 判断当前组件是否挂载,保证只使用一次
const isMount = useRef(false);
useEffect(() => {
isMount.current = true;
return () => {
isMount.current = false;
};
}, []);
useEffect(() => {
const handler = (data: any) => {
// 保证值调用一次
if (!isMount.current) {
// 如果 handler 执行过程中,组件被卸载了,则强制更新全局 data
// TODO: 需要加个 example 测试
setTimeout(() => {
dispatcher.data[namespace] = data;
dispatcher.update(namespace);
});
} else {
const currentState = selectorRef.current
? selectorRef.current(data) // 用户可能对useModel 设置了第二个参数,需要挑选部分值
: data;
const previousState = stateRef.current;
if (!isEqual(currentState, previousState)) {
// 避免 currentState 拿到的数据是老的,从而导致 isEqual 比对逻辑有问题
stateRef.current = currentState;
// 设置最新的 namespace 的值给state
setState(currentState);
}
}
};
dispatcher.callbacks[namespace] ||= new Set() as any; // rawModels 是 umi 动态生成的文件,导致前面 callback[namespace] 的类型无法推导出来,所以用 as any 来忽略掉
// 由于将当前组件使用到了 某个 namespace 的值,所以需要 将当前组件 依赖记录到 对应 namespace 下面的 依赖列表中(callbacks, 其实我觉得叫 deps 似乎更合适)
dispatcher.callbacks[namespace].add(handler);
dispatcher.update(namespace);
return () => {
dispatcher.callbacks[namespace].delete(handler);
};
}, [namespace]);
return state;
}
- 本文链接: https://mrgaogang.github.io/react/umi_usemodel.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!