首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

useModel 源码如此简单

  • 24-12-16 16:06
  • 3044
  • 10891
juejin.cn

之前在项目中一直用到 umi 中 useModel 这种状态管理, 一起来探索一下它是如何实现的

使用

使用 umi.js 可以 在 src/models 下面新建一个文件夹,例如 countModel.ts

javascript
代码解读
复制代码
import { useCallback, useState } from 'react'; ​ export default function CounterModel() {  const [counter, setCounter] = useState(0); ​  const increment = useCallback(() => setCounter((c) => c + 1), []);  const decrement = useCallback(() => setCounter((c) => c - 1), []); ​  return { counter, increment, decrement }; } ​

在组件中使用 即可

javascript
代码解读
复制代码
​ const Count = () => {    const { add, minus, counter } = useModel('countModel', (model) => ({    counter: model?.counter,    add: model?.increment,    minus: model?.decrement, }));    return ( <div>   <div>          <div>{counter}div>          <Button onClick={add}>加 1Button>          <Button onClick={minus}>减 1Button>        div>    div> ) }

源码

useModel 本质上其实是使用了 context 来进行存储的,我们来看下代码,在 .umi 下的 plugin-model 文件夹中 包含下面三个文件

diff
代码解读
复制代码
index.tsx model.ts runtime.tsx

我们可以看到,在 model.ts 中,其实他导出了我们业务代码里所有的 model, 并且namespace 是文件名称,model 的值实际上就是model 中我们导出的东西

model.ts

css
代码解读
复制代码
// model.ts ​ import model_1 from '/home/admin/src/models/counterModel'; import model_2 from '/home/admin/src/models/global'; import model_3 from '/home/admin/src/.umi/plugin-initialState/@@initialState'; import model_4 from '/home/admin/src/.umi/plugin-qiankun-slave/qiankunModel'; ​ export const models = {  model_1: { namespace: 'counterModel', model: model_1 },  model_2: { namespace: 'global', model: model_2 },  model_3: { namespace: '@@initialState', model: model_3 },  model_4: { namespace: '@@qiankunStateFromMaster', model: model_4 }, } as const

导出了这些 models 后,在 runtime.tsx 中进行使用,我们看下这个文件

runtime.tsx

javascript
代码解读
复制代码
// runtime.tsx ​ import React  from 'react'; import { Provider } from './'; import { models as rawModels } from './model'; ​ function ProviderWrapper(props: any) {  const models = React.useMemo(() => {    return Object.keys(rawModels).reduce((memo, key) => {      memo[rawModels[key].namespace] = rawModels[key].model;      return memo;   }, {}); }, []);  return <Provider models={models} {...props}>{ props.children }Provider> } ​ export function dataflowProvider(container, opts) {  return <ProviderWrapper {...opts}>{ container }ProviderWrapper>; }

我们看到,这个文件里面创建了一个 ProviderWrapper, 然后通过 Provider 将 models 传入到 children 中,最后导出了 dataflowProvider 这个参数,在 umi 内部,其实他是会将这个 ProviderWrapper 最终放到我们的组件最外层,也就是根组件进行包裹,这样里面所有的子组件都可以访问到 models, umi.js 在插件运行的时候,会执行 runtime.tsx 文件中导出的 dataflowProvider 方法,他会在给 react-dom 渲染根组件的时候,在外面包裹一层

这种方式会导致一个问题

当我们自己给根组件添加一个 provider 的时候,就会导致 ProviderWrapper 在 provider 里面,useModel 只有在 ProviderWrapper 内才可以使用,所以我们在自己定义的根 provider 里是无法使用 useModel 的

然后我们来看下 Provider, 他是从 index.tsx 中导出

Provider

