前提知识
Vuex涉及Vue原理中的computed,Vue的响应式原理。
其实Vuex的原理可以归结为它源码里面的一句话(如下),下面来看看这句话的威力所在。
store._vm = new Vue({ data: { $$state: state }, computed })
install阶段
我们在使用Vuex的时候可能会这么书写配置。
import Vue from 'vue'; import Vuex from 'vuex'; import set = Reflect.set; Vue.use(Vuex); export default new Vuex.Store({ state: { }, getters: { }, mutations: { }, actions: { }, modules: {} });
配置第一句话就是Vue.use()。Vue的插件基本上都要执行这句话,这句话的作用就是执行插件的install方法。下面是install的源码:
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 = _Vue applyMixin(Vue) }
//applyMixin
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
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
}
}
}
install关键就是执行了applyMixin,applyMixin其实很好理解。它的作用就是在于给Vue的组件混入一个beforeCreate的钩子函数,这个钩子函数的作用其实就是在Vue的实例挂载这个$store属性。install阶段做的重要东西就这个
new Store阶段
export 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 #731 if (!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 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() this._makeLocalGettersCache = Object.create(null) // 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) } //我们执行store commit的时候是在执行这个方法 this.commit = function boundCommit (type, payload, options) { return commit.call(store, type, payload, options) } // strict mode this.strict = strict //这个state应该就是根目录的用户编写的state 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才会执行 plugins.forEach(plugin => plugin(this)) const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools if (useDevtools) { devtoolPlugin(this) } } //state的get get state () { //this._vm._data.$$state 就是用户写state写的东西 return this._vm._data.$$state } set state (v) { if (__DEV__) { assert(false, `use store.replaceState() to explicit replace store state.`) } } ... }
我们关注一句 resetStoreVM(this, state)。这句话的作用就是为我们用户写的state和Vue的视图更新产生关联重要地方。源码如下:
function resetStoreVM (store, state, hot) { const oldVm = store._vm // bind store public getters store.getters = {} // reset local getters cache store._makeLocalGettersCache = Object.create(null) 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 environment. computed[key] = partial(fn, store) Object.defineProperty(store.getters, key, { //这里应该才是真正执行computed函数的 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()) } }
我们首先关注forEachValue函数,它的源码如下:
export function forEachValue (obj, fn) { Object.keys(obj).forEach(key => fn(obj[key], key)) }
在上面调用它的使用传入了两个参数第一个是wrappedGetters,第二个函数是一个fn。wrappedGetters是一个对象,它存放着用户写的getters函数,例如你的getters是这么写的
getters: { getUser: state => state.username }, //那么wrappedGetters就长这样 wrappedGetters: { getUser: fn }
所以对他使用forEachValue方法就是遍历这个getters的每一个键,然后去执行fn,参数是getters里面用户写的函数和键的名称。从上面看fn是这个函数(如下):
(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 }) }
它接受用户getters里面的键值函数和getters键为参数。
这个函数的作用就是把getters里面的key,复制到computed中,并且执行一个叫做partical的函数。partical这个函数的作用其实就是执行用户的那个getters定义的函数而已,就不贴代码了。就是说computed和getters里面的东西有相同的键值
然后为store的getters属性,定义一个get的拦截器。
上面就是forEachValue的作用。
回到resetStoreVM上来,我们继续关注这段话。
store._vm = new Vue({ data: { $$state: state }, computed })
这句话在整个Vuex起着十分关键的作用。他在store上定义了一个_vm属性,这个属性的值是一个Vue的实例。并且传入一个data参数,他有一个$$state属性,这个属性的值就是用户编写的state对象。第二个参数是一个computed计算属性。通过上面对forEachValue的分析,我们知道了这个computed是一个对象,对象的键和用户自定义的getters里面的键是一样的。
那为什么Vuex需要这么做。
他把用户写的state作为新Vue实例的data,那么在Vue响应式原理中,当我们读取这个state里面的属性的时候就会开始收集依赖,意思就是说,假如你在别的组件使用计算属性或者别的方法引用着state里面的属性,那么这些依赖即Watcher就会被收集到state对象其下的属性中。意思就是说,一些别的组件引用着这个store里面的state,本质上就是在使用这个new Vue里面的data。当你这个data的state里面的属性一更新,就会通知引用它的组件进行更新。所以这就解释了为什么state里面的数据是响应式的。本质上就是通过这个new Vue组件去让这个state成为一个响应式数据,让它去收集使用着它的组件的Watcher依赖。
还有一个问题:就是Vuex的commit是怎么响应式的更新视图
commint的时候Vuex会通过一些方法执行到你的自定义函数,在我们的自定义函数中我们就会把store里面state的值进行修改。此时就会走回上面state响应式的流程。你修改state的值,就会触发Vue响应式里面的set拦截器,从而会导致视图的更新。这就是为什么commit修改数据之后可以响应式的更新视图。
还有一个小问题,在Vuex官方文档中,它的响应式规则有这么句话:
看圈住的两句话,为什么会是这样的。从上面的原理上看,我们在state里面写好的属性都是作为参数传入new Vue,还是这句话(如下),从而使他成为一个响应式的数据。
store._vm = new Vue({ data: { $$state: state }, computed})
但是假如你在中途突然增加一个属性,那么它是不会被作为new Vue的参数传入其中,从而这个属性不是响应式的,更加不可能收集到依赖进行视图的更新。