近万字!手摸手从0实现一个ts版的Vuex4 🎉🎉

711 阅读20分钟

本文正在参加「金石计划」

hey🖐! 我是小黄瓜😊😊。不定期更新,期待关注➕ 点赞,共同成长~

写在前面

本文涉及到的代码已经上传到github,顺便安利一下tiny-ts-tools,这个项目会使用Typescript来实现一些经典的前端库,相关库核心逻辑的最简实现,以及分步骤实现的文章。接下来会实现mini-nest依赖注入、axios等欢迎star! 地址:Tiny-Blog

Vuex是什么?

我相信经常使用vue进行日常开发的小伙伴肯定不会陌生,按照vue官方文档上的说法来解释:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

其实就是一个数据管理的容器,对不同组件中所需要依赖的数据进行管理。举个例子来说: 如果我们在父子组件中进行数据传递,很简单,我们可以在组件上定义一个自定义数据,然后在子组件中通过props进行获取。如果组件中有多层级的嵌套,他们之间进行数据共享怎么做呢,难道要一层层的进行传递?这也太麻烦了。

由此vuex的作用就显现出来了,其实在很多框架都有类似的状态管理工具,比如vue提供的vuex,react的redux、mobx,都是解决类似的问题。 说回到vuex,这个状态自管理应用包含以下几个部分:

  • 状态,驱动应用的数据源;
  • 视图,以声明方式将状态映射到视图;
  • 操作,响应在视图上的用户输入导致的状态变化。

整个的"单向数据流"理念的简单示意图: image.png

而整个vuex的数据流向是这样的: image.png 页面数据发生更改 -> 调用 dispatch来分发actions,在actions调用commit方法来触发mutations进行数据更改,更改 Vuexstore 中的状态的唯一方法是提交 mutation,而获取state中的数据可以使用store.getters.xx或者store.state.xx

无标题-2023-03-06-1550.png store 主要由actionsmutationsstategettersmodule这几部分构成。

  • state:数据源,可以使用一个对象来保存状态
state: {
  name: '小黄瓜',
  foodList: ['瓜', '苹果', '橘子']
}
  • actions:用于提交 mutation,可以用于操作异步,而不直接变更 state的状态
const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      // 触发 mutations 中的方法
      context.commit('increment')
    }
  }
})
  • mutations:使用接收到的数据用于更新 state,一般为同步操作
const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})
  • getters:提供组件的 获取 响应式 state 数据 的对象,并做一些额外的操作
const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    // 调用getters 中的 doneTodos 函数获取state中的数据
    doneTodos (state) {
      return state.todos.filter(todo => todo.done)
    }
  }
})
  • module:模块,每个模块拥有自己的actionsmutationsstategetters,嵌套子模块
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... },
  // moduleC为moduleA的子模块
  modules: {
    c: moduleC
  }
}

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

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

初始化

vuex是如何初始化的呢?回想一下在使用vuex的时候,先使用npm/yarn安装vuex,然后在项目的main.js文件下注册插件:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from '@/store/index'

// 使用use进行注册
createApp(App).use(store).use(router).mount('#app')

store文件,就是我们的vuex仓库定义的根目录:

// 这里已经被替换为我们自己的vuex源码文件
import { createStore } from '../vuex/indexSingle'

// 使用createStore创建仓库
export default createStore({
  state: {
    navList: ['这是一个测试', 'ok']
  },
  getters: {
    showNavList(state) {
      return state.navList
    }
  },
  mutations: {
    modifyNavList(state, navList) {
      return state.navList = navList
    }
  },
  actions: {
    handlerNavList({ commit }) {
      setTimeout(() => {
        const navList = ['a', 'b', 'c']
        commit('modifyNavList', navList)
      }, 600)
    }
  }
})

可以看到,我们的store文件是由createStore函数生成。 接下来就开始实现手写vuex的初始化逻辑:

其中StoreOptions这个类型约束将会在下文实现。

首先使用createStore函数创建Store类,Store类是我们实现vuex的核心,所有的数据都会被保存到此类中。Store类中的install方法是vue注册插件要求提供的方法,install是开发插件的方法,这个方法的第一个参数是 Vue 构造器,在install方法使用provideStore类的实例绑定到全局,然后在页面中使用useStore方法调用inject来获取Store类。