ini
代码解读
复制代码
const Context = React.createContext<{ dispatcher: Dispatcher }>(null); ​ export function Provider(props: {  models: Record;  children: React.ReactNode; }) {  return (    value={{ dispatcher }}>     {Object.keys(props.models).map((namespace) => {        return (                      key={namespace}            hook={props.models[namespace]}            namespace={namespace}            onUpdate={(val) => {              dispatcher.data[namespace] = val;              dispatcher.update(namespace);           }}          />       );     })}     {props.children}     ); }

创建一个 context, 然后传入 models, 还有 children, 以及 value={{ dispatcher }}。最后 返回 children ,以及 Executor 组件

我们知道,children 就是所有的子组件. Models 是我们写的所有的 model, 那么 我们将 namespace 和 model 都传给了 Executor 组件,并且还传入了一个 onUpdate 函数,这个函数执行了 dispatcher 的一些方法

那么 这个dispatcher 到底是什么呢?我们来看下他的定义

typescript
代码解读
复制代码
class Dispatcher {  callbacks: Record<Namespaces, Set<Function>> = {};  data: Record<Namespaces, unknown> = {};  update = (namespace: Namespaces) => {    if (this.callbacks[namespace]) {      this.callbacks[namespace].forEach((cb) => {        try {          const data = this.data[namespace];          cb(data);       } catch (e) {          cb(undefined);       }     });   } }; } ​ const dispatcher = new Dispatcher();

dispatcher 其实就是 Dispatcher 的一个实例,而 Dispatcher 不就类似于发布订阅吗? update 将存入的 callbacks 全部取出来,然后传入 data 数据。这里我们先暂留一个疑问就是, callbacks 的回调是什么时候被放入进去的呢?我们接着往下看

Provider 内部还写了一个 Executor,它是用来干什么的呢?

