Vuex 源码解析
ZengTianShengZ opened this issue · comments
Vuex 源码解析
本篇意在理清楚 Vuex 的工作原理,相应的会忽略一些技术细节,更多源码请移步到 github vux
1、目录结构
Vuex
的源码其实并不多,短小精悍,主要的工作都在 store.js
下完成
2、入口文件 index.js
分析源码先从入口文件入手,理清来龙去脉。
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
入口文件有整个状态管理的 Store
对象,安装 Vue.js 插件必须提供的 install
方法,版本号以及辅助函数 mapState
、mapMutations
、mapGetters
、 mapActions
。
3、Vuex 插件的安装 install
Vuex
的使用要从插件的安装说起,安装 Vuex 插件做了哪些事情呢。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex) // 会调用 Vuex 提供的 install 方法
安装 Vuex 插件,插件提供的 install
方法会被调用,方法如下:
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)
}
可看出插件主要执行了 applyMixin(Vue)
,如下:
// applyMixin
export default function (Vue) {
Vue.mixin({ beforeCreate: vuexInit })
/**
* 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.mixin
,订阅了 beforeCreate
钩子,beforeCreate
则会在 Vue 初始化前调用执行。beforeCreate
对应执行了 vuexInit
, 如下:
this.$store = options.store
this.$store = options.parent.$store
即为当前 Vue 实例(this)的 $store 属性挂上 store 实例。如果当前Vue 实例没有 store 实例则从父组件的 Vue 实例继承过来。
这样做是干嘛用的呢,是为了我们在任何一个 Vue 组件或子孙组件中都可以方便的使用
this.$store
去对 store 做任何操作
4、 Store 对象初始化
了解了 store 如何挂载到 vue 实例,下面来看下 store 做了哪些操作
export class Store {
constructor (options = {}) {
const {
plugins = [],
strict = false
} = options
/****** _committing 严格模式下的标志位 ******/
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
/****** module收集器 ******/
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
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
get state () {
return this._vm._data.$$state
}
set state (v) {
if (process.env.NODE_ENV !== 'production') {
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
commit (_type, _payload, _options) {
// ...
}
dispatch (_type, _payload) {
// ...
}
}
从 Store 的构造函数我们可以看到几个熟悉的属性和方法,state
, _mutations
, _actions
, this.dispatch
, this.commit
,我们先抛开其他细节,来简单的回顾下我们是怎么使用 Vuex 的:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
store.commit('increment')
console.log(store.state.count) // -> 1
我们往 Store
传入一个 option
对象(包含 state,mutations属性 ),接着我们可以使用 store
实例 进行 store.commit
更新 state
,store.state
获取 state
。store.commit('increment')
到底做了什么事呢,下面我们主要分析下 store.commit
5、store.commit
更多的技术细节我们需要通过分析 store.commit 到底做了哪些操作:
// store.commit
commit (_type, _payload, _options) {
// check object-style commit
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
const entry = this._mutations[type] // ①
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => { // ②
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
this._subscribers.forEach(sub => sub(mutation, this.state))
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
commit
方法接收一些参数,比如我们例子中传入的 type
,根据 type
获取到可执行的数组 entry
:
const entry = this._mutations[type] // ①
最后数组 entry
在 _withCommit
的包裹下遍历执行
this._withCommit(() => { // ②
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
分析到这里那么问题就拓展到了 store.commit('increment')
是怎么根据 type: increment
获取到 entry
的,执行完 entry
又是怎么更新了 state
的。
5.1、 module 的收集
要分析上面的问题我们还需再重新回到上面提到的 Store 的构造方法,构造方法有这么一步操作
installModule(this, state, [], this._modules.root)
其中:
function installModule (store, rootState, path, module, hot) {
const local = module.context = makeLocalContext(store, namespace, path)
// module forEachMutation 方法能找到自己对应的 mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
// 注册 mutation
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
// 注册 action
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
// 注册 getter
registerGetter(store, namespacedType, getter, local)
})
// 递归注册 module.module.module
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
可见 installModule
里头做了一些注册相关的初始化操作,如 registerMutation
,还有后续我们会提到的 registerAction
,registerGetter
。
关键先来看下 registerMutation
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)
})
}
到这里应该就比较清晰整个 store.commit('increment')
的流程了。
// store
store.commit('increment')
==>
// commit
const entry = this._mutations[type]
==>
// installModule
registerMutation(store, namespacedType, mutation, local)
==>
// registerMutation
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
store.commit('increment')
需要通过 type
去 _mutations
拿到可执行的 entry
遍历执行,_mutations
是通过 registerMutation
注册初始化的,_mutations
根据 type
初始化为为数组 store._mutations[type] = []
, 再往数组中 push
进去 mutation
, mutation
其实就是最最开始 往 Store
传入的初始化 options
配置了
简单梳理下数据结构:
// options
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
},
}
Store 实例对象
mutations
下的方法会被收集到 Store
的 _mutations
对象上,根据对应的 type
构造出 entry
数组,当
store.commit('increment')
时就会根据 type
找到 _mutations
对象对应的 _mutations
数组,对应执行。
5.2、 更新 state
上面只分析了 store.commit('increment')
执行的过程,漏掉了最重要的一步,commit
完最后是怎么更新 state
的呢。
还记得我们有提到的 _withCommit
方法吗:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn() // 方法内部更新了 state
this._committing = committing
}
执行 _withCommit
方法会遍历执行 entry
方法。
this._withCommit(() => { // ②
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
最终是执行了这个 handler(payload)
,handler
对应的就是处理 options
对应的 mutation
方法了,如例子中的
mutations: {
increment (state) { // 处理为 handler 方法
state.count++ // 更新了 state
}
}
最终的 state
更新就是在 mutations
对应 type
函数执行了更新。
至此我们完成了 store.commit('increment')
执行过程的分析
6、 state 状态的获取
store.state.count // -> 1
源码:
get state () {
return this._vm._data.$$state
}
set state (v) {
if (process.env.NODE_ENV !== 'production') { // 禁止直接更新 state 状态
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
state
的获取也很简单,从 Store
的 _vm
返回 _data
状态就是了,但禁止了直接更新 state
的状态,原因是稍后提到的 Vuex
的状态跟踪。
这里还有一个不明白的点是为什么 state
的获取是从 _vm
属性来的呢,原来 Vuex
初始化的时候有这么一步
resetStoreVM(store, state, hot)
其中:
function resetStoreVM (store, state, hot) {
// ...
// Vuex依赖Vue核心实现数据的“响应式化”。
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// ...
}
Store
将 state
挂载到了一个 Vue
实例上,利用 Vue
的双向绑定的原理来实现 Store
state
的更新从而触发视图的更新。
7、store.dispatch
dispatch
和 commit
的原理差不多,如果你通过上面理解了 commit
的过程,那分析 dispatch
的过程就轻松多了。
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
return res
})
}
同样的,根据 type
获取到对应 _actions
属性下的 entry
,不过和 commit
不同的是,这里获取到的 entry
放在了 Promise
下执行,这也就是 store.dispatch
触发的 action
能执行异步函数的原因。
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return result.then(res => {})
store.dispatch
触发了 action
,action
更新 state
需要通过 commit
,具体过程就上面分析 Store.commit
的过程了,不在赘述。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
8、状态跟踪
在 Store
初始化时还有这么一段代码:
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
其中:
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)
})
}
devtoolPlugin
干嘛用的呢,它做了个状态的订阅,每当状态更新时会触发 store.subscribe
,从而做一次 vuex:mutation
的 emit
,配合控制台我们就能方便的跟踪到每次状态更新的数据情况了。
store.subscribe((mutation, state) => { // 状态订阅
devtoolHook.emit('vuex:mutation', mutation, state)
})
利用控制台还可做 时间旅行
,回退到任意时间节点的状态。
小结
我们从源码入手,分析了 Vuex
插件的安装过程,store
的 commit
过程,如何获取 store
的 state
,以及 store
的 action
过程,最后我们提了下 Vuex
的 Vuex
配合控制台可方便做状态管理。中间隐藏了一些技术细节,想深入研究的同学可查看源码继续分析。