const injectKey = "store"

// 页面组件调用
export function useStore<S>(): Store<S> {
  return inject(injectKey) as any
}

// vuex/index生成Store类
export function createStore<S>(options: StoreOptions<S>) {
  return new Store<S>(options)
}

// Store类
class Store<S = any>{
  constructor(options: StoreOptions<S>) {}
  install(app: App) {
    app.provide(injectKey, this)
  }
  test() {
    return "我是store";
  }
}

provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。 例如现在有父组件和子孙组件,通过 provideinject来进行数据传递:

  <template>
    <div id="parent">
      <p>父组件</p>
      <Child></Child>
    </div>
  </template>
  <script>
    import Child from './child.vue'
    export default {
      components: {
        Child
      },
      // 父组件中返回要传给下级的数据
      provide () {  
        return {
          msg: '小黄瓜'
        }
      },
      // 或者 写法如下
      // provide: {
      //   msg: '小黄瓜'
      // },
    }
  </script>


  <template>
    <div id="child">
    <p>子孙组件</p>
    </div>
  </template>

  <script>
    export default {
      name: "child",
      // 子孙组件中接收祖先组件中传递下来的数据
      inject: ['dataInfo'],
    }
  </script>

在初始化vuex的过程中,我们使用provide来注册根store的数据,在页面中使用inject来获取数据:

type RootState = {
  navList: any
}

export default defineComponent({
  name: 'HomeView',
  setup() {
    const store = useStore<RootState>()
    return {
      store
    }
  }
});

整个过程如下:

无标题-2023-03-07-1409.png

类型定义

首先来定义Store类中接接收的参数,也就是options,其实也就是定义的store结构,一般是这样子的:

const store = {
  state: {},
  actions: {
    xxx() {}
  },
  mutations: {
    xxx() {}
  },
  getters: {
    xxx() {}
  }
}

由此就可以定义出options的接口类型:

interface StoreOptions<S> {
  state?: S;
  getters?: GetterTree<S, S>;
  mutations?: MutationTree<S>;
  actions?: ActionTree<S, S>
}

actions

actions的约束: actions接收一个对象,key为函数名,value为函数体。 其中函数的第一个参数可以调用commit等方法对mutations中的函数进行操作。 S代表当前state R代表根state

interface ActionTree<S, R> {
  [key: string]: Action<S, R>
}
type Action<S, R> = (context: ActionContext<S, R>, payload?: any) => any

interface ActionContext<S, R> {
  dispatch: Dispatch;
  commit: Commit;
  state: S;
}

// dispatch方法
type Dispatch = (type: string, payload?: any) => any
// commit方法
type Commit = (type: string, payload?: any) => any

mutations

mutationsactions相似,同样是函数对象,不同的是,mutation函数的第一个参数接收state第二个参数为新值,用于更新数据:

interface MutationTree<S> {
  [key: string]: Mutation<S>
}
type Mutation<S> = (state: S, payload?: any) => void

getters

getters主要获取state中的数据,并增加一些额外的处理逻辑,getter函数的第一个参数为当前state(当前store中),第二个参数为当前getters对象(当前store中),第三个和第四个参数分别为根stategetters

interface GetterTree<S, R> {
  [key: string]: Getter<S, R>
}

type Getter<S, R> = (state: S, getters: any, rootState: R, rootGetters: any) => any

现在我们已经可以在页面中调用store中的方法了,上面我们定义了一个test方法,现在就执行一下:

<template>
  <div class="home">
    {{ store.test() }}
  </div>
</template>

<script lang="ts">
export default defineComponent({
  name: 'HomeView',
  setup() {
    const store = useStore<RootState>()
    return {
      store
    }
  }
});
</script>

页面中已经成功显示出来了: image.png 现在我们已经实现了一下单模块的store,但是实际的情况往往不是这么简单的,比如我们想要将不同的模块进行拆分,这就需要用到module,相应的,也会继续在下文实现commitdispatch两个方法。

