6

学习Vue有一段时间了,感觉对于Vuex总是停留在一知半解的状态,决定花点时间好好学习研究下Vuex的实现。Vuex的设计思想,借鉴了Redux将数据存放到全局的store,再将store挂载到每个vue实例组件中,利用Vue.js的数据响应机制来进行高效的派发和状态更新。

开始前的准备

个人觉得有必要理解这几个知识点对于理解源码有很大的帮助

  • reduce函数的作用
  • Object.defineProperty和Object.defineProperties的作用
  • Object.create的作用
  • 闭包
  • 原型链,构造函数
  • 引用类型和值类型

基本使用方法

举个例子,假设该模块需要命名空间,根据例子再去摸索源码会有更加不错的帮助

store/user.js

modules: {
    users: {
        namespaced: true,
        state: {
            username: null,
        },
        mutations: {
            SET_USER(state, payload) {
                state.username = payload
            }
        },
        actions: {
            // context包含commit, dispatch, localState, localGetters, rootGetters, rootState
            FETCH_USER(context, payload) {
            }
        },
        getters: {
            GET_USER(localState, localGetters, rootState, rootGetters) {
                return localState.username
            }
        }
    }
}

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import user from './user'

Vue.use(vuex);

new Store({
    modules: {
        user
    }
})

user.vue

<script>
    import { mapGetters, mapActions, mapMutations } from 'vuex';
    export default {
        computed: {
            ...mapGetters('user', [
                'GET_USER'
            ])
        },
        methods: {
            ...mapActions('user', {
                'fetchUser': FETCH_USER,
            }),
            ...mapMutations('user', {
                'setUser': SET_USER,
            }),
            loginByUsername() {
                // fetchUser请求
            },
            loginByDispatch() {
                this.$store.dispatch('user/FETCH_USER', {
                    user: ...,
                    password: ....,
                    randomStr: ....,
                    code: ...
                }).then(res => console.log(res))
                .catch(err => console.log(err))
                .finally()
            }
        }
    }
</script>

new Store

主要完成了对于一些状态的初始化,_mutations对象将用于存放模块中的所有mutations, _actions对象将用于存放模块中的所有actions,_wrappedGetters用于存放模块中的所有getter, _modulesNamespaceMap用于存放存在namespaced为true的key-value表,对于module对象进行重新注册:

// rawRootModule为传入Store中的原生module对象
var ModuleCollection = function ModuleCollection (rawRootModule) {
  // register root module (Vuex.Store options)
  this.register([], rawRootModule, false);
};
ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
  var this$1 = this;
  ..
  var newModule = new Module(rawModule, runtime);
  if (path.length === 0) {
    this.root = newModule;
  } else {
    var parent = this.get(path.slice(0, -1));
    parent.addChild(path[path.length - 1], newModule);
  }

  // register nested modules
  if (rawModule.modules) {
    forEachValue(rawModule.modules, function (rawChildModule, key) {
      this$1.register(path.concat(key), rawChildModule, runtime);
    });
  }
};

register

  • 注册函数主要完成两个事情:

    • 1.构造module对象
    • 2.判断是否有子模块,如果有则继续进行遍历

1. 构造module对象

// Base data struct for store's module, package with some attribute and method
var Module = function Module (rawModule, runtime) {
  this.runtime = runtime;
  // Store some children item
  this._children = Object.create(null);
  // Store the origin module object which passed by programmer
  this._rawModule = rawModule;
  var rawState = rawModule.state;

  // Store the origin module's state
  this.state = (typeof rawState === 'function' ? rawState() : rawState) || {};
};

2. 判断是否有子模块,如果有则继续进行遍历

modules: {
    user: {
        state: {level: 1}
        post: {
            state: {level: 2}
        }
    }
}

首先初始化path长度为0,最外层构造出来的module对象即为root, 然后由于存在子模会将user模块add到root下的_children,结果为

root: {
    ...
    _children: {
        user module
    }
}

然后通过判断存在子模块,则继续进行递归遍历,此时由于上一层的函数没有出栈,通过path.concat(key), path为['user', 'post'],通过ModuleCollection原型中的get来获取当前模块的父模块

// result: ['user']
let parentPath = ['user', 'post'].slice(0, -1);
// root为根模块,最终获取到的为user module
parentPath.reduce((module, key) => module._children, root)
// 将新增模块加入到user module
root: {
    ...
    _children: {
        user: {
            _children: {
                post module
            }
        }
    }
}

