[vuex4] 让你彻底了解模块

前言

为什么要使用模块?

由于使用单一的 state tree, state 会变成一个臃肿的对象,我们可以使用模块,每一个模块都可以包含属于自己的 state / mutations / getters / actions,甚至可以嵌套,形如

js 复制代码
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

可以根据不同功能编写不同的模块,这样做不仅可以做到代码功能清晰,也可以避免写出长代码

格式化

虽然使用起来很方便,但是这个实现起来可一点不方便

对于用户传递的嵌套格式,需要进行格式化我们可以更方便处理的数据结构

对于嵌套结构什么数据结构是方便处理的呢?

对于可以无限嵌套的结构,使用 这个数据结构是最好不过了

ok,那我们就构建出一棵树
假如我们的传递的是这样的数据[点击查看👋]

js 复制代码
 state: {
   namespaced: true,
   name: "zs",
 },
 modules: {
   aCount: {
     namespaced: true,
     state: {
       name: "zs2",
     },
     mutations: {
       add2(state) {
         console.log("aaa")
          state.name = 'lisi'
       }
     },
     modules: {
       bCount: {
         namespaced: true,
         state: {
           name: "zs3",
         },
         mutations: {
           add(state) {
             console.log("bbb")
              state.name = 'wangwu'
           }
         }
       }
     }
   }
 }

使用 ModuleCollection 类来处理,因为类方便扩展

js 复制代码
new ModuleCollection(options)

我们需要构建出形如

js 复制代码
{
  state: { name: 'zs', aCount: {...}},
  _children: {aCount: Module},
  _rawModule: {state: {...}, modules: {...}}
}
  1. state 是方便获取模块中的 state
  2. _children 是为了 构建出一个子父查找链, 方便找到子模块
  3. _rawModule 保存原始数据

ok,我们来看看 ModuleCollection

其中的核心难点就在于查找,我们使用一个简单的🌰来梳理一下执行顺序

modules 中有嵌套了 aModulebModule, 其中在 aModule 中又嵌套了 a1Module

  1. path 为空数组,newModule 为根模块

    发现 newModule 中还有模块,遍历所有内部模块,key值是对象名,即 aModulebModule, 然后递归调用

  2. 现在 path 为 [aModule]

    现在 path 不为 [],进入到 else 分支, 使用 slice 把最后一个去掉,因为最后一个是自己, 执行 get 方法找到所有父级 ,只要最近的父级把自己给添加进去

  3. 发现 aModule 下面还有模块,那么 path 变为 [aModule,a1Module],然后执行递归

  4. a1Module 下没有新的模块,那么就轮到到 bModule执行了

经过这样一来,我们的数据结构就变成了


上述源代码 [点击查看👋]

js 复制代码
function forEachValue(obj, fn) {
 Object.keys(obj).forEach(key => fn(obj[key], key))
}

class Module {
 constructor(rawModule) {
   this._children = Object.create(null)
   this._rawModule = rawModule
 }
 addChild(key, module) {
   this._children[key] = module
 }

 getChild(key) {
   return this._children[key]
 }
}

class ModuleCollection {
 constructor(rawRootModule) {
   this.register([], rawRootModule)
 }

 register(path, rawModule) {
   const newModule = new Module(rawModule)
   // 说明是根目录
   if (path.length === 0) {
     this.root = newModule
   } else {
     // 找到父级
     //  [ 'aModule' ] [ 'aModule', 'a1Module' ] [ 'bModule' ]
     const parent = this.get(path.slice(0, -1))
     console.log(" parent:", parent);
     // 父级添加子模块
     parent.addChild(path[path.length - 1], newModule)
   }

   // 说明有子 modules,需要递归注册
   if (rawModule.modules) {
     forEachValue(rawModule.modules, (rawChildModule, key) => {
       // 递归注册
       this.register(path.concat(key), rawChildModule)
     })
   }
 }

 //找到所有的
 get(path) {
   return path.reduce((module, key) => {
     return module.getChild(key)
   }, this.root)
 }
}

let x = new ModuleCollection({
 modules: {
   aModule: {
     modules: {
       a1Module: {

       }
     }
   },
   bModule: {}
 }
})

到这一步,格式化数据已经完成了,剩下的就是对 state / commit 等api 的操作了

state

操作 state 和 格式化数据结构是一样的思路

js 复制代码
const state = this._modules.root.state;
installModule(this, state, [], this._modules.root);