注册Module

首先还是先来定义modules的类型,在StoreOptions中增加modules属性,他的类型约束与StoreOptions类似,只是多了一个namespaced布尔类型,标识是否为独立命名空间。

interface StoreOptions<S> {
  state?: S,
  getters?: GetterTree<S, S>,
  mutations?: MutationTree<S>,
  actions?: ActionTree<S, S>,
  // 增加ModuleTree类型
  modules?: ModuleTree<S>
}

interface ModuleTree<R> {
  [key: string]: Module<any, R>
}

export interface Module<S, R> {
  namespaced?: boolean,
  state?: S,
  getters?: GetterTree<S, R>,
  mutations?: MutationTree<S>,
  actions?: ActionTree<S, R>,
  modules?: ModuleTree<R>
}

然后先顶一下一下我们需要使用的store结构,这里我是定义了homemine两个模块作为根store的子模块: image.png

export default createStore<RootState>({
  modules: {
    mineModule: mineModule,
    homeModule: homeModule
  }
})

然后将food模块作为mine的子模块,现在的模块结构是这样的:

- 根模块
	- mineModule
  	- foodModule
	- homeModule

接下来定义homeModule文件:

interface IMsg {
  id: number,
  code: string,
  name: string,
  addr: string
}

interface IHomeList {
  [key: number]: IMsg
}

interface IState {
  'homeList': IHomeList
}

const state: IState = {
  homeList: [{
    id: 0,
    code: '123d1',
    name: '红太阳',
    addr: '幸福路1号'
  },
  {
    id: 1,
    code: '123a2',
    name: '金浪漫',
    addr: '浪漫路2号'
  },
  {
    id: 2,
    code: '123d6',
    name: '红彤彤',
    addr: '喜悦路23号'
  },
  {
    id: 3,
    code: '123c5',
    name: '喜洋洋',
    addr: '快乐路1号'
  },
  {
    id: 4,
    code: '123p0',
    name: '大草原',
    addr: '搞笑路1号'
  },
  {
    id: 5,
    code: '123h3',
    name: '红太郎',
    addr: '幸福路24号'
  }
  ]
}

export const homeModule: Module<IState, RootState> = {
  namespaced: true,
  state,
  getters: {
    showNavList(state) {
      return state.homeList
    }
  },
  mutations: {
    modifyHomeList(state, newList) {
      return state.homeList = newList
    }
  },
  actions: {
    handlerNavList({ commit }) {
      setTimeout(() => {
        const navList = ['a', 'b', 'c']
        commit('modifyHomeList', navList)
      }, 600)
    }
  }
}

RootState代表根state的空对象,作为根state的类型定义:

export type RootState = {}

定义mineModule文件:

interface IState {
  'typeList': string[]
}

const state: IState = {
  typeList: ['猛烈', '温柔', '触宝', '精神病']
}


export const mineModule: Module<IState, RootState> = {
  namespaced: true,
  state,
  getters: {
    showNavList(state) {
      return state.typeList
    }
  },
  mutations: {
    modifyNavList(state, navList) {
      return state.typeList = navList
    }
  },
  modules: {
    foodModule: foodModule
  },
  actions: {
    handlerNavList({ commit }) {
      setTimeout(() => {
        const navList = ['a', 'b', 'c']
        commit('modifyNavList', navList)
      }, 600)
    }
  }
}

定义mineModule模块的子模块foodModule

interface IState {
  [key: string]: number[]
}

export const foodModule: Module<IState, RootState> = {
  namespaced: true,
  state: {
    foodList: [1, 2, 3]
  },
  getters: {
    getFoodList(state) {
      return state.foodList
    }
  },
  mutations: {
    modifyNavList(state, navList) {
      return state.foodList = navList
    }
  },
  actions: {
    handlerNavList({ commit }) {
      setTimeout(() => {
        const navList = [4, 5, 6]
        commit('modifyNavList', navList)
      }, 600)
    }
  }
}

现在可以在store类中打印下,看看我们传入的是一个什么样子的结构?

