
https://tech.meituan.com/2017/04/27/vuex-code-analysis.html
Vuex 3.x 源码解析
index.js 对外暴漏了可以访问的方法:
- install:vuex 的安装 plugin
- Store 类
- mapState, mapMutations, mapGetters, mapAction ```javascript import { Store, install } from ‘./store’ import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from ‘./helpers’ import createLogger from ‘./plugins/logger’
export default { Store, install, version: ‘VERSION‘, mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers, createLogger }
export { Store, install, mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers, createLogger }
Vuex 一共有三个核心类:- Store- Module- ModuleCollectionStore 类主要有以下几个方法:- commit- dispatch- subscribe- subscribeAction- watch- replaceState- registerModule- unregisterModule- hasModule- hotUpdate- _withCommitStore Utils 核心方法:- genericSubscribe- resetStore- resetStoreVM(resetStoreState)- installModule- makeLocalContext- makeLocalGetters- registerMutation- registerAction- registerGetter- enableStrictMode- getNestedState- unifyObjectStyle```javascriptimport applyMixin from './mixin'import devtoolPlugin from './plugins/devtool'import ModuleCollection from './module/module-collection'import { forEachValue, isObject, isPromise, assert, partial } from './util'let Vue // bind on installexport class Store {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 #731if (!Vue && typeof window !== 'undefined' && window.Vue) {install(window.Vue)}if (__DEV__) {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 statethis._committing = falsethis._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()this._makeLocalGettersCache = Object.create(null)// bind commit and dispatch to selfconst store = thisconst { dispatch, commit } = thisthis.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 modethis.strict = strictconst state = this._modules.root.state// init root module.// this also recursively registers all sub-modules// and collects all module getters inside this._wrappedGettersinstallModule(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 pluginsplugins.forEach(plugin => plugin(this))const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtoolsif (useDevtools) {devtoolPlugin(this)}}get state () {return this._vm._data.$$state}set state (v) {if (__DEV__) {assert(false, `use store.replaceState() to explicit replace store state.`)}}commit (_type, _payload, _options) {// check object-style commitconst {type,payload,options} = unifyObjectStyle(_type, _payload, _options)const mutation = { type, payload }const entry = this._mutations[type]if (!entry) {if (__DEV__) {console.error(`[vuex] unknown mutation type: ${type}`)}return}this._withCommit(() => {entry.forEach(function commitIterator (handler) {handler(payload)})})this._subscribers.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe.forEach(sub => sub(mutation, this.state))if (__DEV__ &&options && options.silent) {console.warn(`[vuex] mutation type: ${type}. Silent option has been removed. ` +'Use the filter functionality in the vue-devtools')}}dispatch (_type, _payload) {// check object-style dispatchconst {type,payload} = unifyObjectStyle(_type, _payload)const action = { type, payload }const entry = this._actions[type]if (!entry) {if (__DEV__) {console.error(`[vuex] unknown action type: ${type}`)}return}try {this._actionSubscribers.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe.filter(sub => sub.before).forEach(sub => sub.before(action, this.state))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in before action subscribers: `)console.error(e)}}const result = entry.length > 1? Promise.all(entry.map(handler => handler(payload))): entry[0](payload)return new Promise((resolve, reject) => {result.then(res => {try {this._actionSubscribers.filter(sub => sub.after).forEach(sub => sub.after(action, this.state))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in after action subscribers: `)console.error(e)}}resolve(res)}, error => {try {this._actionSubscribers.filter(sub => sub.error).forEach(sub => sub.error(action, this.state, error))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in error action subscribers: `)console.error(e)}}reject(error)})})}subscribe (fn, options) {return genericSubscribe(fn, this._subscribers, options)}subscribeAction (fn, options) {const subs = typeof fn === 'function' ? { before: fn } : fnreturn genericSubscribe(subs, this._actionSubscribers, options)}watch (getter, cb, options) {if (__DEV__) {assert(typeof getter === 'function', `store.watch only accepts a function.`)}return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)}replaceState (state) {this._withCommit(() => {this._vm._data.$$state = state})}registerModule (path, rawModule, options = {}) {if (typeof path === 'string') path = [path]if (__DEV__) {assert(Array.isArray(path), `module path must be a string or an Array.`)assert(path.length > 0, 'cannot register the root module by using registerModule.')}this._modules.register(path, rawModule)installModule(this, this.state, path, this._modules.get(path), options.preserveState)// reset store to update getters...resetStoreVM(this, this.state)}unregisterModule (path) {if (typeof path === 'string') path = [path]if (__DEV__) {assert(Array.isArray(path), `module path must be a string or an Array.`)}this._modules.unregister(path)this._withCommit(() => {const parentState = getNestedState(this.state, path.slice(0, -1))Vue.delete(parentState, path[path.length - 1])})resetStore(this)}hasModule (path) {if (typeof path === 'string') path = [path]if (__DEV__) {assert(Array.isArray(path), `module path must be a string or an Array.`)}return this._modules.isRegistered(path)}hotUpdate (newOptions) {this._modules.update(newOptions)resetStore(this, true)}_withCommit (fn) {const committing = this._committingthis._committing = truefn()this._committing = committing}}function genericSubscribe (fn, subs, options) {if (subs.indexOf(fn) < 0) {options && options.prepend? subs.unshift(fn): subs.push(fn)}return () => {const i = subs.indexOf(fn)if (i > -1) {subs.splice(i, 1)}}}function resetStore (store, hot) {store._actions = Object.create(null)store._mutations = Object.create(null)store._wrappedGetters = Object.create(null)store._modulesNamespaceMap = Object.create(null)const state = store.state// init all modulesinstallModule(store, state, [], store._modules.root, true)// reset vmresetStoreVM(store, state, hot)}function resetStoreVM (store, state, hot) {const oldVm = store._vm// bind store public gettersstore.getters = {}// reset local getters cachestore._makeLocalGettersCache = Object.create(null)const wrappedGetters = store._wrappedGettersconst 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 environment.computed[key] = partial(fn, store)Object.defineProperty(store.getters, key, {get: () => store._vm[key],enumerable: true // for local getters})})// use a Vue instance to store the state tree// suppress warnings just in case the user has added// some funky global mixinsconst silent = Vue.config.silentVue.config.silent = truestore._vm = new Vue({data: {$$state: state},computed})Vue.config.silent = silent// enable strict mode for new vmif (store.strict) {enableStrictMode(store)}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())}}function installModule (store, rootState, path, module, hot) {const isRoot = !path.lengthconst namespace = store._modules.getNamespace(path)// register in namespace mapif (module.namespaced) {if (store._modulesNamespaceMap[namespace] && __DEV__) {console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)}store._modulesNamespaceMap[namespace] = module}// set stateif (!isRoot && !hot) {const parentState = getNestedState(rootState, path.slice(0, -1))const moduleName = path[path.length - 1]store._withCommit(() => {if (__DEV__) {if (moduleName in parentState) {console.warn(`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`)}}Vue.set(parentState, moduleName, module.state)})}const local = module.context = makeLocalContext(store, namespace, path)module.forEachMutation((mutation, key) => {const namespacedType = namespace + keyregisterMutation(store, namespacedType, mutation, local)})module.forEachAction((action, key) => {const type = action.root ? key : namespace + keyconst handler = action.handler || actionregisterAction(store, type, handler, local)})module.forEachGetter((getter, key) => {const namespacedType = namespace + keyregisterGetter(store, namespacedType, getter, local)})module.forEachChild((child, key) => {installModule(store, rootState, path.concat(key), child, hot)})}/*** make localized dispatch, commit, getters and state* if there is no namespace, just use root ones*/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 } = argslet { type } = argsif (!options || !options.root) {type = namespace + typeif (__DEV__ && !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 } = argslet { type } = argsif (!options || !options.root) {type = namespace + typeif (__DEV__ && !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 updateObject.defineProperties(local, {getters: {get: noNamespace? () => store.getters: () => makeLocalGetters(store, namespace)},state: {get: () => getNestedState(store.state, path)}})return local}function makeLocalGetters (store, namespace) {if (!store._makeLocalGettersCache[namespace]) {const gettersProxy = {}const splitPos = namespace.lengthObject.keys(store.getters).forEach(type => {// skip if the target getter is not match this namespaceif (type.slice(0, splitPos) !== namespace) return// extract local getter typeconst 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: () => store.getters[type],enumerable: true})})store._makeLocalGettersCache[namespace] = gettersProxy}return store._makeLocalGettersCache[namespace]}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)})}function registerAction (store, type, handler, local) {const entry = store._actions[type] || (store._actions[type] = [])entry.push(function wrappedActionHandler (payload) {let res = handler.call(store, {dispatch: local.dispatch,commit: local.commit,getters: local.getters,state: local.state,rootGetters: store.getters,rootState: store.state}, payload)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}})}function registerGetter (store, type, rawGetter, local) {if (store._wrappedGetters[type]) {if (__DEV__) {console.error(`[vuex] duplicate getter key: ${type}`)}return}store._wrappedGetters[type] = function wrappedGetter (store) {return rawGetter(local.state, // local statelocal.getters, // local gettersstore.state, // root statestore.getters // root getters)}}function enableStrictMode (store) {store._vm.$watch(function () { return this._data.$$state }, () => {if (__DEV__) {assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)}}, { deep: true, sync: true })}function getNestedState (state, path) {return path.reduce((state, key) => state[key], state)}function unifyObjectStyle (type, payload, options) {if (isObject(type) && type.type) {options = payloadpayload = typetype = type.type}if (__DEV__) {assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)}return { type, payload, options }}export function install (_Vue) {if (Vue && _Vue === Vue) {if (__DEV__) {console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.')}return}Vue = _VueapplyMixin(Vue)}
Store 类核心原理

