最近参加的几场面试,全部都问到了Vuex。虽然之前在工作中经常用到,但是被问到了还是只能答出个大概,不能够从原理上去剖析。趁着周末的时间,好好的捋一捋Vuex的源码。
为什么要使用Vuex? 首先我们来看看官网中的介绍
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
首先我们要知道的就是这个‘状态’,什么是状态,在Vuex中,状态就是数据,Vuex就是用来管理这些数据的。 Vuex通过将共享的数据抽离到全局,并以一个单例存放,通过Vue的响应式原理来进行数据的管理和更新,这也就是为什么Vuex只能跟Vue搭配使用的原因了。
用过Vuex源码的童靴应该对这张图很熟悉吧! vuex实现的是一个单向数据流,在全局拥有一个State存放数据,所有修改State的操作必须通过Mutation进行,Mutation的同时提供了订阅者模式供外部插件调用获取State数据的更新。所有异步的接口需要走Action,经常用在调用后端异步的接口,并且Action也不能修改state的数据,还是需要通过mutation来修改,最后根据state的修改渲染到视图上。 上面的这段话就是我在面试时经常拿来说的,但是我感觉这样说是肯定不行的,所以今天我们就来看看Vuex的源码
注入 用过Vuex的小伙伴一定知道,Vuex使用前的配置十分简单,只需要提供一个store,然后将store注入到Vue实例中即可
1 2 3 4 5 6 7 8 9 new Vue({ store, router, render : h => h(App) }).$mount('#app' ); Vue.use(Vuex)
Vue使用插件的方法很简单,只需要调用Vue.use(plugin), 对于Vuex只需要Vue.use(plugin)。Vue.use()的原理就是通过调用插件的install方法来进行插件的安装,前提是插件是一个对象,如果传入的是一个函数,则直接执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function install (_Vue ) { if (Vue && _Vue === Vue) { { console .error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ); } return } Vue = _Vue; applyMixin(Vue); }
这段代码很简单,主要做了两件事,一是防止Vuex被重复安装,二是执行applyMixin方法。 至于applyMixin方法,主要是为了初始化Vuex,Vue针对Vue1.0和Vue2.0进行了不同的处理。对于Vue1.0,Vuex会将vuexInit放入到Vue的_init方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function vuexInit () { var options = this.$ options ; if (options .store) { this.$ store = typeof options .store === 'function' ? options .store() : options .store; } else if (options .parent && options .parent.$ store) { this.$ store = options .parent.$ store; } }
对于Vue2.0就简单了,直接利用mixin混入到实例中
1 Vue.mixin ({ beforeCreate: vuexInit });
store对象 在分析store源码之前,我们先来看看它的构造过程(图片来自网上)
环境判断 首先进行了Vue判断,如果没有通过Vue.use(Vuex)注册,则调用install方法注册,然后判断当前环境是否支持promise以及是否通过new来创建store对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 ... if (!Vue && typeof window !== 'undefined' && window .Vue ) { install (window .Vue ); } { assert (Vue , "must call Vue.use(Vuex) before creating a store instance." ); assert (typeof Promise !== 'undefined' , "vuex requires a Promise polyfill in this browser." ); assert (this instanceof Store , "store must be called with the new operator." ); } ...
初始化 判断通过后,开始进行一系列的初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];var strict = options.strict; if ( strict === void 0 ) strict = false ;this ._committing = false ;this ._actions = Object.create(null );this ._actionSubscribers = [];this ._mutations = Object.create(null );保存getter this ._wrappedGetters = Object.create(null );this ._modules = new ModuleCollection(options);this ._modulesNamespaceMap = Object.create(null );this ._subscribers = [];this ._watcherVM = new Vue();var store = this ;var ref = this ;var dispatch = ref.dispatch;var commit = ref.commit;this .dispatch = function boundDispatch (type, payload) { return dispatch.call(store, type, payload) }; this .commit = function boundCommit (type, payload, options) { return commit.call(store, type, payload, options) }; this .strict = strict;var state = this ._modules.root.state;installModule(this , state, [], this ._modules.root); resetStoreVM(this , state); plugins.forEach(function (plugin) { return plugin(this $1 ); }); var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;if (useDevtools) { devtoolPlugin(this ); }
在上面这段代码,主要实现的就是初始化一些内部变量,然后执行了两个核心的方法–installModule(初始化module)和resetStoreVM(通过VM使store变成响应式)。
初始化Module installModule主要是为module加上命名空间,注册action、mutation、getter,同时递归所有子模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 function installModule (store, rootState, path , module , hot) {// 判断是不是根module var isRoot = !path .length; // 获取module 的namespace var namespace = store._modules.getNamespace(path ); // 如果有namespace 则在_modulesNamespaceMap中注册 if (module .namespaced) { store._modulesNamespaceMap[namespace ] = module ; } // 不是根module ,就把子module 的每一个state设置到其父级的state属性上 if (!isRoot && !hot) { // 获取父module var parentState = getNestedState(rootState, path .slice(0 , -1 )); // 当前模块的模块名 var moduleName = path [path .length - 1 ]; store._withCommit(function () { // 将子module 设置为响应式的 Vue.set(parentState, moduleName, module .state); }); } // 给context 对象赋值 var local = module .context = makeLocalContext(store, namespace , path ); // 循环注册mutation module .forEachMutation(function (mutation, key ) { var namespacedType = namespace + key ; registerMutation(store, namespacedType, mutation, local); }); // 循环注册每一个module 的action module .forEachAction(function (action, key ) { var type = action.root ? key : namespace + key ; var handler = action.handler || action; registerAction(store, type, handler, local); }); // 循环注册每一个module 的getter module .forEachGetter(function (getter, key ) { var namespacedType = namespace + key ; registerGetter(store, namespacedType, getter, local); }); // 递归注册每一个子module module .forEachChild(function (child, key ) { installModule(store, rootState, path .concat (key ), child, hot); }); }
在installModule函数中,首先判断是不是根module以及是否设置了命名空间,如果设置了命名空间,则将module放入_modulesNamespaceMap中。 如果不是根module,则调用Vue.set()方法将当前module的state挂载到父module的state中。然后调用 makeLocalContext函数给module.context赋值,设置局部的dispatch、commit方法以及getters和state。 具体的makeLocalContext方法就不再展开了(里面同样使用了Object.defineProperty来设置响应式)。
设置响应式 resetStoreVM函数中主要包括初始化store._vm,将state属性赋值到Vue实例的data属性,形成数据的响应式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 function resetStoreVM (store, state, hot) { var oldVm = store._vm; store.getters = {}; var wrappedGetters = store._wrappedGetters; var computed = {}; forEachValue(wrappedGetters, function (fn, key) { computed[key] = function () { return fn (store) ; }; Object.defineProperty(store.getters, key, { get: function () { return store._vm[key]; }, enumerable: true }); }); var silent = Vue.config.silent; Vue.config.silent = true ; store._vm = new Vue({ data: { $$state: state }, computed: computed }); Vue.config.silent = silent; if (store.strict) { enableStrictMode(store); } if (oldVm) { if (hot) { store._withCommit(function () { oldVm._data.$$state = null ; }); } Vue.nextTick(function () { return oldVm.$destroy(); }); } }
resetStoreVM会首先遍历_wrappedGetters,使用Object.defineProperty方法为每一个getter添加get方法,这样我们在组件中调用this.$store.getter.user就等同于访问store._vm.user。然后Vuex通过实例化一个Vue对象来实现数据的响应式原理,运用Vue内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。这样我们访问store._vm.user就相当于访问Vue实例中data的属性了。 注意:开启严格模式时,会深度监听$$state的变化,如果不是通过this._withCommit()方法触发的state修改,也就是store._committing如果是false,就会报错。 执行完上述代码之后,我们就可以利用this.$store.getter.test访问Vue实例中的test属性了。 看到了这儿,Vuex内部的实现已经完成理解一大半,接下来就可以愉快的去和面试官吹牛逼了。
为了更好的把这个牛逼吹下去,我们再来看看store提供的一些API。
dispatch 让我们先来看看dispatch的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 Store .prototype .dispatch = function dispatch (_type, _payload ) { var this $1 = this ; var ref = unifyObjectStyle (_type, _payload); var type = ref.type ; var payload = ref.payload ; var action = { type : type, payload : payload }; var entry = this ._actions [type]; if (!entry) { { console .error (("[vuex] unknown action type: " + type)); } return } try { this ._actionSubscribers .filter (function (sub ) { return sub.before ; }) .forEach (function (sub ) { return sub.before (action, this $1.state ); }); } catch (e) { { console .warn ("[vuex] error in before action subscribers: " ); console .error (e); } } var result = entry.length > 1 ? Promise .all (entry.map (function (handler ) { return handler (payload); })) : entry[0 ](payload); ... };
再来看看registerAction的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function registerAction (store, type, handler, local ) { var entry = store._actions[type] || (store._actions[type] = []); entry.push (function wrappedActionHandler (payload, cb) { var res = handler.call (store, { dispatch : local.dispatch, commit : local.commit, getters : local.getters, state : local.state, rootGetters : store.getters, rootState : store.state }, payload, cb); // 判断是否是promise if (!isPromise (res)) { res = Promise.resolve (res); } if (store._devtoolHook) { return res.catch (function (err ) { store._devtoolHook.emit ('vuex:error' , err); throw err }) } else { return res } }); }
因为registerAction的时候将push进_actions的action进行了一层封装(wrappedActionHandler),所以我们在进行dispatch的第一个参数中获取state、commit等方法。 之后,执行结果res会被进行判断是否是Promise,不是则会进行一层封装,将其转化成Promise对象。dispatch时则从_actions中取出,只有一个的时候直接返回,否则用Promise.all处理再返回。
commit commit函数会先进行一个参数适配,然后判断当前type对应的action type是否存在,如果存在则调用_withCommit函数执行相应的 mutation。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // 提交mutation函数 Store.prototype.commit = function commit (_type, _payload, _options) { var this$1 = this; // check object -style commit // 适配commit 的两种调用方式 // commit ('test' , object )或者commit ({type : 'test' , object : object }) var ref = unifyObjectStyle(_type, _payload, _options); var type = ref .type ; var payload = ref .payload; var options = ref .options ; var mutation = { type : type , payload: payload }; // 这里的entry取值就是我们在registerMutation函数中push到_mutations中的函数,已经经过处理 var entry = this._mutations[type ]; if (!entry) { { console.error(("[vuex] unknown mutation type: " + type )); } return } // 执行mutation中的所有方法 this._withCommit(function () { entry.forEach (function commitIterator (handler ) { handler (payload); }); }); // 通知所有订阅者 this._subscribers.forEach (function (sub) { return sub(mutation, this$1 .state); }); if ( options && options .silent ) { console.warn( "[vuex] mutation type: " + type + ". Silent option has been removed. " + 'Use the filter functionality in the vue-devtools' ); } };
commit方法会根据type找到并调用_mutations中的所有type对应的mutation方法,所以当没有namespace的时候,commit方法会触发所有module中的mutation方法。 再执行完所有的mutation之后会执行_subscribers中的所有订阅者。在commit结束以后则会调用这些_subscribers中的订阅者,这个订阅者模式提供给外部一个监视state变化的可能。
最后 本文主要介绍了Vuex组件库的一些源码实现,Vuex是一个非常优秀的Vue数据管理库,代码虽然不多但是结构很清晰,非常适合学习。希望小伙伴们能够自己去研读一番,如果有不懂得可以通过微信交流。写这篇文章也希望可以帮助到更多想要学习探索Vuex内部实现原理的同学。
本文完
参考:
Vuex 源码分析
Vuex 源码解析(如何阅读源代码实践篇)