// 为了区分默认的Store,将我们的store类改为TinyStore
class TinyStore<S = any> {
  constructor(options: StoreOptions<S>) {
    console.log(options) // 打印传入的options
  }

  install(app: App) {
    app.provide(injectKey, this)
  }

  test() {
    return 'is a test'
  }
}

image.png 可以看到,打印的结果完全符合我们的预期。

在开始处理module之前先来了解一下两个类。

ModuleCollection

封装和管理所有模块的类,类成员,说白了就是对所有的模块统一进行管理,包括我们接下来要做的酱所有的模块进行统一包装和注册。 属性:

  1. root ——> 根模块属性

方法:

  1. register ——> 注册根模块和子模块的方法 【注册就是添加】
  2. get ——> 获取子模块方法

ModuleWrapper

封装和管理某一个模块的类,注意和**ModuleCollection**类的区别**,****ModuleWrapper**只专注于某一个具体的模块的处理。相当于对每个模块的扩展,为每个模块穿上了一身装备。

属性:

  1. children ——> 保存当前模块下子模块
  2. rawModule ——> 保存当前模块的属性
  3. state ——> 保存当前模块的 state 的属性
  4. namespaced ——> 判断 当前模块是否有命名空间的属性

方法:

  1. addChild —— 添加子模块到当前模块中
  2. getChild —— 获取子模块

ModuleCollectionModuleWrapperModule之间关系如下图:

无标题-2023-03-07-1518.png ModuleCollection类专注于处理所有的模块,所谓的处理,是指将所有的模块,递归进行遍历,包括最深的层级,然后使用ModuleWrapper类对每个模块进行包裹,按照层级关系添加到每个ModuleWrapper类的children属性中,这就是所谓的模块注册。 所以吗,模块注册的逻辑应当被添加到TinyStore类的constructor函数中执行:

class TinyStore<S = any> {
  // 保存处理后的结果
  moduleCollection: ModuleCollection<S>
  constructor(options: StoreOptions<S>) {
    // 执行ModuleCollection类,传入options
    this.moduleCollection = new ModuleCollection<S>(options)
  }

  install(app: App) {
    app.provide(injectKey, this)
  }

  test() {
    return 'is a test'
  }
}

先来实现一下ModuleWrapper这个工具类,在递归深层遍历模块时,被传入ModuleWrapper类的模块是原始模块,而处理之后保存到children属性中的是包装后的模块类。这一点也可以在两个属性的类型约束中可以看出来:

  • children: Record<string, ModuleWrapper<any, R>> = {}
  • rawModule: Module<any, R>
class ModuleWrapper<S, R> {
  // 保存使用ModuleWrapper包装后的模块对象
  children: Record<string, ModuleWrapper<any, R>> = {}
  // 保存包装前的模块
  rawModule: Module<any, R>
  // 保存state,数据源
  state: S
  // 定义是否具有独立命名空间?直接获取原模块的namespaced属性
  namespaced: boolean
  constructor(rawModule_: Module<any, R>) {
    this.rawModule = rawModule_
    // state 如果不存在,初始化为空对象
    this.state = rawModule_.state || Object.create(null)
    this.namespaced = rawModule_.namespaced || false
  }
  addChild(key: string, moduleWrapper: ModuleWrapper<any, R>) {
    this.children[key] = moduleWrapper
  }
  getChild(key: string) {
    return this.children[key]
  }
}

addChild方法使用传入的key和包装后的模块类保存到children属性中,而getChild通过key来获取模块类。

此时的两个概念:

  • 模块

模块指我们在store中定义的原始的模块,例如:

foodModule: {
  actions: {}
  mutations: {}
  ...
}
  • 模块类

此时的模块类是经过ModuleWrapper包装的类,而已经不是单纯的原对象了。

const newModule = new ModuleWrapper(rawModule)

接下来就可以开始在ModuleCollection类中实现将所有的子模块类添加到父级模块类的children属性中。整理逻辑是这样的,我们需要为每个模块路径都维护一个path数组,便于对每条父子级关系可追溯。就那我们上面的例子来说,他们对应的path数组的关系为:

1: ['homeModule']
2: ['mineModule', 'foodModule']

