手写简易 Vuex 4.x-源码分析系列

前言

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它借鉴了Flux、redux的基本思想,将共享的数据抽离到全局,同时利用Vue.js的响应式机制来进行高效的状态管理与更新。想要掌握了解基础知识可以查阅Vuex官网,本篇主要是对vuex 4.x版本的源码进行研究分析。

Vuex 源码架构

Vuex 源码架构主要由几个核心的类和方法组成:Store 类ModuleCollection 类ModuleWrapper类installModule 方法makeLocalContext 方法

初始化装载与注入

Store 类是 vuex 中核心类之一,初始化创建通过 new Store(),创建一个返回 store 实例的createStore函数实现:

export function createStore<S>(options: StoreOptions<S>) {
  return new Store<S>(options);
}
复制代码

通过 Vue3 的 provide/inject api 实现 useStore hook 和组件的注册(app.use(store)[store中间件挂载app上]时需要调用的方法):

import { App, inject } from "vue";

const injectKey = "store";
export function useStore<S>(): Store<S> {
  return inject(injectKey) as any;
}

class Store<S = any> {
  ...
  install(app: App) {
    app.provide(injectKey, this);
  }
  ...
}
复制代码

Store 类对象构造

Store 类作为 vuex 中的核心类,它通过定义一系列的方法和属性实现:

  • 属性

    • _moduleCollection:模块集合对象

    • _modulesNamespaceMap:模块和命名空间映射

    • dispatch:访问actions异步方法的函数类型的属性

    • commit:访问mutations 方法的函数类型的属性

    • state:一个提供所有组件渲染的渲染数据【指响应式数据】的对象或函数【一般为对象】

    • _state:state 响应式数据的备份

  • 方法

    • commit:一个可以访问mutations对象中方法的方法
    • dispatch: 一个可以访问actions对象中方法的方法
    • reactiveState:把根模块中的state 变成响应式state的方法
    • install:app.use(store)[store中间件挂载app上]时需要调用的方法

数据初始化、module树构造

根据new构造传入的options或默认值,初始化内部数据。其调用 new Store(options) 时传入的options对象,用于构造 ModuleCollection 类,下面看看其功能。

type Dispatch = (type: string, payload?: any) => any;
type Commit = (type: string, payload?: any) => any;

class Store<S = any> {
  moduleCollection: ModuleCollection<S>;
  mutations: Record<string, any> = {};
  actions: Record<string, any> = {};
  commit: Commit;
  getters: Record<string, any> = {};
  dispatch: Dispatch;
  constructor(options: StoreOptions<S>) {
    this.moduleCollection = new ModuleCollection<S>(options);
    ...
  }
 ...
}
复制代码

ModuleCollection 主要将传入的options对象整个构造为一个 ModuleCollection 对象,实现原理可以查看下面 ModuleCollection 模块。详细实现可以查看源文件module-collection.js

dispatch 与 commit 的实现

Store 类中 封装替换原型中的dispatch和commit方法,将this指向当前store对象。dispatch和commit方法具体实现如下:

class Store<S = any> {
  ...
  mutations: Record<string, any> = {};
  actions: Record<string, any> = {};
  commit: Commit;
  dispatch: Dispatch;
  constructor(options: StoreOptions<S>) {
    const store = this;
    const { dispatch_: dispatch, commit_: commit } = this;
    this.commit = function bundCommit(type: string, payload: any) {
      commit.call(store, type, payload);
    };
    this.dispatch = function bundDispatch(type: string, payload: any) {
      dispatch.call(store, type, payload);
    };
    ...
  }
  commit_(type: string, payload: any) {
    if (!this.mutations[type]) {
      return console.error(`[vuex] unknown mutation type: ${type}`)
    }
    this.mutations[type](payload);
  }
  dispatch_(type: string, payload: any) {
    if (!this.actions[type]) {
      return console.error(`[vuex] unknown action type: ${type}`)
    }
    this.actions[type](payload);
  }
}
复制代码

dispatch 的功能是触发并传递一些参数(payload)给对应 type 的 action。因为其支持2种调用方法,所以在 dispatch 中,先进行参数的适配处理,然后判断action type是否存在,若存在就逐个执行(注:上面代码中的this._actions[type] 以及 下面的 this._mutations[type] 均是处理过的函数集合,具体内容留到后面进行分析)。

