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))
结语
当学无所学之时, 看优秀源码或许是一种突破瓶颈的方法, 可以更加深入的了解这个库, 知其然, 亦知其所以然, 同时作者的一些库的设计思想, 也会对我们大有裨益.