从每个模块的分叉出进行拆分,每个路径都创建独立的数组,便于对每个模块进行索引取值。

定义ModuleCollection类,并且开始执行register方法从根模块开始注册: 一开始传入的是根模块,此时执行register方法,创建根模块的模块类,path为空数组,长度为0,所以保存根模块到root属性中,因为在js中对象都是引用类型,所以后续在处理完整个模块对象后,可以通过root访问整个处理后的模块对象。如果path的长度不为0,那么说明此时处于某一层的子模块当中,这时我们就需要将当前的子模块类添加到其父模块类的children属性中,这部分逻辑暂时略过。

先来看一下模块递归的逻辑:如果当前模块依然拥有modules属性,说明还需要向下遍历,取出modules属性,这里使用了一个额外的工具类,用来作为通用的遍历方法,UtilforEachValue为静态方法,接受一个数组和处理函数,将处理函数作用于数组的每一项。这里我们将子模块数组传入,处理函数中的keymodules此时分别为模块名和模块对象。,此时调用register方法将path添加模块名后当作新的path传入,第二个参数自然就是遍历的每个模块对象。

class ModuleCollection<R> {
  root!: ModuleWrapper<any, R>
  constructor(rawRootModule: Module<any, R>) {
    this.register([], rawRootModule)
  }
  register(path: string[], rawModule: Module<any, R>) {
    // 创建模块类
    const newModule = new ModuleWrapper(rawModule)
    if (path.length === 0) {
      // 根模块
      this.root = newModule
    } else {
      // 子模块
    }

    if (rawModule.modules) {
      // 取出当前模块下的子模块
      const { modules: sonModules } = rawModule
      Util.forEachValue(sonModules, (key: string, modules: Module<any, R>) => {
        // 递归模块,拼接path
        this.register(path.concat(key), modules)
      })
    }
  }
}

class Util {
  // 接收数组与处理函数
  static forEachValue(obj: Record<string, any>, fn: (...args: any) => void) {
    Object.keys(obj).forEach(key => {
      fn(key, obj[key])
    })
  }
}

接下来就是添加的逻辑,上文中已经说过了,处于当前的模块中需要做的就是将自己的模块类添加到父级模块类的children属性中,那么怎么能找到所属的父级类呢?答案还是path

因为在递归过程中我们已经保存了path(模块名)的路径,所以在寻找父级的时候按照模块名就好了,如果我们当前处于foodModule模块,直接在path数组中截取0-当前模块的前一位不就寻找到整个祖先路径可以了吗!再使用root根据截取的path数组遍历就可以寻找到父级的模块类。 如果我们的path 保存的是一条这样的模块路径:(这只是一个为了展示路径截取的例子,并不是我们当前代码实现所使用的模块路径)

person -> man -> teacher -> student

如果我们当前处于student模块中,那么此时截取的path

无标题-2023-03-08-1137.png 接下来的实现代码就水到渠成了:

class ModuleCollection<R> {
  root!: ModuleWrapper<any, R>
  constructor(rawRootModule: Module<any, R>) {
    // this.root = new ModuleWrapper(rawRootModule)
    // this.register([], this.root)
    this.register([], rawRootModule)
  }
  register(path: string[], rawModule: Module<any, R>) {

    const newModule = new ModuleWrapper(rawModule)
    if (path.length === 0) {
      this.root = newModule
    } else {
      // 添加子模块 - > 父模块 children
      // 获取父级的moduleWrapper对象
      const parentModule: ModuleWrapper<any, R> = this.get(path.slice(0, -1))
      // 添加到父级模块的children
      parentModule.addChild(path[path.length - 1], newModule)
    }

    if (rawModule.modules) {
      const { modules: sonModules } = rawModule
      Util.forEachValue(sonModules, (key: string, modules: Module<any, R>) => {
        this.register(path.concat(key), modules)
      })
    }
  }
  // 获取父级
  get(path: string[]) {
    const module = this.root
    // 以根模块为初始循环模块
    return path.reduce((moduleWrapper: ModuleWrapper<AnalyserNode, R>, key: string) => {
      // 循环获取模块的children
      return moduleWrapper.getChild(key)
    }, module)
  }
}

