作为Vue全家桶之一的vuex,在Vue框架中起着很重要的角色。Vuex源码篇幅虽然不多,但解读起来还是需要下一些功夫的。下面我们就参照vuex官方指南文档内容进行分析。
vuex是什么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
解决了什么问题?
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
如果只是简单的单页应用,最好不要使用 Vuex,使用 Vuex 可能是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果构建一个中大型单页应用,需要考虑如何更好地在组件外部管理状态,Vuex 将会成为最佳选择。
Vuex设计思想是什么?
Vuex是一个专门为Vue.js框架设计的、用于对Vue.js应用程序进行状态管理的库,它借鉴了Flux、redux的基本思想,将共享的数据抽离到全局,以一个单例存放,同时利用Vue.js的响应式机制来进行高效的状态管理与更新。正是因为Vuex使用了Vue.js内部的“响应式机制”,所以Vuex是一个专门为Vue.js设计并与之高度契合的框架(优点是更加简洁高效,缺点是只能跟Vue.js搭配使用)。
vuex状态自管理应用包含以下几个部分:
state:驱动应用的数据源;
view:以声明方式将 state 映射到视图;
actions:响应在 view 上的用户输入导致的状态变化。
来看一下“单向数据流”理念的简单示意图:
Vuex的完整数据流程图
Vuex最佳实践演示 - 一个小实例:
vuex在项目中应用的目录,在src下创建store文件:
├── store
│ ├── modules
│ ├── todos.js
│ ├── getter.js
│ ├── index.js
├── APP.vue
└── main.js
index.js
//index.js
import Vue from 'vue';
import Vuex from 'vuex';
import getters from './getters'
import app from './modules/todos'
//Load Vuex
Vue.use(Vuex)
//create store
export default new Vuex.Store({
modules: { app },
getters
})
getters.js
//getters.js
const getters = {
allTodos: (state) => state.app.todos,
red: (state) => state.app.red,
blue: (state) => state.app.blue
}
export default getters
todos.js
// todos.js
const app = {
state: {
todos: [{
id: 1,
title: 'one'
},{
id: 2,
title: 'two'
}],
red: 0,
blue: 0
},
mutations: {
ADD_COUNT: (state) => {
state.red++,
state.blue++
},
MINUS_COUNT: (state)=> {
state.red--,
state.blue--
}
},
actions: {
addCounts: ({commit})=>{
commit('ADD_COUNT')
},
minusCounts: ({commit})=>{
commit('MINUS_COUNT')
}
}
}
export default app
main.js
//main.js
import store from './store'
new Vue({
store,
render: h => h(App),
}).$mount('#app')
APP.vue
import { mapGetters } from 'vuex'
......
export default {
name: 'app',
computed: mapGetters(['allTodos'])
}
子组件应用:
//组件应用
computed:{
red(){
return this.$store.state.app.red
},
blue(){
return this.$store.state.app.blue
}
},
methods: {
addOne() {
this.$store.dispatch('addCounts')
},
minusOne() {
this.$store.dispatch('minusCounts')
},
}
Vuex核心源码分析:
先看下vuex官方指南文档,再结合上面的小实例对核心源码进行分析。
vuex官网指南:vuex.vuejs.org/zh/guide/
vuex源码:github.com/vuejs/vuex
git上下载的源码文件很多,但是核心执行源码存在/src里,源码文件不多,目录划分也很清晰,每个文件都有着各自的功能:
- module:提供 module 对象与 module 对象树的创建功能;
- plugins:提供开发辅助插件,如 “时光穿梭” 功能,state 修改的日志记录功能等;
- helpers.js:提供 action、mutations 以及 getters 的查找 API;
- index.js:是源码主入口文件,提供 store 的各 module 构建安装;
- mixin.js:提供了 store 在 Vue 实例上的装载注入;
- store.js:实现Store构造方法;
- util.js:提供了工具方法如 find、deepCopy、forEachValue 以及 assert 等方法。
Vuex源码目录结构
├── module
│ ├── module-collection.js
│ ├── module.js
├── plugins // 插件
│ ├── devtool.js
│ ├── logger.js
├── helpers.js //辅助函数
├── index.esm.js
├── index.js //入口文件
├── mixin.js
├── store.js
└── util.js
初始化装载与注入
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
}
constructor - 构造方法
在实例的store文件夹的index.js里通过new store来注册store,下面是具体Store构造方法,可以结合代码注释看:
constructor (options = {}) {
......
// store internal state
/* 用来判断严格模式下是否是用mutation修改state的 */
this._committing = false
/* 存放action */
this._actions = Object.create(null)
this._actionSubscribers = []
/* 存放mutation */
this._mutations = Object.create(null)
/* 存放getter */
this._wrappedGetters = Object.create(null)
/* module收集器 */
this._modules = new ModuleCollection(options)
/* 根据namespace存放module */
this._modulesNamespaceMap = Object.create(null)
/* 存放订阅者 */
this._subscribers = []
/* 用以实现Watch的Vue实例 */
this._watcherVM = new Vue()
// bind commit and dispatch to self
/*将dispatch与commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时的this会指向组件的vm*/
const store = this
const { dispatch, commit } = this
/* 为dispatch与commit绑定this(Store实例本身) */
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
/*严格模式(使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误)*/
this.strict = strict
const state = this._modules.root.state
/*初始化根module,这也同时递归注册了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才独有保存的Module对象*/
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
resetStoreVM(this, state)
// apply plugins 应用插件
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
dispatch - 分发Action
在实例的子组件应用里会用到dispatch,dispatch的功能是触发并传递一些参数(payload)给对应 type 的 action。在dispatch 中,先进行参数的适配处理,然后判断 action type 是否存在,若存在就逐个执行。
/* 调用action的dispatch方法 */
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
/* actions中取出type对应的ation */
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
/* 是数组则包装Promise形成一个新的Promise,只有一个则直接返回第0个 */
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
})
}
commit - mutation事件提交
commit方法和dispatch相比虽然都是触发type,但是对应的处理却相对复杂。先进行参数适配,判断触发mutation type,利用_withCommit方法执行本次批量触发mutation处理函数,并传入payload参数。执行完成后,通知所有_subscribers(订阅函数)本次操作的mutation对象以及当前的 state 状态,如果传入了已经移除的 silent 选项则进行提示警告。可以看实例里module的actions的属性。
/* 调用mutation的commit方法 */
commit (_type, _payload, _options) {
// check object-style commit 校验参数
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
/* 取出type对应的mutation的方法 */
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
/* 执行mutation中的所有方法 */
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'
)
}
}
state 方法
在实例module里state有常量和变量,可以结合源码来看状态的改变原理。_withCommit是一个代理方法,所有触发mutation的进行state修改的操作都经过它,由此来统一管理监控state状态的修改。缓存执行时的 committing 状态将当前状态设置为 true 后进行本次提交操作,待操作完毕后,将 committing 状态还原为之前的状态。
//保存之前的提交状态
_withCommit (fn) {
/* 调用withCommit修改state的值时会将store的committing值置为true,
内部会有断言检查该值,在严格模式下只允许使用mutation来修改store中的值,
而不允许直接修改store的数值 */
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
}
Module - 模块管理
installModule方法 - 遍历注册mutation、action、getter、安装module
在实例的module里用到了state、mutation、action,Store的构造类除了初始化一些内部变量以外,主要执行了installModule(初始化module)以及resetStoreVM(通过VM使store“响应式”)。 installModule的作用主要是用为module加上namespace名字空间(如果有)后,注册mutation、action以及getter,同时递归安装所有子module。最后执行plugin的植入。
/*初始化module*/
function installModule (store, rootState, path, module, hot) {
/* 是否是根module */
const isRoot = !path.length
/* 获取module的namespace */
const namespace = store._modules.getNamespace(path)
// register in namespace map
/* 如果有namespace则在_modulesNamespaceMap中注册 */
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
/* 获取父级的state */
const parentState = getNestedState(rootState, path.slice(0, -1))
/* module的name */
const moduleName = path[path.length - 1]
store._withCommit(() => {
/* 将子module设置成响应式的 */
Vue.set(parentState, moduleName, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
/* 遍历注册mutation */
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
/* 遍历注册action */
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
/* 遍历注册getter */
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
/* 递归安装mudule */
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
registerModule - 注册模块
registerModule用以注册一个动态模块,也就是在store创建以后再注册模块的时候用该接口。
/* 注册一个动态module,当业务进行异步加载的时候,可以通过该接口进行注册动态module */
registerModule (path, rawModule, options = {}) {
/* 转化成Array */
if (typeof path === 'string') path = [path]
if (process.env.NODE_ENV !== 'production') {
assert(Array.isArray(path), `module path must be a string or an Array.`)
assert(path.length > 0, 'cannot register the root module by using registerModule.')
}
/*注册*/
this._modules.register(path, rawModule)
/*初始化module*/
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
resetStoreVM(this, this.state)
}
registerModule - 注销模块
同样,与registerModule对应的方法unregisterModule,动态注销模块。实现方法是先从state中删除模块,然后用resetStore来重制store
/* 注销一个动态module */
unregisterModule (path) {
/* 转化成Array */
if (typeof path === 'string') path = [path]
if (process.env.NODE_ENV !== 'production') {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}
/*注销*/
this._modules.unregister(path)
this._withCommit(() => {
/* 获取父级的state */
const parentState = getNestedState(this.state, path.slice(0, -1))
/* 从父级中删除 */
Vue.delete(parentState, path[path.length - 1])
})
/* 重制store */
resetStore(this)
}
getters - 获取数据状态
可以看到实例将getters做成单独的一个getters.js文件进行getter状态管理。
resetStoreVM - 绑定getter
执行完各 module 的 install 后,执行 resetStoreVM 方法,进行 store 组件的初始化,resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法为每一个getter绑定上get方法:
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
function resetStoreVM (store, state, hot) {
/* 存放之前的vm对象 */
const oldVm = store._vm
// bind store public getters(computed)
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
/* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
const silent = Vue.config.silent
/* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
Vue.config.silent = true
/* 这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// 恢复Vue的模式
Vue.config.silent = silent
// enable strict mode for new vm
/* 使用严格模式,保证修改store只能通过mutation */
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
/* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
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())
}
}
注册getter
//注册getter
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
辅助函数 - mapState, mapMutations, mapGetters, mapActions
实例APP.vue里用到了mapGetters,通过computed计算属性获取state状态。当一个组件需要获取多个状态、多个getter、多个mutations方法、多个action分发事件时候,这些辅助函数帮助我们生成计算属性,这四个辅助函数具体实现方法在helpers.js里:
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
......
插件
Vuex源码里提供了两个插件源码:devtool.js和logger.js。如果已经安装了Vue.js devtools,则会在windows对象上暴露一个VUE_DEVTOOLS_GLOBAL_HOOK。devtoolHook用在初始化的时候会触发“vuex:init”事件通知插件,然后通过on方法监听“vuex:travel-to-state”事件来重置state。最后通过Store的subscribe方法来添加一个订阅者,在触发commit方法修改mutation数据以后,该订阅者会被通知,从而触发“vuex:mutation”事件:
/* 从window对象的__VUE_DEVTOOLS_GLOBAL_HOOK__中获取devtool插件 */
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
/* devtool插件实例存储在store的_devtoolHook上 */
store._devtoolHook = devtoolHook
// 触发Vuex组件初始化的hook,并将store的引用地址传给devtool插件,使插件获取store的实例
devtoolHook.emit('vuex:init', store)
// 提供“时空穿梭”功能,即state操作的前进和倒退
devtoolHook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})
// 订阅store的变化, mutation被执行时,触发hook,并提供被触发的mutation函数和当前的state状态
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
})
}
总结
Vuex是一个非常优秀的库,代码简洁,逻辑清晰。源码中还有一些工具函数类似hotUpdate、watch 以及 subscribe等,感兴趣的小伙伴们有时间可以好好研究一下源码,了解了源码设计思想和实现逻辑后,会豁然开朗,应用起来也会游刃有余。