1.入口
分析Vue插件,首先应找到其install方法,这是插件的入口。Vuex的install如下:
// src/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)
}
复制代码
可以看到,Vuex保存了局部变量Vue,防止插件重复安装,接下来调用了applyMixin(Vue)。
这里似乎存在一个问题,即用Vue安装Vuex后,仍然可以用Vue的子类安装,但用Vue子类安装之后,保存的局部变量Vue就变成了子类,这样Vue使用Vuex时似乎会受到子类的影响。这里暂时不去探究,如果我想错了或者你有什么想法,请在评论中告诉我。
根据依赖import applyMixin from './mixin'
找到mixin,源码如下:
// src/mixin.js
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) { //2.0 and upper
// mixin Vue with a beforeCreate hook
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)
}
}
复制代码
install中对Vue 1.x 做了一个兼容,然后给Vue mixin了一个beforeCreate的钩子,往下看,这个钩子到底干了什么
function vuexInit () {
// 这是被vue实例在beforeCreate hook 是调用
// 给实例添加了一个$store 属性,是Vuex.Stroe的实例
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
}
}
复制代码
这个钩子的作用就是从新建Vue实例的options中找到store属性,然后赋值给vm.$store。如果当前options没有该属性,则从父类上找。这样只要根组件具有$store,则每个子组件都会具有$store属性。子组件也可以有一个不同于父级的$store属性。
2. $store解析
上面讲到Vuex会给每个vm实例都添加一个$store属性,那这个$store是什么呢?我们回想一下Vuex的用法:
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from 'xx/mutations'
import actions from 'xx/action'
import * as getters from 'xx/getters'
Vue.use(Vuex)
const state = {
//...
}
let store = new Vuex.Store({
state,
getters,
actions,
mutations
});
let vm = new Vue({
//...
store
});
vm.$store....
复制代码
所以,Vue实例上的$store属性,指向的是一个Vuex.Store的实例。然后我们回到store.js寻找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)
}
// 省略 测试语句 及插件语句
// 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 省略
}
复制代码
构造函数首句,如果当前没有安装Vuex但是Vue暴露在了window下,则自动安装,用途不详。
-
属性定义
接下来,给store实例定义了一些属性,以_开头,意为私有属性。
this._committing = false //flag
this._actions = Object.create(null) //定义的actions
this._actionSubscribers = [] //action的订阅者
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null) //包装Getter
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = [] //订阅者
this._watcherVM = new Vue()
复制代码
这里插一句,
Object.create(null)
表示以null为原型创建一个对象,这是一个纯净的空对象,它没有原型,所以不会继承Object.prototype.toString等方法,也没有contructor构造函数。
其中我们看一下_modules属性,它是以options为参数,新建的一个ModuleCollection。找到ModuleCollection。
// src/module/module-collection
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// 注册嵌套的modules,我们这里没有
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
// 其余省略
}
复制代码
构造函数中直接调用了register。我们往下看register。它新建了新建了一个newModule,并且由于狗造函数中传入的path为[],所以,这个newModule被赋值给ModuleCollection实例的root属性。
再去看看Module构造函数
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
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
复制代码
所以newModule其实是这样的:
newModule = {
runtime:false,
_children:{},
_rawModule:options, // 新建Store时传入的options
state:options.state //若是函数,则是求值后的,为空时则为{}
}
复制代码
所以store实例的_modules属性其实是这样的:
_modules:{
root:{ //Module对象的实例,方法省略
runtime:false,
_children:{},
_rawModule:options, // 新建Store时传入的options
state:options.state //若是函数,则是求值后的,为空时则为{}
}
}
复制代码
-
方法绑定
属性定义之后,是这样的代码:
// 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)
}
复制代码
目的是绑定dispatch和commit的运行环境为当前实例,为什么要这么做呢? 我们来看一下MapMutation的用法:
// vue模板中这样使用
methods:{
...mapMutations(['method1','method2'])
}
复制代码
这相当于把mutation方法赋值到了Vue实例上,然后可以通过vm.method1()这样的调用,此时method1可以绑定内部this为store,但method1内部使用的commit却无法绑定,所以需要预先绑定。
-
方法调用
Store构造方法的最后,调用了两个函数
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)
复制代码
state临时变量,其实就是我们options的state属性求值后的结果,所以我们的state可以定义成一个函数或者说应该定义成一个函数,就像Vue的data一样。
接下来先看一下installModule(this,state,[],this._modules.root)
函数
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length //true
//...子module命名空间相关,此处省略
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)
})
// child module 处理,省略...
}
复制代码
根据精简后的代码可以看到,代码先是定义了一个local局部变量,用来获取当前命名空间下的属性,这里暂时我们不需要关心,因为我们没有使用命名空间,可以把它看成是简单复制了store的几个属性。
然后使用了四个foreachXXX,看名字就应该知道是将options参数中的mutations、actions...注册到store上,以registerMutation(store, namespacedType, mutation, local)
为例:
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)
})
}
复制代码
可以看到这个函数的目的就是将options.mutations放入到store._mutations,并绑定了执行环境为store。假设你定义了如下mutation:
options = {
mutations:{
initData:()=>{},
initProps:()=>{}
}
}
复制代码
则store._mutations会变成:
_mutations:{
initData:[wrappedInitData],
initProps:[wrappedInitProps]
}
复制代码
然后到了resetStoreVM(this, state)
函数
function resetStoreVM (store, state, hot) {
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
})
})
// 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())
}
}
复制代码
首先用局部变量oldVm保存了store._vm属性,这是一个Vue实例,但目前并未定义
接着给store定义了getters属性,
然后遍历了store._wrappedGetters,用computed缓存了里面方法的闭包,同时以_wrappedGetters的keys给getters定义了若干存储器属性,代理访问store._vm的同名属性
接下来定义了store._vm,是一个Vue实例,里面的data包含一个属性$$state,以state赋值,还包含了前面定义的computed作为计算属性,即是保存了参数的_wrappedGetters。新建Vue实例时用到了Vue.config.silent开关,目的是取消Vue的所有日志和警告。 接下来就没有了,store的初始化已经完成。
总结一下,就是store定义了一个_vm,并将state作为data的属性,getters作为计算属性。所以说Vuex还是利用了Vue来完成响应式。
3.store长什么样
写了这么多,这里先总结一下经过初始化的store长成什么样,便于理解记忆
vm:{
$store:{
_committing:false,
_actions:{ //经过包装的选项中的actions
action1:[wrappedActionFunc],
// ...
},
_actionSubscribers:[],
_mutations:{ //经过包装的选项中的mutations
mutation1:[wrappedMutationFunc],
//...
},
_wrappedGetters:{
getter1:wrappedGetter //这里并不是数组,getter不能重复
},
_modules:{
root:{
runtime:false,
_children:{},
_rawModule:options, // 新建Store时传入的options
state //求值后的options.state
}
},
_modulesNamespaceMap:{},
_subscribers:[],
dispatch:()=>{},
commit:()=>{}, //这两个方法专门绑定过,内部执行环境始终指向store
getters:{
//根据options.getters的key定义的存储器属性,
// 实际访问的是store._vm的computed属性
},
_vm:{
data:{
$$state:state
},
computed:{
//由options.getters生成,被store.getters代理访问
}
},
state:{
get(){
},
set(v){
//无效,应使用 store.replaceState() 代替
}
},
subscribe:(fn)=>{},
subscribeAction:(fn)=>{},
watch:(getter, cb, options)=>{},
replaceState:(state)=>{},
registerModule:(path, rawModule, options = {})=>{},
unregisterModule:(path)=>{},
hotUpdate:(newOptions)=>{},
_withCommit:(fn)=>{}
}
}
复制代码
以上就是store初始化后的全貌了,接下来准备以一个简单的Vuex的使用例子去探寻更多的实现细节及原理。