Vuex源码解析——结构初始化
和Redux相似,在Vue中也有一套状态管理模式。相比于Redux,其充分利用了Vue数据劫持的响应式机制特点进行状态更新,显得更加简洁和高效,但相对应的也舍弃了Redux的通用性。下面就从源码来梳理Vuex的初始化流程。
store注入
通过Vuex获取的state都是从store实例中读取的,由此导致的问题就是每个需要使用state的组件中都要频繁导入。
Vuex通过在根组件中使用store选项注入到每一个子组件中,使用如下:
Vue.use(Vuex);
const app = new Vue({
el: '#app',
store
})
子组件就能通过this.$store访问到了。那么这个过程是如何实现的呢?
其实看到Vue.use就应该很熟悉了,这是Vue使用插件的一个通用方法。也就是说Vuex可以看做一个插件,注入方法也遵守Vue第三方插件的通用安装规则。先看Vuex的入口文件index.js。
export default {
Store,
install,
mapState,
mapMutations,
mapGetters,
mapActions
}
这个文件就是 Vuex对外暴露的API。其中的install方法就是我们的注入方法了。
function install (_Vue) {
if (Vue) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
return
}
Vue = _Vue
applyMixin(Vue)
}
// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
这段代码主要做了两件事。
- 防止Vuex被重复安装
- 通过applyMixin方法初始化Vuex
再进入applyMixin看其实现:
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
Vue.mixin(usesInit ? { init: vuexInit } : { 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 = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
可以看到主要是针对Vue版本进行不同的mixin处理。
如果是1.X版本,则把vuexInit方法混入到init阶段中。如果是2.X版本则混入到beforeCreate阶段中。
vuexInit就是完成Vue实例注入store的核心方法了。
store实例化
初始化内部变量
现在有了注入store的机制,当然store实例对象也不是随便传入的。必须依照Vuex的store构造函数传入拥有state,getters等等规定属性的对象去构造。下面就来看store构造函数做的事。
先看其实现源码:
class Store {
constructor (options = {}) {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
const {
state = {},
plugins = [],
strict = false
} = options
// store internal state
this._options = options
this._committing = false
this._actions = Object.create(null)
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._runtimeModules = 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
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], options)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
}
...
}
首先做两个断言达到两个目的:
- 确保Vuex的安装
- 确保Promise可用,因为Vuex依赖了Promise
然后创建了一些内部需要的属性:
- _options: 存储参数
- _committing: 提交标志,保证Vuex对state的修改只能在mutation的回调中
- _actions:存储actions
- _mutations:存储mutations
- _wrappedGetters:存储getters
- _runtimeModules:存储运行时modules
- _subscribers:存储mutation的订阅者
- _watcherVM:一个Vue实例,用来利用Vue的$watch机制观测变化
接下来更改dispatch和commit方法的this为当前store实例。
再判断是否为严格模式,严格模式下只允许使用mutation更新state。
初始化module
单一状态树的一个问题就是当应用规模扩大时,store对象会大到难以管理。module就是为了解决这个问题,其将单一状态树从上到下切分到各个模块管理。 installModule方法就是该功能的初始化。
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const {
state,
actions,
mutations,
getters,
modules
} = 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, state || {})
})
}
if (mutations) {
Object.keys(mutations).forEach(key => {
registerMutation(store, key, mutations[key], path)
})
}
if (actions) {
Object.keys(actions).forEach(key => {
registerAction(store, key, actions[key], path)
})
}
if (getters) {
wrapGetters(store, getters, path)
}
if (modules) {
Object.keys(modules).forEach(key => {
installModule(store, rootState, path.concat(key), modules[key], hot)
})
}
}
该方法接受如下5个参数:
- store: 当前Store实例
- rootState:根state
- path: 当前嵌套模块的路径数组
- module:当前安装的模块的options
- hot: 热更新标志
module的state初始化
首先根据path数组长度判断是否为根,然后拿到options内的相关参数。
然后分别对mutations,actions,getters进行注册,再遍历modules递归调用installModule继续安装嵌套模块。
当我们不设置子模块时,完成以上的注册其实modules初始化就结束了。但当我们设置子模块时,需要重点关注以上的这段代码:
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, state || {})
})
}
首先会调用getNestedState方法根据path查找state上的子state相对于根state的路径,然后把子state添加到根state中。这里我们通过_withCommit函数去修改当前模块的state,Vuex中对state的修改都会用该函数包装保证状态的正确性。
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
到此就完成了所有模块的state嵌套初始化。
registerMutation
function registerMutation (store, type, handler, path = []) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler(getNestedState(store.state, path), payload)
})
}
该函数接受4个参数:
- store:当前Store实例
- type:mutation的key
- handler:mutation的回调
- path:当前模块路径
其将注册的mutations回调包装一层后,挂载到store的_mutations内部属性上。
该包装函数的作用就是接受定义mutation时传入的payload参数,并将当前模块的state和payload一起作为回调的参数。
registerAction
function registerAction (store, type, handler, path = []) {
const entry = store._actions[type] || (store._actions[type] = [])
const { dispatch, commit } = store
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler({
dispatch,
commit,
getters: store.getters,
state: getNestedState(store.state, path),
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的注册和mutation大致相同,关键区别在于mutation是同步修改state,而action可以实现异步的修改。但有一点需要注意,Vuex中mutation是修改state的唯一途径,action异步修改的本质是修改提交mutation的时机。
其接受参数为context对象和payload,cb并没有被用到。context对象包括dispatch,commit,getters,模块state和根state。
既然包含了dispatch,就说明action内组合action调用也是允许的。
最后对返回值做一层Promise包装以及开发工具钩子检测。
wrapGetters
function wrapGetters (store, moduleGetters, modulePath) {
Object.keys(moduleGetters).forEach(getterKey => {
const rawGetter = moduleGetters[getterKey]
if (store._wrappedGetters[getterKey]) {
console.error(`[vuex] duplicate getter key: ${getterKey}`)
return
}
store._wrappedGetters[getterKey] = function wrappedGetter (store) {
return rawGetter(
getNestedState(store.state, modulePath), // local state
store.getters, // getters
store.state // root state
)
}
})
}
getters的初始化即是遍历所有getters并挂载到store的_wrappedGetters属性中,并将其包装一层传入当前模块state,其他getter和根State作为参数。
至此module的初始化就完成了。
resetStoreVM
function resetStoreVM (store, state) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
Object.keys(wrappedGetters).forEach(key => {
const fn = wrappedGetters[key]
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})
// 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 },
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation.
store._withCommit(() => {
oldVm.state = null
})
Vue.nextTick(() => oldVm.$destroy())
}
}
我们虽然初始化了各种参数,但Vuex还有一个关键功能在于视图的响应式更新。这部分的实现原理和Vue的响应式原理相同,关键在于如何实现两者的桥接。也就是这个方法做的事了。
首先保留现有的_vm对象。这个对象是个Vue实例,用来实现Vuex数据和组件数据的中转。
接着遍历_wrappedGetters对象拿到所有包装函数并将其执行结果用computed变量保存,再通过Object.defineProperty方法使我们访问$store.getters时访问vm对象上的getters,即完成了中转的入口实现。
再将store的state作为data和computed变量作为Vue对象的属性实例化并挂载到store的_vm属性上,即完成了中转的出口实现。至此核心部分就完成了。
接下来就是判断是否开启严格模式,如果是严格模式则检测_vm.state的变化是否来自mutation,依据为内部commiting属性的值。
最后即是清理每次调用该函数时都会创建的旧vm对象了。
小结
以上就是Vuex的结构初始化全部核心内容。关键有两点:
- installModule上对mutations,actions,getters注册和模块的嵌套注册。
- resetStoreVM上的响应式数据中转。
-- EOF --