Vuex 插件目录

  1. // src/vuex/index.js
  2. // 容器初始化
  3. class Store {}
  4. // 插件安装逻辑:当 Vue.use(Vuex) 时执行
  5. const install = () => {}
  6. export default {
  7. Store,
  8. install
  9. }

模块化设计

将 Store 类和 install 方法抽离,使得 index.js 文件仅用于方法聚合并导出

  1. // src/vuex/store.js
  2. // 容器初始化
  3. class Store {}
  4. // 插件安装逻辑:当 Vue.use(Vuex) 时执行
  5. const install = () => {}
  1. // src/vuex/index.js
  2. import { Store, install } from './store';
  3. export default {
  4. Store,
  5. install
  6. }

install 插件安装逻辑

install 方法会传入 vue 的构造函数

  1. // 容器初始化
  2. export class Store {}
  3. // 导出传入的 Vue 的构造函数,供插件内部的其他文件使用
  4. export let Vue;
  5. /**
  6. * 插件安装逻辑:当 Vue.use(Vuex) 时执行
  7. * @param {*} _Vue Vue 的构造函数
  8. */
  9. export const install = (_Vue) => {
  10. Vue = _Vue;
  11. }

new Vue 时会注入 store 容器实例,同时通过全局混入,为所有组件添加 store 属性

  1. // src/vuex/store.js
  2. /**
  3. * 将根组件中注入 store 实例,混入到所有子组件上
  4. * @param {*} Vue
  5. */
  6. export default function applyMixin(Vue) {
  7. // 通过 beforeCreate 生命周期,在组件创建前,实现全局混入
  8. Vue.applyMixin({
  9. beforeCreate: vuexInit, // vuexInit 为初始化混入逻辑
  10. })
  11. }
  12. function vuexInit() {
  13. // $options 时在创建 vue 实例时传入的参数
  14. const options = this.$options;
  15. // 如果选项中拥有store属性,说明是根实例;其他情况都是子实例
  16. if (options.store) {
  17. // 为根实例添加 $store 属性,指向 store 实例
  18. this.$store = options.store;
  19. } else if (options.parent && options.parent.$store) {
  20. // 儿子可以通过父亲拿到 $store 属性,放到自己身上继续给儿子
  21. this.$store = options.parent.$store;
  22. }
  23. }
  1. export const install = (_Vue) => {
  2. Vue = _Vue;
  3. applyMixin(Vue);
  4. }