最终构造成如下

installModule

完成了模块的注册以后,最重要的一句代码是installModule, 该方法顾名思义就是将注入的modules进行内部的组装, 如果存在子模块通过递归的方法来获取,并且合并path路径。

首先模块分为有命名空间和没有命名空间两块,通过getNamespace判断获取命名空间,比如path = ['user', 'post', 'report'], 通过reduce方法首先通过getChild获取root下的_children里的user模块, 如果namespaced为true,则加上路径,这样一层层下去,所有的模块,子模块内部将会形成类似user,user/post/形式的命名空间。

ModuleCollection.prototype.getNamespace = function getNamespace (path) {
  var module = this.root;
  return path.reduce(function (namespace, key) {
    // 获取子模块
    module = module.getChild(key);
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
};

命名空间很重要,之后commit,dispatch, mapMutations, mapActions等一些列操作都是基于此。之后将子module下所有的state全部暴露到根节点下的state,通过使用vue.set将新的属性设置到rootState上, 这个state未来将被用于store.state获取其状态

// set state
if (!isRoot && !hot) {
  // 通过path.slice(0, -1)来截取当前module之前的所有父类路径,通过reduce来获取当前模块上一级的父模块
  var parentState = getNestedState(rootState, path.slice(0, -1));
  var moduleName = path[path.length - 1];
  store._withCommit(function () {
    Vue.set(parentState, moduleName, module.state);
  });
}

之后是将注册registerMutation,registerAction,registerGetter,在注册之前,vuex做了巧妙的处理,动态设置当前模块所在的环境

var local = module.context = makeLocalContext(store, namespace, path);
local = {
    dispatch,
    commit,
    // getters and state object must be gotten lazily
    // because they will be changed by vm update
    Object.defineProperties(local, {
      getters: {
      get: noNamespace
        ? function () { return store.getters; }
        : function () { return makeLocalGetters(store, namespace); }
      },
      state: {
        get: function () { return getNestedState(store.state, path); }
      }
    });
}

通过namespaced来判断设置当前环境下local对象内的dispatch,commit, 如果存在就在dispatch和commit内部加上namespaced前缀,此外还加入了了local.state和local.getter,通过Object.defineProperties来设置访问器属性,当不同模块内部比如actions,mutations或者getters中的方法进行获取的时候会进行动态获取。比如带有命名空间的模块:

{
    user: {
        namspaced: true
        state: {
            username: 1
        }
        mutations: {
            SET_USER(state, payload) {}
        },
        actions: {
            FETCH_USER({dispatch, commit, getters, state, rootGetters, rootState}, payload) {
                // ...
            }
        },
        getters: {
            GET_USER() {}
        }
    }
}
  • registerMutation

    就是将所有模块内部的mutations平铺到_mutations中形成key-value的键值对,key为namespaced+key。当触发方法的时候会内置local.state,可以在方法的第一个参数获取到内部自己的state

    上面例子最后会被平铺成

    _mutations: {
        'user/SET_USER': [wrappedMutationHandler]
    }

    当commit('SET_USER', 1)的时候SET_USER的参数第一个参数会去动态获取state的值, 具体获取方式是通过getNestedState方法,配合path来获取其state。

    // 例如下面例子,通过reduce首先获取root层,再次遍历获取user层对象数据
    path: ['root', 'user']
    store.state:
    {
        root: {
            ...
            user:{
            }
        }
    }
  • registerAction

    类似于注册mutation,会将所有模块下的actions平铺到_actions, 上面例子最后会平铺成

    _actions: {
        'user/FETCH_USER': [wrappedActionHandler]
    }

    所以外部进行dispatch的时候,如果有命名空间需要加上,例如store.dispatch('user/GET_USER',1),内部其实通过key找到_actions内部的entry,然后调用wrappedActionHandler(payload),当触发方法的时候内部同样内置了local.dispatch,local.commmit, local.state,local.getters,store.getters, store.state.

    • 其中内置的dispatch和commit如果存在namespaced可以直接通过方法名进行提交,因为环境下已经配置好了,将namespace组合好了。
    • local.state是访问器属性,同注册mutation。
    • local.getters需要提及一下,当存在命名空间的时候,例如当例子GET_USER方法获取getters的时候,可以直接通过getters['GET_USER'], 他内部设置了代理getter,1.首先遍历最外层所有的getters;2.获取namespace命名空间长度,截取之后的字符串,如果长度为0则仍旧截取所有;3.设置访问器属性,属性名字为截取掉的type;4.当方法内进行调用的时候就会调用get方法动态获取其getter。

      function makeLocalGetters (store, namespace) {
        var gettersProxy = {};
      
        var splitPos = namespace.length;
        Object.keys(store.getters).forEach(function (type) {
          // skip if the target getter is not match this namespace
          if (type.slice(0, splitPos) !== namespace) { return }
      
          // extract local getter type
          var localType = type.slice(splitPos);
      
          // Add a port to the getters proxy.
          // Define as getter property because
          // we do not want to evaluate the getters in this time.
          Object.defineProperty(gettersProxy, localType, {
            get: function () { return store.getters[type]; },
            enumerable: true
          });
        });
      
        return gettersProxy
      }
  • registerGetter

    同样道理其中,将所有模块下的getters平铺到_wrappedGetters, 当获取不同模块下的getters的时候会内置local.getters, local.state, store.getters, store.state

    • store.getters

      为何访问getter属性类似于vue中的computed,原因就在于将所有getter设置进了vm,并且在访问的时候对于store.getter对象内部的每个方法名为key的函数设置了访问器属性,当外部进行调用的时候,返回计算属性计算到的结果。

    • store.state

      prototypeAccessors$1 = { state: { configurable: true } }
      prototypeAccessors$1.state.get = function () {
        return this._vm._data.$$state
      };
      Object.defineProperties( Store.prototype, prototypeAccessors$1 );

这样模块内的基本要素mutation, actions, getters, state等全部注册完了。

MapGetters, MapState, MapActions, MapGetters

这三个方法是为了在vue中更加方便容易地置入以及使用,说白了就是通过命名空间组合的类型分别去_mutations, _actions, store.getters的对象中取对应的value, 所以第一个参数都为命名空间名(如果有命名空间),第二个参数可以是数组的形式也可以是对象的形式,无论哪种形式,最后都会进行标准化例如

mapGetters('user', [
    'GET_USER'                 =>       [{key: 'GET_USER', val: 'GET_USER'}]
])
mapGetters('user', {
    getUser: 'GET_USER'        =>       [{key: 'getUser', val: 'GET_USER'}]
})
mapMutations('user', [
    'SET_USER'                 =>       [{key: 'SET_USER', val: 'SET_USER'}]
])
mapMutations('user', {
    setUser: 'SET_USER'        =>       [{key: 'setUser', val: 'SET_USER'}]
})
mapActions('user', [
    'FETCH_USER'               =>       [{key: 'FETCH_USER', val: 'FETCH_USER'}]
])
mapActions('user', {
    fetchUser: 'FETCH_USER'    =>       [{key: 'fetchUser', val: 'FETCH_USER'}]
})

通过命名空间获取对应的module, 这样就能够获取到该模块的上下文context

当然还有另一种写法, 当其valfunction的时候, 会内置commit, dispatch参数

mapMutations('user', {
    setOtherUser: (commit) => {
        
    }
})
mapActions('user', {
    fetchOtherUser: (dispatch) => {
        
    }
})

最后mutation会进行commit.apply(this.$store, [val].concat(args)) action会进行dispatch.apply(this.$store, [val].concat(args))
state返回state[val],
getter直接返回store.getters[val],其中val为其actions,mutations, getters方法名。

dispatch, commit

上面进行dispatch(_type, _payload)以及commit(_type, _payload, _options),其实处理了2件事情:

  • 处理传参

    一般传参都是:

    commit({
        type: 'SET_USER',
        payload: 1
    }), 

    但也可以

    commit('SET_USER', 1)

    他内部进行对参数的重新组合,如果是对象则type=obj.type; payload=obj, 如果有optionsoptions=payload

  • 找到对应方法进行调用

    通过key找到对应的方法数组,如果是commit,则遍历数组依次执行数组内的方法,如果是dispatch,则将数组内的所有进行Promise.all, 返回Promise对象

接下来就可以快乐地使用vuex了,进行dispatch和commit, 以及mapGetters等一系列操作了。

不得不说vuex内部的实现感觉满满的基础,对于平时知识点的复习以及理解完源码对于平时项目的使用还是很有帮助的,最后有些不正确的地方希望能得到大佬们的指正。


ZHONGJIAFENG7
165 声望5 粉丝

React追求者