打印一下TinyStore,发现模块类已经成功被添加到children中:

image.png 这里有一个地方可能会不大好理解,就是path的收集过程,首先打印一下每次循环path数组的值: image.png 可以看到,当递归到最深的层级后,也就是收集完foodModule模块后,我们的path将已经收集到的模块名弹出了,又重新开始收集homeModule。这里我们对循环的处理是先循环外层模块,然后再每个模块进行递归,递归的时候拼接path数组。

无标题-2023-03-08-1430.png

这张图就很明显了,虽然path在递归的时候每次拼接模块名,但是在下一次循环的时候当次循环依然还是初始的数组。

实例方法 Commit & Dispatch

在Vuex中可以通过实例直接进行dispatch分发action,或者使用commit提交mutation 未来我们需要将actionsmutationsgetters 都作为TinyStore类的实例属性保存,所以调用dispatchcommit时,也在实例属性中取值。

class TinyStore<S = any> {
  moduleCollection: ModuleCollection<S>
  // mutations
  mutations: Record<string, any> = {}
  // actions
  actions: Record<string, any> = {}
  // getters
  getters: GetterTree<any, S> = {}
  commit: Commit
  dispatch: Dispatch
  constructor(options: StoreOptions<S>) {
    this.moduleCollection = new ModuleCollection<S>(options)

    // store和ref变量都为本实例,分别定义不同的名字,语义化
    const store = this
    const ref = this
    // 赋值实例方法commit_
    const commit = ref.commit_
    // 赋值实例方法dispatch_
    const dispatch = ref.dispatch_
    this.commit = function boundCommit(type: string, payload: any) {
      commit.call(store, type, payload)
    }
    this.dispatch = function boundDispatch(type: string, payload: any) {
      dispatch.call(store, type, payload)
    }
  }

  install(app: App) {
    app.provide(injectKey, this)
  }

  test() {
    return 'is a test'
  }

  commit_(type: string, payload: any) {
    if (!this.mutations[type]) throw new Error('[vuex] unknown mutations type: ' + type)
    this.mutations[type](payload)
  }

  dispatch_(type: string, payload: any) {
    if (!this.actions[type]) throw new Error('[vuex] unknown actions type: ' + type)
    this.actions[type](payload)
  }
}

这两个方法都是通过type也就是方法名在mutationsactions中查找处理函数,然后传入值执行。

值得注意的是,这两个方法目前并不能成功获取到处理函数,未来我们将所有的相关处理函数都挂载到mutationsactions之后,才能成功执行。

注册State

目前在store中的数据源也就是state位置分散在各个模块中,在这一节我们就将不同层级的state收集在ModuleWrapper模块类中各个层级的state中,类似于对module的做法。

定义installModule函数,它的作用是对每个模块的注册操作,初始化根模块,递归注册所有子模块,包括state、以及后续的actionsmutationsgetters等。 installModule函数的参数如下:

  • storeTinyStore类的实例
  • rootState_:根state
  • path:模块路径
  • module:当前模块类
function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
  const isRoot = !path.length

  // 如果不是根模块
  if (!isRoot) {
    // 1. 拿到父级的state
    const parentState: any = getParentState(rootState_, path.slice(0, -1))
    // 2. 拿到当前模块的state和当前模块名合成一个对象,加到父级的state上
    parentState[path[path.length - 1]] = module.state
  }

  // 为每个模块递归installModule
  module.forEachChild((child, key) => {
    installModule(store, rootState_, path.concat(key), child)
  })
}

获取父级state

function getParentState<R>(rootState: R, path: string[]) {
  return path.reduce((state, key) => {
    return (state as any)[key]
  }, rootState)
}

收集state的逻辑同module类似,通过path逐级寻找每个层级的模块,而进入某一层的遍历函数forEachChild定义在ModuleWrapper模块类中:

class ModuleWrapper<S, R> {

  // 省略...
  