State 状态的实现

  • State 状态的响应式:使用 vue 实例来保存 state

    1. // 容器初始化
    2. export class Store {
    3. constructor(options) { // options:{state, mutation, actions}
    4. const state = options.state; // 获取 options 选项中的 state 对象
    5. // 响应式数据:new Vue({data})
    6. // vuex 中 state 状态的响应式是借助了 Vue 来实现的
    7. this._vm = new Vue({
    8. data: {
    9. // 在 data 中,默认不会将以$开头的属性挂载到 vm 上
    10. $$state: state // $$state 对象将通过 defineProperty 进行属性劫持
    11. }
    12. })
    13. }
    14. // 相当于 Object.defineProperty({}) 中的 getter
    15. get state() { // 对外提供属性访问器:当访问state时,实际是访问 _vm._data.$$state
    16. return this._vm._data.$$state;
    17. }
    18. }

    getters 方法的实现

  • 将选项中的 getters 方法,保存到 store 实例中的 getters 对象中

  • 借助 Vue 原生 computed,实现 Vuex 中 getters 的数据缓存功能

    1. // 容器初始化
    2. export class Store {
    3. constructor(options) { // options:{state, mutation, actions}
    4. const state = options.state; // 获取 options 选项中的 state 对象
    5. // 获取 options 选项中的 getters 对象:内部包含多个方法
    6. const getters = options.getters;
    7. // 声明 store 实例中的 getters 对象
    8. this.getters = {};
    9. // 将 options.getters 中的方法定义到计算属性中
    10. const computed = {};
    11. // 将用户传入的 options.getters 属性中的方法,转变成为 store 实例中的 getters 对象上对应的属性
    12. Object.keys(getters).forEach(key => {
    13. // 将 options.getters 中定义的方法,放入计算属性 computed 中,即定义在 Vue 的实例 _vm 上
    14. computed[key] = () => {
    15. return getters[key](this.state);
    16. }
    17. // 将 options.getters 中定义的方法,放入store 实例中的 getters 对象中
    18. Object.defineProperty(this.getters, key, {
    19. // 取值操作时,执行计算属性逻辑
    20. get: () => this._vm[key]
    21. })
    22. })
    23. // 响应式数据:new Vue({data})
    24. // vuex 中 state 状态的响应式是借助了 Vue 来实现的
    25. this._vm = new Vue({
    26. data: {
    27. // 在 data 中,默认不会将以$开头的属性挂载到 vm 上
    28. $$state: state // $$state 对象将通过 defineProperty 进行属性劫持
    29. },
    30. computed // 将 options.getters 定义到 computed 实现数据缓存
    31. })
    32. }
    33. }

    mutations 的实现

  • 将 options.mutations 中定义的方法,绑定到 store 实例中的 mutations 对象 ```typescript // 声明 store 实例中的 mutations 对象 this.mutations = {}; // 获取 options 选项中的 mutations 对象 const mutations = options.mutations;

// 将 options.mutations 中定义的方法,绑定到 store 实例中的 mutations 对象 Object.keys(mutations).forEach(key => { // payload:commit 方法中调用 store 实例中的 mutations 方法时传入 this.mutations[key] = (payload) => mutationskey; });

/**

  1. * 通过 type 找到 store 实例的 mutations 对象中对应的方法,并执行
  2. * 用户可能会解构使用{ commit }, 也有可能在页面使用 $store.commit
  3. * 所以,在实际执行时,this是不确定的,{ commit } 写法 this 为空,
  4. * 使用箭头函数:确保 this 指向 store 实例;
  5. * @param {*} type mutation 方法名
  6. * @param {*} payload 载荷:值或对象
  7. */

this.commit = (type, payload) => { this.mutationstype; }

  1. <a name="jgvhh"></a>
  2. # Actions 的实现
  3. ```typescript
  4. // 声明 store 实例中的 actions 对象
  5. this.actions = {};
  6. // 获取 options 选项中的 actions 对象
  7. const actions = options.actions;
  8. // 将 options.actions 中定义的方法,绑定到 store 实例中的 actions 对象
  9. Object.keys(actions).forEach(key => {
  10. // payload:dispatch 方法中调用 store 实例中的 actions 方法时传入
  11. this.actions[key] = (payload) => actions[key](this, payload);
  12. })
  13. /**
  14. * 通过 type 找到 store 实例的 actions 对象中对应的方法,并执行
  15. * 用户可能会解构使用{ dispatch }, 也有可能在页面使用 $store.dispatch,
  16. * 所以,在实际执行时,this 是不确定的,{ dispatch } 写法 this 为空,
  17. * 使用箭头函数:确保 this 指向 store 实例;
  18. * @param {*} type action 方法名
  19. * @param {*} payload 载荷:值或对象
  20. */
  21. this.dispatch = (type, payload) => {
  22. // 执行 actions 对象中对应的方法,并传入 payload 执行
  23. this.actions[type](payload);
  24. }

模块收集

将模块对象格式化成为一颗“模块树”

模块的树型结构-模块树