ModuleWrapper 类实现

ModuleWrapper 类是封装和管理某一个模块的类,ModuleWrapper 类成员有:

  • 属性
    • _children:保存当前模块下子模块
    • _rawModule:保存当前模块的属性
    • state:保存当前模块的state的属性
    • namespace:判断当前模块是否有命名空间的属性
    • actionContext:一个可以向action、mutations 中的方法参数传递state、commit、dispatch 值的对象,此对象类型为ActionContext
  • 方法
    • addChild:添加子模块
    • getChild:获取子模块
    • forEachChild:遍历添加所有子模块到根 store 的方法
    • forEachGetter:遍历添加所有子模块 getter 到根 store 的方法
    • forEachMutation:遍历添加所有子模块 mutation 到根 store 的方法
    • forEachAction:遍历添加所有子模块 action 到根 store 的方法

初始化 state、rawModule


class ModuleWrapper<S, R> {
  children: Record<string, ModuleWrapper<any, R>> = {};
  rawModule: Module<any, R>;
  state: S;
  namespaced: boolean;
  constructor(_rawModule: Module<any, R>) {
    this.rawModule = _rawModule;
    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];
  }
 ...
}
复制代码

mutations、actions 以及 getters 注册实现方法

  // 注册对应模块的getters,供installModule 模块调用
  forEachGetter(fn: GetterToKey<R>) {
    if (this.rawModule.getters) {
      console.log("object :>> ", this.rawModule.getters);
      Util.forEachValue(this.rawModule.getters, fn);
    }
  }
  // 注册对应模块的mutation,供installModule 模块调用
  forEachMutation(fn: MutationToKey<R>) {
    if (this.rawModule.mutations) {
      Util.forEachValue(this.rawModule.mutations, fn);
    }
  }
  // 注册对应模块的action,供installModule 模块调用
  forEachAction(fn: ActionToKey<R>) {
    if (this.rawModule.actions) {
      Util.forEachValue(this.rawModule.actions, fn);
    }
  }
  class Util {
    static forEachValue(obj: any, fn: Function) {
      Object.keys(obj).forEach((key) => {
        fn(obj[key], key);
      });
    }
  }
复制代码

子 module 注册实现方法

注册完了模块的actions、mutations以及getters后,提供installModule 子模块注册时递归调用自身,为子组件注册其state,actions、mutations以及getters等。

  forEachChild(fn: ChildModuleWrapperToKey<R>) {
    Object.keys(this.children).forEach((key) => {
      fn(this.children[key], key);
    });
  }
复制代码

ModuleCollection 模块实现

ModuleCollection 主要用来封装和管理所有模块的类,其将传入的 options 对象整个构造为一个module对象,并循环调用 this.register([key], rawModule) 为其中的 modules 属性进行模块注册,使其都成为 module 对象,最后 options 对象被构造成一个完整的组件树。

其包含的属性和方法如下:

  • 属性
    • root:根模块属性
  • 方法
    • register:注册根模块和子模块的方法
    • getNameSpace:循环递归获取命名空间方法
    • getChild:获取子模块方法

初始化注册根模块

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<any, R>(rawModule);
    if (!path.length) {
      this.root = newModule;
    } else {
      const parentModule = this.get(path.slice(0, -1));
      parentModule.addChild(path[path.length - 1], newModule);
    }

    if (rawModule.modules) {
      const sonModules = rawModule.modules;
      Object.keys(sonModules).forEach((key) =>
        this.register(path.concat(key), sonModules[key])
      );
    }
  }
复制代码

通过闯进来的 options 生成新模块,通过 path 判断是否是根模块,如果是根模块则将模块直接挂在 ModuleCollection 的 root 属性上,如果是不根模块通过 get 获取父级模块,并把模块添加到父模块上。

获取父模块 和 获取命名空间方法实现

  get(path: string[]) {
    const module = this.root;
    return path.reduce((moduleWrapper: ModuleWrapper<any, R>, key: string) => {
      return moduleWrapper.getChild(key);
    }, module);
  }
  getNamespace(path: string[]) {
    let moduleWrapper = this.root;
    return path.reduce(function (namespace, key) {
      moduleWrapper = moduleWrapper.getChild(key);
      return namespace + (moduleWrapper.namespaced ? key + "/" : "");
    }, "");
  }