  forEachChild(fn: ChildMdleWraperToKey<R>) {
    // 遍历children执行fn函数
    Util.forEachValue(this.children, fn)
  }
}
// 参数fn约束为接收模块类和key的函数
type ChildMdleWraperToKey<R> = (moduleWrapper: ModuleWrapper<any, R>, key: string) => void

那么installModule函数需要在那里开始执行呢?当然是TinyStoreconstructor中:

class TinyStore<S = any> {
  moduleCollection: ModuleCollection<S>
  mutations: Record<string, any> = {}
  actions: Record<string, any> = {}
  commit: Commit
  dispatch: Dispatch
  constructor(options: StoreOptions<S>) {
    this.moduleCollection = new ModuleCollection<S>(options)
	
    const store = this

    // 省略...
  	
    // 注册state模块
    const rootState = this.moduleCollection.root.state
    installModule(store, rootState, [], this.moduleCollection.root)
    console.log('注册完成', rootState)
  }

  // 省略...
}

image.png 执行完毕,打印下我们的根模块类的state,因为在js中对象是引用类型,所以后续的变动也直接反映在根对象中。

注册Getters

getters中的函数的注册与state略有不同,在保存getters函数时,key应当使用路径来保存,因为我们需要将actionsmutationsgetters保存在TinyStore类中,所以需要维护路径。 例如:

getters: {
  'mineModule/foodModule/handlerNavList': (payload)=> {
    // ...
  }
}

收集getters函数的操作依然在installModule函数中处理。

function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
  const isRoot = !path.length
	// 获取当前层级的路径key
  const nameSpace = store.moduleCollection.getNameSpace(path)

  // 省略...

  module.forEachGetter((getter, key) => {
    const nameSpaceType = nameSpace + key
    // store.getters[nameSpaceType] = getter
    Object.defineProperty(store.getters, nameSpaceType, {
      get() {
        // getter接收当前模块的state
        return getter(module.state)
      }
    })
  })
}

当获取到路径组成的key后,拼接函数名,注册到TinyStore实例中的getters属性中,这里使用Object.defineProperty进行注册,因为我们希望访问到getters函数后立即执行,直接返回state数据。

获取模块路径的函数放置在ModuleCollection类中,实现思路比较简单,获取根模块类,逐层遍历,获取模块的key,拼接在一起。

class ModuleCollection<R> {
  root!: ModuleWrapper<any, R>
  // 省略...

  getNameSpace(path: string[]) {
    // 获取根模块类
    let moduleWrapper = this.root
    // 遍历,逐层查找key
    return path.reduce((nameSpace, key) => {
      moduleWrapper = moduleWrapper.getChild(key)
      // 拼接
      return nameSpace + (moduleWrapper.namespaced ? key + '/' : '')
    }, '')
  }
}

遍历模块下面getters对象的操作,还是放到模块类ModuleWrapper中:

type GetterToKey<R> = (getter: Getter<any, R>, key: string) => any
class ModuleWrapper<S, R> {
  children: Record<string, ModuleWrapper<any, R>> = {}
  rawModule: Module<any, R>
  state: S
  namespaced: boolean

  // 省略...
  
  forEachGetter(fn: GetterToKey<R>) {
    if (this.rawModule.getters) {
      Util.forEachValue(this.rawModule.getters, fn)
    }
  }
}

image.png getter函数已经成功绑定。

注册Mutations

注册mutations与上文的getter逻辑基本一致,不过多赘述了。

function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
  const isRoot = !path.length
	// 获取当前层级的路径key
  const nameSpace = store.moduleCollection.getNameSpace(path)

  // 省略...

  module.forEachMutation((matation, key) => {
    const nameSpaceType = nameSpace + key
    store.mutations[nameSpaceType] = (payload: any) => {
      matation.call(store, module.state, payload)
    }
  })
}

type MutationToKey<S> = (mutation: Mutation<S>, key: string) => any
class ModuleWrapper<S, R> {
  children: Record<string, ModuleWrapper<any, R>> = {}
  rawModule: Module<any, R>
  state: S
  namespaced: boolean

  // 省略...
  
  forEachMutation(fn: MutationToKey<S>) {
    if (this.rawModule.mutations) {
      Util.forEachValue(this.rawModule.mutations, fn)
    }
  }
}