以模块名为 key,value 为模块中的 state 作为对象挂载到 根state 上

🚝 这样你就完成了 store.state.aModule.state重要的一步了(还需要其他方法配合)
上述源代码,可以直接复制执行 [点击查看👋]

js 复制代码
function getNestedState(state, path) {
 return path.reduce((state, key) => state[key], state)
}

function installModule( rootState, path, module) {
const isRoot = !path.length

if (!isRoot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  parentState[moduleName] = module.state
}

if (module._children) {
  Object.entries(module._children).forEach(([key, child]) => {
    installModule(rootState, path.concat(key), child)
  })
}
}

let module = {
_children: {
  aModule: {
    state: { name: "aModule" },
    _children: {
      a1Module: {
        state: { name: "a1Module" },
      }
    }
  },
  bModule: {
    state: {
      name: "bModule"
    }
  }
}
}

let rootState = { name: "zs" }
installModule(rootState, [], module);
console.log(rootState)

commit

同样的,commit 也是同样的道理

如果想要执行 aModule模块下面的 a1Module 下的 add 方法 , 需要这样执行 commit('aModule/a1Module/add')模块之间使用了 / 分割

js 复制代码
function installModule(store, rootState, path, module){
  // .....
  
  // 获取 模块上的 namespace
   const namespace = store._modules.getNamespace(path)
   
   // 设置上下文
   const local = makeLocalContext(store, namespace,path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key;
    registerMutation(store, namespacedType, mutation, local)
  })
  
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child)
  })
}

由于在 installModule 方法中使用了递归,我们获取了 父级路径 数组,通过 / 不断拼接 path 即可

如果模块上有 namespaced 属性并且为 true,我们就把对应的 key 值拼接上

js 复制代码
 getNamespace(path) {
     // 根模块
    let module = this.root;
    return path.reduce((namespace, key) => {
      // 获取对应的孩子
      module = module.getChild(key);
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

makeLocalContext 设置上下文,父级 commit 中 可以访问子 state,不是重点

js 复制代码
/**
 * make localized dispatch, commit, getters and state
 * if there is no namespace, just use root ones
 */
function makeLocalContext(store, namespace, path) {
  const noNamespace = namespace === '';

  const local = {
    commit: noNamespace ? store.commit : (type, payload) => {
      type = namespace + type;
      store.commit(type, payload)
    }
  };

  Object.defineProperties(local, {
    state: {
      get: () => getNestedState(store.state, path)
    }
  })
  
  return local
}

registerMutation 这个方法就是把commit Name 注册进 _mutations对象,等到 真正commit触发事件时执行,使用数组的原因是有可能 父级没有设置 namespace ,并且父级中的方法和自己方法重名,那么触发的时候就要一起触发

js 复制代码
function registerMutation(store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler(payload) {
    handler.call(store, local.state, payload)
  })
}

结束

这个嵌套结构最重要的思想就是格式化,格式化出容易使用的数据结构

那个注册模块的方法很巧妙,通过路径 -1 找到父级然后注入进去

今天是周四4点41,快下班了,就这样吧

相关推荐
T^T尚15 分钟前
uniapp H5上传图片前压缩
前端·javascript·uni-app
出逃日志37 分钟前
JS的DOM操作和事件监听综合练习 (具备三种功能的轮播图案例)
开发语言·前端·javascript
XIE39244 分钟前
如何开发一个脚手架
前端·javascript·git·npm·node.js·github
GISer_Jing1 小时前
React渲染相关内容——渲染流程API、Fragment、渲染相关底层API
javascript·react.js·ecmascript
山猪打不过家猪1 小时前
React(五)——useContecxt/Reducer/useCallback/useRef/React.memo/useMemo
前端·javascript·react.js
前端青山1 小时前
React事件处理机制详解
开发语言·前端·javascript·react.js
科技D人生1 小时前
Vue.js 学习总结(14)—— Vue3 为什么推荐使用 ref 而不是 reactive
前端·vue.js·vue ref·vue ref 响应式·vue reactive
对卦卦上心1 小时前
React-useEffect的使用
前端·javascript·react.js
练习两年半的工程师1 小时前
React的基本知识:事件监听器、Props和State的区分、改变state的方法、使用回调函数改变state、使用三元运算符改变state
前端·javascript·react.js
啵咿傲1 小时前
在React中实践一些软件设计思想 ✅
前端·react.js·前端框架