数据格式化,即构建“模块树”的逻辑,就是递归的将子模块注册到父模块中。和 AST 语法树的构建过程相似:

  • 首先,层级上有父子关系,理论上是都支持无限递归的;
  • 其次,由于采用深度优先的递归方式,需要使用栈结构来保存层级关系(相当于地图);

    1. // 模块树对象
    2. {
    3. _raw: '根模块',
    4. _children:{
    5. moduleA:{
    6. _raw:"模块A",
    7. _children:{
    8. moduleC:{
    9. _raw:"模块C",
    10. _children:{},
    11. state:'模块C的状态'
    12. }
    13. },
    14. state:'模块A的状态'
    15. },
    16. moduleB:{
    17. _raw:"模块B",
    18. _children:{},
    19. state:'模块B的状态'
    20. }
    21. },
    22. state:'根模块的状态'
    23. }

    模块收集的实现

  • 深度遍历构建树型结构 ```typescript /**

    • 模块收集操作
    • 处理用户传入的 options 选项
    • 将子模块注册到对应的父模块上 */ class ModuleCollection { constructor(options) {

      1. // 从根模块开始,将子模块注册到父模块中
      2. // 参数 1:数组,用于存储路径,标记模块树的层级关系
      3. this.register([], options);

      }

      /**

      • 将子模块注册到父模块中
      • @param {*} path 数据类型,当前待注册模块的完整路径
      • @param {} rootModule 当前待注册模块对象 / register(path, rootModule) { // 格式化,并将当前模块,注册到对应父模块上,构建 Module 对象 let newModule = {

        1. _raw: rootModule, // 当前模块的完整对象
        2. _children: {}, // 当前模块的子模块
        3. state: rootModule.state // 当前模块的状态

        }

        // 根模块时:创建模块树的根对象 if (path.length == 0) {

        1. this.root = newModule;

        } else {

        1. // 非根模块时,将当前模块,注册到对应父模块上
        2. // 逐层找到当前模块的父亲(例如:path = [a,b,c,d])
        3. let parent = path.slice(0, -1).reduce((memo, current) => {
        4. //从根模块中找到a模块;从a模块中找到b模块;从b模块中找到c模块;结束返回c模块即为d模块的父亲
        5. return memo._children[current];
        6. }, this.root);
        7. // 将d模块注册到c模块上
        8. parent._children[path[path.length - 1]] = newModule;

        }

        // 若当前模块存在子模块,继续注册子模块 if (rootModule.modules) {

        1. // 采用深度递归方式处理子模块
        2. Object.keys(rootModule.modules).forEach(moduleName => {
        3. let module = rootModule.modules[moduleName];
        4. // 将子模块注册到对应的父模块上
        5. // 1.path:待注册子模块的完整路径,当前父模块 path 拼接子模块名 moduleName
        6. // 2. module:当前待注册子模块对象
        7. this.register(path.concat(moduleName), module);
        8. })

        } } }

export default ModuleCollection;

  1. <a name="LVklz"></a>
  2. # 模块安装
  3. > 递归“模块树”并将所有模块的 getter、mutation、action 定义到当前 store 实例中
  4. - 从根模块开始进行模块安装,递归处理格式化后的“模块树”对象
  5. - 根据模块名称,将全部子模块定义到根模块上,同时将状态合并到根模块上
  6. ```typescript
  7. /**
  8. * 安装模块
  9. * @param {*} store 容器
  10. * @param {*} rootState 根状态
  11. * @param {*} path 所有路径
  12. * @param {*} module 格式化后的模块对象
  13. */
  14. const installModule = (store, rootState, path, module) => {
  15. // 遍历当前模块中的 actions、mutations、getters
  16. // 将它们分别定义到 store 中的 _actions、_mutations、_wrappedGetters;
  17. // 遍历 mutation
  18. module.forEachMutation((mutation, key) => {
  19. // 处理成为数组类型:每个 key 可能会存在多个需要被处理的函数
  20. store._mutations[key] = (store._mutations[key] || []);
  21. // 向 _mutations 对应 key 的数组中,放入对应的处理函数
  22. store._mutations[key].push((payload) => {
  23. // 执行 mutation,传入当前模块的 state 状态
  24. mutation.call(store, module.state, payload);
  25. })
  26. })
  27. // 遍历 action
  28. module.forEachAction((action, key) => {
  29. store._actions[key] = (store._actions[key] || []);
  30. store._actions[key].push((payload) => {
  31. action.call(store, store, payload);
  32. })
  33. })
  34. // 遍历 getter
  35. module.forEachGetter((getter, key) => {
  36. // 注意:getter 重名将会被覆盖
  37. store._wrappedGetters[key] = function () {
  38. // 执行对应的 getter 方法,传入当前模块的 state 状态,返回执行结果
  39. return getter(module.state)
  40. }
  41. })
  42. // 遍历当前模块的儿子
  43. module.forEachChild((child, key) => {
  44. // 递归安装/加载子模块
  45. installModule(store, rootState, path.concat(key), child);
  46. })
  47. }
  48. // 容器的初始化
  49. export class Store {
  50. constructor(options) {
  51. const state = options.state;
  52. this._actions = {};
  53. this._mutations = {};
  54. this._wrappedGetters = {};
  55. this._modules = new ModuleCollection(options);
  56. installModule(this, state, [], this._modules.root);
  57. }
  58. // ...
  59. }