image.png

注册Actions

注册actions也基本与上文一致,只不过传递给action的函数的参数不同于mutationsgetter函数,actions函数的第一个参数接受一个对象,可以结构出来commitdispatch等方法(这里只实现commit),需要我们将TinyStore中的commit方法传递过来。

function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
  const isRoot = !path.length
	// 获取当前层级的路径key
  const nameSpace = store.moduleCollection.getNameSpace(path)

  // 省略...

  module.forEachAction((action, key) => {
    const nameSpaceType = nameSpace + key
    store.actions[nameSpaceType] = (payload: any) => {
      // 传递commit,用于触发mutation
      action.call(store, { commit: store.commit }, payload)
    }
  })
}

type ActionToKey<S, R> = (action: Action<S, R>, key: string) => any
class ModuleWrapper<S, R> {
  children: Record<string, ModuleWrapper<any, R>> = {}
  rawModule: Module<any, R>
  state: S
  namespaced: boolean

  // 省略...
  
  forEachAction(fn: ActionToKey<S, R>) {
    if (this.rawModule.actions) {
      Util.forEachValue(this.rawModule.actions, fn)
    }
  }
}

image.png

截止到现在我们的模块注册已经接近尾声了,各个模块已经全都注册成功。🎉🎉

image.png

一个小问题🐛

其实我们在处理actions函数传入的commit函数时是有问题的,来看一下我们是怎么做的:

module.forEachAction((action, key) => {
  const nameSpaceType = nameSpace + key
  store.actions[nameSpaceType] = (payload: any) => {
    action.call(store, { commit: store.commit }, payload)
  }
})

这传入commit函数时我们是直接将TinyStore类中的commit方法直接传入的,那么相应的,我们在使用是就会这样写:

const foodModule = {
  actions: {
    handlerNavList({ commit }) {
      setTimeout(() => {
        const navList = [4, 5, 6]
        commit('modifyNavList', navList)
      }, 600)
    }
  }
}

foodModule模块中我们定义的action函数,直接使用mutation的函数名modifyNavList作为key来触发,但是还记得我们注册后的mutations的对象是怎么定义的吗?

{
  'homeModule/modifyHomeListayload': () => {}
	'mineModule/foodModule/modifyNavList': () => {}
	'mineModule/modifyNavList': () => {}
}

函数的key已经被我们全部更改为了路径地址,所以我们直接拿着函数名是无法触发函数的。

如果能够自动拦截commit,并且拼接上路径就好了,那么如何做呢?还记得我们的注册actions是在那个函数中进行的吗?installModule!而且installModule函数是会递归调用的,也就是说我们在installModule函数可以直接使用path来获取整个路径地址!

interface ActionContext<S, R> {
  dispatch?: Dispatch,
  commit: Commit,
  state?: S
}

function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
  const isRoot = !path.length
	// 获取路径地址,当前的path包含当前模块的模块名
  const nameSpace = store.moduleCollection.getNameSpace(path)

  // 解决直接使用commit,路径错误,重置commit
  const actionContext: ActionContext<any, R> = makeLocalContext(store, nameSpace)
  
  module.forEachAction((action, key) => {
    const nameSpaceType = nameSpace + key
    store.actions[nameSpaceType] = (payload: any) => {
      // 替换为拦截后的actionContext
      action.call(store, { commit: actionContext.commit }, payload)
    }
  })
}

makeLocalContext函数中我们做了一层拦截,如果nameSpace不为空,此时就不是位于根模块,直接使用一个新函数进行替换,新的函数中使用nameSpace拼接key,然后再执行原来的commit方法。

function makeLocalContext<R>(store: TinyStore<R>, nameSpace: string) {
  const noNameSpace = nameSpace === ''
  const actionContext: ActionContext<any, R> = {
    // 是否为根模块
    commit: noNameSpace ? store.commit : (type, payload) => {
      type = nameSpace + type
      store.commit(type, payload)
    }
  }
  return actionContext
}

大功告成啦!!! 🥳🥳

写在最后 ⛳

未来可能会更新typescriptreact基础知识系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