ini
代码解读
复制代码
function Executor(props: ExecutorProps) {  const { hook, onUpdate, namespace } = props; ​  const updateRef = useRef(onUpdate);  const initialLoad = useRef(false); ​  let data: any;  try {    data = hook(); } catch (e) {    console.error(      `plugin-model: Invoking '${namespace || 'unknown'}' model failed:`,      e,   ); } ​  // 首次执行时立刻返回初始值  useMemo(() => {    updateRef.current(data); }, []); ​  // React 16.13 后 update 函数用 useEffect 包裹  useEffect(() => {    if (initialLoad.current) {      updateRef.current(data);   } else {      initialLoad.current = true;   } }); ​  return null; }

hook 是什么?不就是我们写的 model 函数吗, 当调用首次执行 data = hook(); 拿到我们写的 model 函数的初始值的时候,然后调用 updateRef.current(data),将 data 传入到 onUpdate 函数中

ini
代码解读
复制代码
onUpdate={(val) => {  dispatcher.data[namespace] = val;  dispatcher.update(namespace); }}

这样就将数据传入到了我们的 dispatcher.data 中去了,而 dispatcher.update(namespace) 一开始调用的时候,还没有 callback 函数,所以也不会执行

所以, 实际上就是为了给 dispatcher 设置值,每次 Executor 重新渲染,都会调用 updateRef.current(data) 设置值

那 Executor 是如何重新渲染的呢?其实就是 hook() 执行, data = hook(), 每当 hook 中的状态改变后(也就是我们写的model) 都会导致 重新渲染

所以我们可以看出 useModel 的一个问题, 定义 model 后,即使没有使用,源码中的 model hook 也会被自动执行,这样一些异步请求的操作,就会在没使用就被触发

然后我们就可以看 useModel 是如何进行使用的了

ini
代码解读
复制代码
export function useModel(  namespace: N,  selector?: Selector, ): SelectedModel {  const { dispatcher } = useContext<{ dispatcher: Dispatcher }>(Context);  const selectorRef = useRef(selector);  selectorRef.current = selector;  const [state, setState] = useState(() =>    selectorRef.current      ? selectorRef.current(dispatcher.data[namespace])     : dispatcher.data[namespace], );  const stateRef = useRef(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)         : data;        const previousState = stateRef.current;        if (!isEqual(currentState, previousState)) {          // 避免 currentState 拿到的数据是老的,从而导致 isEqual 比对逻辑有问题          stateRef.current = currentState;          setState(currentState);       }     }   }; ​    dispatcher.callbacks[namespace] ||= new Set() as any; // rawModels 是 umi 动态生成的文件,导致前面 callback[namespace] 的类型无法推导出来,所以用 as any 来忽略掉    dispatcher.callbacks[namespace].add(handler);    dispatcher.update(namespace); ​    return () => {      dispatcher.callbacks[namespace].delete(handler);   }; }, [namespace]); ​  return state; } ​

通过 useContext 拿到 dispatcher. 然后通过 dispatcher.data[namespace] 拿到我们自定义store里面的返回值。并且不只在State中进行了存储,还利用 useRef(data) 在 StateRef 中存储了一份

然后我们看到 这里 dispatcher.callbacks[namespace] 是一个set, 然后往里面放了 callback 回调,也就是handler 函数,然后调用了 updae(namespace) 这个函数

scss
代码解读
复制代码
 useEffect(() => { .....,    dispatcher.callbacks[namespace] ||= new Set() as any;    dispatcher.callbacks[namespace].add(handler);    dispatcher.update(namespace); ​    return () => {      dispatcher.callbacks[namespace].delete(handler);   }; }, [namespace]);

而 handler 函数做了什么呢?

ini
代码解读
复制代码
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)     : data;    const previousState = stateRef.current;    if (!isEqual(currentState, previousState)) {      // 避免 currentState 拿到的数据是老的,从而导致 isEqual 比对逻辑有问题      stateRef.current = currentState;      setState(currentState);   } } };

handle 会接收一个data 参数,这个参数是最新的值,因为在 const data = this.data[namespace]; cb(data); 的时候,在 Executor 每次渲染的是,将 store 中最新的data 通过 onUpdate 传入, isMount.current 初次进来会变为true,我们可以看 else 分支, 其实这里就是对新老数据通过 isEqual 进行了一个对比,然后调用 setState 更新最新的State, useModel 在组件中使用,则调用 setState 会进行渲染业务组件更新视,也就是说usemodel 中的状态发生改变,会由 Executor 执行 update 更新, 然后重新渲染页面

到这里,其实 useModel 的源码就已经结束了,我们在整体来捋一遍

javascript
代码解读
复制代码
const Count = () => {    const { add, minus, counter } = useModel('countModel', (model) => ({    counter: model?.counter,    add: model?.increment,    minus: model?.decrement, }));    return ( <div>   <div>          <div>{counter}div>          <Button onClick={add}>加 1Button>          <Button onClick={minus}>减 1Button>        div>    div> ) }
  • umi.js 会创建一个 provider 放到根组件下,之后传入所有的 models
  • 创建一个 Dispatcher 来存放 callbacks 以及 data, 以及提供一个更新函数 update
  • Update 会通过调用 cb 来进行判断,cb 其实就是 handle 来进行判断状态前后有没有变化,有变化就进行 setState 来刷新视图
  • 创建一个 context, 用来给子组件提供 dispatcher
  • 通过 Executor 来更新 dispatcher 中的数据

总结

当我们调用 add 这个函数,修改了 model 中的状态 counter, 这个时候在 Executor 会执行 data=hook(), 会导致 Executor 组件重新渲染,然后将他调用了 onUpdate 方法,将最新的返回值设置到 dispatcher 中,然后调用 dispatcher 的 update 方法,update 会进行调用每一个回调 cb 将最新的值传入回调 handler 中去,然后 通过 state 和 上一次的 stateRef 来进行对比,如果不一样,就调用 useModel 函数中的 setState 设置最新的 state,触发组件重新渲染。这就是整个源码流程

注:本文转载自juejin.cn的你捉不到的this的文章"https://juejin.cn/post/7448826638437466163"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top