流程梳理

  • 当项目引用并注册 vuex 插件时,即 Vuex.use(vuex),将执行 Vuex 插件中的 install 方法;
  • install 方法,接收外部传入的 Vue 实例,并通过 Vue.mixin 实现 store 实例的全局共享;
  • 项目中通过 new Vuex.Store(options) 配置 vuex 并完成 store 状态实例的初始化;
  • 在 Store 实例化阶段时,将会对 options 选项进行处理,此时完成 Vuex 模块收集和安装操作;
  • 在 new Vue 初始化时,将 store 实例注入到 vue 根实例中(此时的 store 实例已实现全局共享);

    State 状态安装

    对“模块树”中状态的安装,就是将所有子模块上的 State 状态,挂载到对应父模块的 State 状态上:

  • 处理范围:子模块,即 path.length > 0 时,执行状态安装逻辑

  • 处理逻辑:将子模块的状态 module.state,挂载到其父模块的状态上
  • 通过 Vue.set API 向父模块状态中添加子模块状态,以此实现对象新增属性为响应式数据

    1. /**
    2. * 安装模块
    3. * @param {*} store 容器
    4. * @param {*} rootState 根状态
    5. * @param {*} path 所有路径
    6. * @param {*} module 格式化后的模块对象
    7. */
    8. const installModule = (store, rootState, path, module) => {
    9. // 处理子模块:将子模块上的状态,添加到对应父模块的状态中;
    10. if(path.length > 0){
    11. // 从根状态开始逐层差找,找到当前子模块对应的父模块状态
    12. let parent = path.slice(0, -1).reduce((memo, current)=>{
    13. return memo[current]
    14. }, rootState)
    15. // 支持 Vuex 动态添加模块,将新增状态直接定义成为响应式数据;
    16. Vue.set(parent, path[path.length-1], module.state);
    17. }
    18. }

    Vuex 插件的开发

    ```typescript import Vue from ‘vue’; import Vuex from ‘vuex’;

// 引入 Vuex 日志插件 logger import logger from ‘vuex/dist/logger’

Vue.use(Vuex);

const store = new Vuex.Store({ // 在 plugins 数组中可以注册多个 Vuex 插件,插件的执行是串行顺序执行的 plugins: [ logger(), // 日志插件:导出的 logger 是一个高阶函数 ], }); export default store;

  1. 1. 创建一个 Vuex 插件,最终导出一个高阶函数(在 plugin 数组中进行插件注册);
  2. 1. Vuex Store 类提供的订阅方法 store.subscribe:当 mutation 方法触发时被执行;
  3. 1. Vuex Store 类提供的状态替换方法 store.replaceState:能够更新 Vuex 中的状态;
  4. ```typescript
  5. import Vue from 'vue';
  6. import Vuex from 'vuex';
  7. Vue.use(Vuex);
  8. // vuex-persists 插件实现
  9. function persists() {
  10. return function (store) {
  11. console.log("----- persists 插件执行 -----")
  12. // 取出本地存储的状态
  13. let data = localStorage.getItem('VUEX:STATE');
  14. if (data) {
  15. console.log("----- 存在本地状态,同步至 Vuex -----")
  16. // 如果存在,使用本地状态替换 Vuex 中的状态
  17. store.replaceState(JSON.parse(data));
  18. }
  19. // subscribe:由 vuex 提供的订阅方法,当触发 mutation 方法时被执行;
  20. store.subscribe((mutation, state) => {
  21. console.log("----- 进入 store.subscribe -----")
  22. localStorage.setItem('VUEX:STATE', JSON.stringify(state));
  23. })
  24. }
  25. }
  26. const store = new Vuex.Store({
  27. plugins: [
  28. logger(), // 日志插件:导出的 logger 是一个高阶函数
  29. persists() // 持久化插件:vuex-persists
  30. ]
  31. });
  32. export default store;

插件机制内部的实现:

  • store.subscribe:状态变更时的订阅回调功能;

该函数和众多插件实现类似

  1. // 调用时传入一个队列中
  2. subscribe(fn) {
  3. this._subscribes.push(fn);
  4. }
  5. // 状态改变时,从队列取出触发
  6. store._subscribes.forEach(fn => {
  7. fn(mutation, rootState);
  8. })

参考资料

1.【Vuex 源码学习】第一篇 - Vuex 的基本使用