var store = { debug: true, state: { message: 'Hello!' }, setMessageAction (newValue) { if (this.debug) console.log('setMessageAction triggered with', newValue) this.state.message = newValue }, clearMessageAction () { if (this.debug) console.log('clearMessageAction triggered') this.state.message = '' } }
所有 store 中 state 的改变,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的 mutation 将会发生,以及它们是如何被触发。当错误出现时,我们现在也会有一个 log 记录 bug 之前发生了什么,此外,每个实例/组件仍然可以拥有和管理自己的私有状态:
1 2 3 4 5 6 7 8 9 10 11 12 13
var vmA = new Vue({ data: { privateState: {}, sharedState: store.state } })
var vmB = new Vue({ data: { privateState: {}, sharedState: store.state } })
接着我们继续延伸约定,组件不允许直接修改属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,这样的话,一个Flux架构就实现了。
exportclassStore{ 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 && typeofwindow !== '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(typeofPromise !== 'undefined', `vuex requires a Promise polyfill in this browser.`) assert(thisinstanceof Store, `store must be called with the new operator.`) } // 省略无关代码 }
assert这个方法被定义在utils.js当中:
1 2 3
exportfunctionassert (condition, msg) { if (!condition) thrownewError(`[vuex] ${msg}`) }
由于Store使用的是单一的状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
// update nested modules if (newModule.modules) { for (const key in newModule.modules) { if (!targetModule.getChild(key)) { if (process.env.NODE_ENV !== 'production') { console.warn( `[vuex] trying to add a new module '${key}' on hot reloading, ` + 'manual reload is needed' ) } return } update( path.concat(key), targetModule.getChild(key), newModule.modules[key] ) } } }
const functionAssert = { assert: value =>typeof value === 'function', expected: 'function' }
const objectAssert = { assert: value =>typeof value === 'function' || (typeof value === 'object' && typeof value.handler === 'function'), expected: 'function or object with "handler" function' }
// Base data struct for store's module, package with some attribute and method exportdefaultclassModule{ constructor (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 const rawState = rawModule.state // Store the origin module's state // state() { // return { // // state here instead // } // } this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} }
get namespaced () { return !!this._rawModule.namespaced }
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)
/** * make localized dispatch, commit, getters and state * if there is no namespace, just use root ones */ functionmakeLocalContext (store, namespace, path) { const noNamespace = namespace === ''
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 } }
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) } })
// 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) } })
const splitPos = namespace.length Object.keys(store.getters).forEach(type => { // skip if the target getter is not match this namespace if (type.slice(0, splitPos) !== namespace) return
// extract local getter type const 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 }) })
// bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => 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 mixins const silent = Vue.config.silent Vue.config.silent = true store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent
// enable strict mode for new vm if (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()) } }
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。这里首先遍历wrappedGetters得到对应的fn组成的数组,然后将其定义为一个个计算属性computed[key] = () => fn(store),fn(store)其实就是执行了下面的方法:
1 2 3 4 5 6 7 8
store._wrappedGetters[type] = functionwrappedGetter (store) { return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ) }
/** * Reduce the code which written in Vue.js for getting the state. * @param {String} [namespace] - Module's namespace * @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it. * @param {Object} */ exportconst mapState = normalizeNamespace((namespace, states) => { const res = {} normalizeMap(states).forEach(({ key, val }) => { res[key] = functionmappedState () { let state = this.$store.state let getters = this.$store.getters if (namespace) { constmodule = getModuleByNamespace(this.$store, 'mapState', namespace) if (!module) { return } state = module.context.state getters = module.context.getters } returntypeof val === 'function' ? val.call(this, state, getters) : state[val] } // mark vuex getter for devtools res[key].vuex = true }) return res })
/** * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map. * @param {Function} fn * @return {Function} */ functionnormalizeNamespace (fn) { return(namespace, map) => { if (typeof namespace !== 'string') { map = namespace namespace = '' } elseif (namespace.charAt(namespace.length - 1) !== '/') { namespace += '/' } return fn(namespace, map) } }