Vuex 2.0 源码分析
Vuex 2.0 源码分析
在一般情况之下, 我们普遍使用 global event bus 来解决全局状态共享, 组件通讯的问题, 当遇到大型应用的时候, 这种方式将使代码变得难以维护, Vuex应运而生, 接下来我将从源码的角度分析Vuex的整个实现过程.
目录结构
整个Vuex的目录结构还是非常清晰地, index.js 是整个项目的入口, helpers.js 提供Vuex的辅助方法>, mixin.js 是$store注入到vue实例的方法, util.js 是一些工具函数, store.js是store类的实现 等等, 接下来就从项目入口一步步分析整个源码.
项目入口
首先我们可以从index.js看起:
export default { Store, install, version: '__VERSION__', mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers }
可以看到, index.js就是导出了一个Vuex对象, 这里可以看到Vuex暴露的api, Store就是一个Vuex提供的状态存储类, 通常就是使用 new Vuex.Store(...)的方式, 来创建一个Vuex的实例. 接下来看, install 方法, 在store.js中;
export function install (_Vue) { if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== 'production') { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } Vue = _Vue applyMixin(Vue) }
install 方法有个重复install的检测报错, 并将传入的_Vue赋值给自己定义的Vue变量, 而这个Vue变量已经变导出, 整个项目就可以使用Vue, 而不用安装Vue;
export let Vue
接着调用applyMixin方法, 该方法在mixin.js当中;
export default function (Vue) { const version = Number(Vue.version.split('.')[0]) Vue.mixin({ beforeCreate: vuexInit }) }
所以, applyMixin方法的逻辑就是全局混入一个beforeCreate钩子函数-vuexInit;
function vuexInit () { const options = this.$options // store injection 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 } }
整个代码很简单, 就是将用户传入的store注入到每个vue实例的$store属性中去, 从而在每个实例我们都可以通过调用this.$store.xx访问到Vuex的数据和状态;
Store类
在我们使用Vuex的时候, 通常会实例化一个Vuex.Store类, 传入一个对象, 对象包括state、getters、mutations、actions、modules, 而我们实例化的时候, Vuex到底做了什么呢? 带着这个疑问, 我们一起来看store.js中的代码, 首先是构造函数;
constructor (options = {}) { // Auto install if it is not done yet and `window` has `Vue`. // To allow users to avoid auto-installation in some cases, // this code should be placed here. See #731 if (!Vue && typeof window !== 'undefined' && window.Vue) { install(window.Vue) } if (process.env.NODE_ENV !== 'production') { 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.`) } const { plugins = [], strict = false } = options // store internal state this._committing = false this._actions = Object.create(null) this._actionSubscribers = [] this._mutations = Object.create(null) this._wrappedGetters = Object.create(null) this._modules = new ModuleCollection(options) this._modulesNamespaceMap = Object.create(null) this._subscribers = [] this._watcherVM = new Vue() // bind commit and dispatch to self const store = this const { dispatch, commit } = this 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) } // strict mode this.strict = strict const state = this._modules.root.state // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters installModule(this, state, [], this._modules.root) // initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) resetStoreVM(this, state) // apply plugins plugins.forEach(plugin => plugin(this)) const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools if (useDevtools) { devtoolPlugin(this) } }
构造函数一开始是判断当window.Vue存在的时候, 调用install方法, 确保script加载的Vuex可以正确被安装, 接着是三个断言函数, 确保Vue存在, 环境支持Promise, 当前环境的this是Store;
const { plugins = [], strict = false } = options
利用es6的赋值结构拿到options中的plugins(默认是[]), strict(默认是false), plugins 表示应用的插件、strict 表示是否开启严格模式, 接着往下看;
// store internal state this._committing = false this._actions = Object.create(null) this._actionSubscribers = [] this._mutations = Object.create(null) this._wrappedGetters = Object.create(null) this._modules = new ModuleCollection(options) this._modulesNamespaceMap = Object.create(null) this._subscribers = [] this._watcherVM = new Vue()
这里主要是初始化一些Vuex内部的属性, _开头, 一般代表着私有属性,this._committing
标志着一个提交状态;this._actions
存储用户的所有的actions;this.mutations
存储用户所有的mutations;this.wrappedGetters
存储用户所有的getters;this._subscribers
用来存储所有对 mutation 变化的订阅者;this._modules
表示所有modules的集合;this._modulesNamespaceMap
表示子模块名称记录.
继续往下看:
// bind commit and dispatch to self const store = this const { dispatch, commit } = this 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) } // strict mode this.strict = strict const state = this._modules.root.state
这段代码就是通过赋值结构拿到store对象的dispatch, commit 方法, 并重新定义store的dispatch, commit 方法, 使他们的this指向store的实例, 具体的dispatch和comiit实现稍后分析.
Vuex核心
installModule方法
installModule方法主要是根据用户传入的options, 进行各个模块的安装和注册, 具体实现如下:
function installModule (store, rootState, path, module, hot) { const isRoot = !path.length const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') { console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`) } store._modulesNamespaceMap[namespace] = module } // set state if (!isRoot && !hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) } const local = module.context = makeLocalContext(store, namespace, path) module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) }) module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) }
installModules方法需要传入5个参数, store, rootState, path, module, hot; store指的是当前Store实例, rootState是根实例的state, path当前子模块的路径数组, module指的是当前的安装模块, hot 当动态改变 modules 或者热更新的时候为 true。
先看这段代码:
if (module.namespaced) { if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') { console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`) } store._modulesNamespaceMap[namespace] = module }
这段代码主要是为了防止子模块命名重复, 故定义了一个map记录每个子模块;
接下来看下面的代码:
// set state if (!isRoot && !hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) }
这里判断当不为根且非热更新的情况,然后设置级联状态,这里乍一看不好理解,我们先放一放,稍后来回顾。
再往下看代码:
const local = module.context = makeLocalContext(store, namespace, path)
首先, 定义一个local变量来接收makeLocalContext函数返回的结果, makeLocalContext有三个参数, store指的是根实例, namespace 指的是命名空间字符, path是路径数组;
function makeLocalContext (store, namespace, path) { const noNamespace = namespace === '' const local = { dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args if (!options || !options.root) { type = namespace + type if (process.env.NODE_ENV !== 'production' && !store._actions[type]) { console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`) return } } return store.dispatch(type, payload) }, commit: noNamespace ? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args if (!options || !options.root) { type = namespace + type if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) { console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`) return } } store.commit(type, payload, options) } } // getters and state object must be gotten lazily // because they will be changed by vm update Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, state: { get: () => getNestedState(store.state, path) } }) return local }
makeLocalContext 函数主要的功能就是根据是否有namespce定义不同的dispatch和commit, 并监听local的getters和sate的get属性, 那namespace是从何而来呢, 在installModule的开始:
const isRoot = !path.length const namespace = store._modules.getNamespace(path)
namespace 是根据path数组通过_modules中的getNamespace获得, 而store._modules是ModuleCollection的实例, 所以可以到ModuleCollection中找到getNamespace方法:
getNamespace (path) { let module = this.root return path.reduce((namespace, key) => { module = module.getChild(key) return namespace + (module.namespaced ? key + '/' : '') }, '') }
该函数通过对path路径数组reduce遍历, 获得模块的命名空间(eg: 'city/');,接下来是各个模块的注册流程, 首先看mutaiton的注册;
module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) })
forEachMutation函数一个循环遍历, 拿到用户传入的mutation函数和key值, 接着调用registerMutation函数;
// $store.state.commit('add', 1) function registerMutation (store, type, handler, local) { const entry = store._mutations[type] || (store._mutations[type] = []) entry.push(function wrappedMutationHandler (payload) { handler.call(store, local.state, payload) }) }
这段代码的作用就是, 将所有的mutation函数封装成wrappedMutationHandler存入store._mutations
这个对象当中, 我们结合前面提过的commit的过程, 可以更好的理解;
commit (_type, _payload, _options) { // check object-style commit const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } const entry = this._mutations[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown mutation type: ${type}`) } return } this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) this._subscribers.forEach(sub => sub(mutation, this.state)) if ( process.env.NODE_ENV !== 'production' && options && options.silent ) { console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } }
unifyObjectStyle 函数就是对参数的规范, 而后, 通过`
this._mutations[type] 拿到type所对应的所有wrappedMutationHandler函数, 遍历执行, 传入payload,
this._withCommit`函数在源码中出现过很多次, 代码如下:
_withCommit (fn) { const committing = this._committing this._committing = true fn() this._committing = committing }
代码作用就是每次提交的时候, 将this._committing
置为true, 执行完提交操作之后, 在重新置为初始状态, 确保只有mutation才能更改state的值, _subscribers相关代码暂时不看, 我们接下来看一看action的注册流程:
module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) })
这段代码和mutation的注册流程是类似的, 不同在于registerAction函数
function registerAction (store, type, handler, local) { const entry = store._actions[type] || (store._actions[type] = []) entry.push(function wrappedActionHandler (payload, cb) { let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb) if (!isPromise(res)) { res = Promise.resolve(res) } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) }
可以看到, 基于用户的action函数, 源码封多了一层wrappedActionHandler函数, 在action函数中, 可以获得一个context对象, 就是在这里做的处理, 然后, 它把action函数的执行结果封装成了Promise并返回, 结合dispatch函数可以更好的理解;
dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload) const action = { type, payload } const entry = this._actions[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown action type: ${type}`) } return } const result = entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) return result.then(res => { return res }) }
dispatch 拿到actions后, 根据数组长度, 执行Promise.all或者直接执行, 然后通过then函数拿到promise resolve的结果.
接下来是getters的注册
module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) })
registerGetter函数:
function registerGetter (store, type, rawGetter, local) { // 不允许重复 if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] duplicate getter key: ${type}`) } return } store._wrappedGetters[type] = function wrappedGetter (store) { return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ) } }
将用户传入的rawGetter封装成wrappedGetter, 放入store._wrappedGetters的对象中, 函数的执行稍后再说, 我们继续子模块的安装;
module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })
这段代码首先是对state.modules遍历, 递归调用installModule, 这时候的path是不为空数组的, 所以会走到这个逻辑;
// set state if (!isRoot && !hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) }
通过getNestedState找到它的父state, 它的模块key就是path的最后一项, store._withCommit
上面已经解释过了, 然后通过Vue.set 将子模块响应式的添加到父state, 从而将子模块都注册完毕.
resetStoreVM 方法
resetStoreVM 函数第一部分
const oldVm = store._vm // bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism // direct inline function use will lead to closure preserving oldVm. // using partial to return function with only arguments preserved in closure enviroment. computed[key] = partial(fn, store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) })
首先, 拿到所有的wrappedGetter函数对象, 即包装过的用户传入的getters, 定义一个变量computed, 接受所有的函数, 并通过Ojbect.defineProperty
在store.getters属性定义了get方法, 也就是说, 我们通过this.$store.getters.xx 会访问到 store._vm[xx], 而store._vm又是什么呢?
// use a Vue instance to store the state tree // suppress warnings just in case the user has added // some funky global mixins const silent = Vue.config.silent Vue.config.silent = true // 关闭vue警告, 提醒 store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent
显然, store._vm是一个Vue的实例, 包含所有用户getters的计算属性和 用户state的$$state属性, 而我们访问this.$store.state 其实就是访问这里的$$state属性, 原因在于, Store类直接定义了一个state的取值函数, 其中返回的正是这个$$state属性;
get state () { return this._vm._data.$$state }
我们接着看;
// enable strict mode for new vm if (store.strict) { enableStrictMode(store) }
当在Vuex严格模式下, strict为true, 所以会执行enableStrictMode函数;
function enableStrictMode (store) { store._vm.$watch(function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== 'production') { assert(store._committing, `do not mutate vuex store state outside mutation handlers.`) } }, { deep: true, sync: true }) }
该函数利用Vue.$watch函数, 监听$$state的变化, 当store._committing 为false的话, 就会抛出不允许在mutation函数之外操作state;
接着我们再来看最后一部分;
if (oldVm) { if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) }
oldVm保存着上一个store._vm对象的引用, 每次执行这个函数, 都会创建一个新的store._vm, 所以需要在这段代码中销毁;
至此, Store类初始化大致都讲完了, 接下来分析Vuex提供的辅助函数.
辅助函数
mapstate
export const mapState = normalizeNamespace((namespace, states) => { const res = {} normalizeMap(states).forEach(({ key, val }) => { res[key] = function mappedState () { let state = this.$store.state let getters = this.$store.getters if (namespace) { const module = getModuleByNamespace(this.$store, 'mapState', namespace) if (!module) { return } state = module.context.state getters = module.context.getters } return typeof val === 'function' ? val.call(this, state, getters) : state[val] } // mark vuex getter for devtools res[key].vuex = true }) return res })
首先, 先说一说normalizeMap方法, 该方法主要是用于格式化参数, 用户使用mapState函数, 可以使传入一个字符串数组, 也可以是传入一个对象, 经过normalizeMap方法处理, 统一返回一个对象数组;;
// normalizeMap([1,2]) => [{key: 1, val: 1}, {key: 2, val: 2}] // normalizeMap({a: 1, b: 2}) => [{key: 'a', val: 1}, {key: 'b', val: 2}] function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) }
接着, 对于处理过的对象数组遍历, 定义了一个res对象接收, key为键, mappedState方法为值;
function mappedState () { let state = this.$store.state let getters = this.$store.getters if (namespace) { const module = getModuleByNamespace(this.$store, 'mapState', namespace) if (!module) { return } state = module.context.state getters = module.context.getters } return typeof val === 'function' ? val.call(this, state, getters) : state[val] }
整个函数代码比较简单, 唯一需要注意的点是, 当传入了namespace时, 需要通过getModuleByNamespace函数找到该属性对应的module, 还记得在installModule中, 有在store._modulesNamespaceMap
中记录namespace和模块间的对应关系, 因此, getModuleByNamespace就是通过这个map找到了module, 从而拿到了当前module的state和getters;
最后mapstate函数返回一个res函数对象, 用户可以直接利用...操作符导入到计算属性中.
mapMutations
mapMutations函数和mapstate函数是类似的, 唯一的区别在于mappedMutation是commit 函数代理, 并且它需要被导入到methods;
function mappedMutation (...args) { // Get the commit method from store let commit = this.$store.commit if (namespace) { const module = getModuleByNamespace(this.$store, 'mapMutations', namespace) if (!module) { return } commit = module.context.commit } return typeof val === 'function' ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) }
mapActions, mapGetters 的实现也都大同小异, 便不再具体分析.
plugins选项
我们可以通过类似这种方式使用plugins:
const myPlugin = store => { // 当 store 初始化后调用 store.subscribe((mutation, state) => { // 每次 mutation 之后调用 // mutation 的格式为 { type, payload } } )} const store = new Vuex.Store({ // ... plugins: [myPlugin] })
在源码当中, 可以看到这么一段代码:
// apply plugins plugins.forEach(plugin => plugin(this))
即遍历所有plugins, 传入当前Store实例, 执行plugin函数, 因此, 示例的store参数就是Store实例, 然后示例调用store.subscribe
方法, 这是Store类暴露的一个成员方法;
subscribe (fn) { return genericSubscribe(fn, this._subscribers) }
其实这就是一个订阅函数, 当有commit操作的时候, 就会通知所有订阅者, 该函数返回一个函数fn, 调用这个fn即可以取消订阅, 发布通知代码在commit函数中:
this._subscribers.forEach(sub => sub(mutation, this.state))
结语
当学无所学之时, 看优秀源码或许是一种突破瓶颈的方法, 可以更加深入的了解这个库, 知其然, 亦知其所以然, 同时作者的一些库的设计思想, 也会对我们大有裨益.