复制代码

module 安装(installModule 方法实现)

在上面 Store 类构造中,绑定 dispatch 和 commit 方法之后,需要执行模块的安装(installModule)。installModule 方法为模块注册方法。其主要是初始化根模块,递归注册所有子模块,收集此内所有模块getter、mutations、actions方法 ,就是把根模块个子模块state 对象中的数据和mutations、action、getters 对象中方法,全部收集到store对象中。

  • installModule 主要完成:
    • 判断所有模块中是否有重复的命名空间
    • 收集当前模块的state,并保存到父级模块的state 中
    • 调用makeLocalContext 方法,创建ActionContext 类型的对象
    • 注册当前模块mutations 到store
    • 注册当前模块 actions 到store
    • 注册当前模块getter到store
    • 迭代当前模块下的所有子模块时,并完成子模块mutations、actions、getters到store的注册。

初始化rootState

function installModule<R>(
 store: Store<R>,
 rootState_: R,
 path: string[],
 module: ModuleWrapper<any, R>
) {
 const isRoot = !path.length;
 const namespace = store.moduleCollection.getNamespace(path);
 if (!isRoot) {
   const parentState: Record<string, any> = getParentState(
     rootState_,
     path.slice(0, -1)
   );
   parentState[path[path.length - 1]] = module.state;
 }
...
}
复制代码

判断是否是根目录,以及是否设置了命名空间,通过getParentState,(源码中为 getNestedState)方法拿到该 module 父级的 state,拿到其所在的 moduleName ,并将其 state 设置到父级state对象的 moduleName 属性中,由此实现该模块的state注册(首次执行这里,因为是根目录注册,所以并不会执行该条件中的方法)。通过getParentState 方法代码很简单,分析 path 拿到 state,如下。

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

mutations、actions以及getters注册

初始化完成 rootState 之后,循环注册我们在options中配置的action以及mutation等。

下面分析代码逻辑:

  // 注册对应模块的getters,供state读取使用
  module.forEachGetter(function (getter: any, key: string) {
    const namespaceType = namespace + key;
    Object.defineProperty(store.getters, namespaceType, {
      get: () => {
        return getter(module.state);
      },
    });
  });
  // 注册对应模块的mutation,供state修改使用
  module.forEachMutation(function (mutation: Mutation<R>, key: string) {
    const namespaceType = namespace + key;
    store.mutations[namespaceType] = function (payload: any) {
      mutation.call(store, module.state, payload);
    };
  });
  // 注册对应模块的action,供数据操作、提交mutation等异步操作使用
  const actionContext = makeLocalContext(store, namespace);
  module.forEachAction(function (action: Action<any, R>, key: string) {
    const namespaceType = namespace + key;
    store.actions[namespaceType] = function (payload: any) {
      action.call(
        { commit: actionContext.commit, dispatch: store.dispatch },
        payload
      );
    };
  });
复制代码

子 module 安装

注册完了根组件的actions、mutations以及getters后,递归调用自身,为子组件注册其state,actions、mutations以及getters等。

module.forEachChild(function (child, key) {
    installModule(store, rootState_, path.concat(key), child);
});
复制代码

8. makeLocalContext 方法实现

makeLocalContext 方法用来生成模块 ActionContext 类型的对象,对象属性主要包括dispatch、commit、state 三部分方法返回对象主要向 action、mutations 中的方法参数传递state、commit、dispatch 值。

function makeLocalContext<R>(store: Store<R>, namespace: string) {
  const noNamespace = !namespace;
  const actionContext: ActionContext<any, R> = {
    commit: noNamespace
      ? store.commit
      : function (type, payload) {
          type = namespace + type;
          store.commit(type, payload);
        },
  };
  return actionContext;
}
复制代码

makeLocalContext 将action type 进行拦截,如果有命名空间则拼上空间名。

写在最后

本篇主要是对vuex4.0 源码的学习总结,demo 使用 typescript 编写,源代码仓库可以查看vuex-source-demo。如果本篇对你有所帮助,欢迎点赞收藏,顺便给个 star ~~。ps:【想要快速搭建自己的前端静态博客,欢迎查阅Vuepress 快速搭建博客--一款你值得拥有的博客主题。】

参考

おすすめ

転載: juejin.im/post/7034091855058829326