- 初始化 store 的内部状态
绑定 dispatch 和 commit
const { dispatch, commit } = thisthis.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)}
初始化 root module:
installModule(this, state, [], this._modules.root)- 初始化 store state,它负责状态的响应式:
resetStoreState,vuex3 是resetStoreVMState 的响应式原理
其实就是把 state 挂载到 vue 上。
State 修改方法
_withCommit是一个代理方法,所有触发mutation的进行state修改的操作都经过它,由此来统一管理监控state状态的修改。实现代码如下。
_withCommit (fn) {// 保存之前的提交状态const committing = this._committing// 进行本次提交,若不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告this._committing = true// 执行state的修改操作fn()// 修改完成,还原本次修改之前的状态this._committing = committing}
commit(mutation) 和 dispatch(action) 的原理
commit 和 dispatch 原理类似,都是通过触发 entry(commit 是 mutation[type],dispatch 是 action[type]),订阅者遍历执行。
this._subscribers.slice().forEach(sub => sub(mutation, this.state)this._actionSubscribers.filter(sub => sub.before).forEach(sub => sub.before(action, this.state))this._actionSubscribers.filter(sub => sub.after).forEach(sub => sub.after(action, this.state))
commit 通过 _withCommit来改变 state,
dispatch 通过下述代码拿到结果:
const result = entry.length > 1? Promise.all(entry.map(handler => handler(payload))): entry[0](payload)
commit 源码如下:
commit (_type, _payload, _options) {// check object-style commitconst {type,payload,options} = unifyObjectStyle(_type, _payload, _options)const mutation = { type, payload }const entry = this._mutations[type]if (!entry) {if (__DEV__) {console.error(`[vuex] unknown mutation type: ${type}`)}return}this._withCommit(() => {entry.forEach(function commitIterator (handler) {handler(payload)})})this._subscribers.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe.forEach(sub => sub(mutation, this.state))if (__DEV__ &&options && options.silent) {console.warn(`[vuex] mutation type: ${type}. Silent option has been removed. ` +'Use the filter functionality in the vue-devtools')}}
dispatch 源码如下:
dispatch (_type, _payload) {// check object-style dispatchconst {type,payload} = unifyObjectStyle(_type, _payload)const action = { type, payload }const entry = this._actions[type]if (!entry) {if (__DEV__) {console.error(`[vuex] unknown action type: ${type}`)}return}try {this._actionSubscribers.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe.filter(sub => sub.before).forEach(sub => sub.before(action, this.state))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in before action subscribers: `)console.error(e)}}const result = entry.length > 1? Promise.all(entry.map(handler => handler(payload))): entry[0](payload)return new Promise((resolve, reject) => {result.then(res => {try {this._actionSubscribers.filter(sub => sub.after).forEach(sub => sub.after(action, this.state))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in after action subscribers: `)console.error(e)}}resolve(res)}, error => {try {this._actionSubscribers.filter(sub => sub.error).forEach(sub => sub.error(action, this.state, error))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in error action subscribers: `)console.error(e)}}reject(error)})})}
devtools 原理
核心的 travel 原理如下:
- 通过 devtoolHook.on 方法注册事件:
devtoolHook.on('vuex:travel-to-state') - 调用 store 的 replaceState 完成旅行穿梭 ```javascript const target = typeof window !== ‘undefined’ ? window : typeof global !== ‘undefined’ ? global : {} const devtoolHook = target.VUE_DEVTOOLS_GLOBAL_HOOK
export default function devtoolPlugin (store) { if (!devtoolHook) return
store._devtoolHook = devtoolHook
devtoolHook.emit(‘vuex:init’, store)
devtoolHook.on(‘vuex:travel-to-state’, targetState => { store.replaceState(targetState) })
store.subscribe((mutation, state) => { devtoolHook.emit(‘vuex:mutation’, mutation, state) }, { prepend: true })
store.subscribeAction((action, state) => { devtoolHook.emit(‘vuex:action’, action, state) }, { prepend: true }) }
<a name="yujuo"></a>## Vuex 4.x 源码解析相比于 vuex 3.x,vuex4 将操作 store 的一个 utils function 单独抽离了出来,并且没有提供 `install` 方法来安装 store,而是通过 `createStore` 函数来创建 Store,`useStore` hooks 来 inject(vue3 的 api)来安装 store```javascriptimport { watch } from 'vue'import { storeKey } from './injectKey'import { addDevtools } from './plugins/devtool'import ModuleCollection from './module/module-collection'import { assert } from './util'import {genericSubscribe,getNestedState,installModule,resetStore,resetStoreState,unifyObjectStyle} from './store-util'export function createStore (options) {return new Store(options)}export class Store {constructor (options = {}) {if (__DEV__) {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,devtools} = options// store internal statethis._committing = falsethis._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._makeLocalGettersCache = Object.create(null)// EffectScope instance. when registering new getters, we wrap them inside// EffectScope so that getters (computed) would not be destroyed on// component unmount.this._scope = nullthis._devtools = devtools// bind commit and dispatch to selfconst store = thisconst { dispatch, commit } = thisthis.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 modethis.strict = strictconst state = this._modules.root.state// init root module.// this also recursively registers all sub-modules// and collects all module getters inside this._wrappedGettersinstallModule(this, state, [], this._modules.root)// initialize the store state, which is responsible for the reactivity// (also registers _wrappedGetters as computed properties)resetStoreState(this, state)// apply pluginsplugins.forEach(plugin => plugin(this))}install (app, injectKey) {app.provide(injectKey || storeKey, this)app.config.globalProperties.$store = thisconst useDevtools = this._devtools !== undefined? this._devtools: __DEV__ || __VUE_PROD_DEVTOOLS__if (useDevtools) {addDevtools(app, this)}}get state () {return this._state.data}set state (v) {if (__DEV__) {assert(false, `use store.replaceState() to explicit replace store state.`)}}commit (_type, _payload, _options) {// check object-style commitconst {type,payload,options} = unifyObjectStyle(_type, _payload, _options)const mutation = { type, payload }const entry = this._mutations[type]if (!entry) {if (__DEV__) {console.error(`[vuex] unknown mutation type: ${type}`)}return}this._withCommit(() => {entry.forEach(function commitIterator (handler) {handler(payload)})})this._subscribers.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe.forEach(sub => sub(mutation, this.state))if (__DEV__ &&options && options.silent) {console.warn(`[vuex] mutation type: ${type}. Silent option has been removed. ` +'Use the filter functionality in the vue-devtools')}}dispatch (_type, _payload) {// check object-style dispatchconst {type,payload} = unifyObjectStyle(_type, _payload)const action = { type, payload }const entry = this._actions[type]if (!entry) {if (__DEV__) {console.error(`[vuex] unknown action type: ${type}`)}return}try {this._actionSubscribers.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe.filter(sub => sub.before).forEach(sub => sub.before(action, this.state))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in before action subscribers: `)console.error(e)}}const result = entry.length > 1? Promise.all(entry.map(handler => handler(payload))): entry[0](payload)return new Promise((resolve, reject) => {result.then(res => {try {this._actionSubscribers.filter(sub => sub.after).forEach(sub => sub.after(action, this.state))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in after action subscribers: `)console.error(e)}}resolve(res)}, error => {try {this._actionSubscribers.filter(sub => sub.error).forEach(sub => sub.error(action, this.state, error))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in error action subscribers: `)console.error(e)}}reject(error)})})}subscribe (fn, options) {return genericSubscribe(fn, this._subscribers, options)}subscribeAction (fn, options) {const subs = typeof fn === 'function' ? { before: fn } : fnreturn genericSubscribe(subs, this._actionSubscribers, options)}watch (getter, cb, options) {if (__DEV__) {assert(typeof getter === 'function', `store.watch only accepts a function.`)}return watch(() => getter(this.state, this.getters), cb, Object.assign({}, options))}replaceState (state) {this._withCommit(() => {this._state.data = state})}registerModule (path, rawModule, options = {}) {if (typeof path === 'string') path = [path]if (__DEV__) {assert(Array.isArray(path), `module path must be a string or an Array.`)assert(path.length > 0, 'cannot register the root module by using registerModule.')}this._modules.register(path, rawModule)installModule(this, this.state, path, this._modules.get(path), options.preserveState)// reset store to update getters...resetStoreState(this, this.state)}unregisterModule (path) {if (typeof path === 'string') path = [path]if (__DEV__) {assert(Array.isArray(path), `module path must be a string or an Array.`)}this._modules.unregister(path)this._withCommit(() => {const parentState = getNestedState(this.state, path.slice(0, -1))delete parentState[path[path.length - 1]]})resetStore(this)}hasModule (path) {if (typeof path === 'string') path = [path]if (__DEV__) {assert(Array.isArray(path), `module path must be a string or an Array.`)}return this._modules.isRegistered(path)}hotUpdate (newOptions) {this._modules.update(newOptions)resetStore(this, true)}_withCommit (fn) {const committing = this._committingthis._committing = truefn()this._committing = committing}}
