當我們用vue在開發的過程中,經常會遇到以下問題
多個vue組件共享狀態
Vue組件間的通訊
在項目不復雜的時候,我們會利用全局事件bus的方式解決,但隨着復雜度的提升,用這種方式將會使得代碼難以維護,因此vue官網推薦了一種更好用的解決方案Vuex。
Vuex是什么
Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。說直白一點,vuex就是把組件的共享狀態抽取出來,以一個全局單例模式管理的數據倉庫,里面提供了對應用的數據存儲和對數據的操作,維護整個應用的公共數據。
看源代碼之前,我們看一下vuex的使用方法
/***code例子*****/
/**
* store.js文件
* 創建store對象,配置state、action、mutation以及getter
*
**/
import Vue from 'vue'
import Vuex from 'vuex'
//注冊插件
Vue.use(Vuex)
//創建store倉庫
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
/**
* vue-index.js文件
*
*
**/
import Vue from 'vue'
import App from './../pages/app.vue'
import store from './store.js'
//將store掛載到根組件
new Vue({
el: '#root',
router,
store,
render: h => h(App)
})
//app.vue
export default {
computed: {
},
methods: {
increment (obj) {
//組件中調用increment方法
this.$store.dispatch('increment', obj)
}
}
}
整個vuex的使用步驟如下:
1、安裝vuex插件
2、創建store狀態管理倉庫
3、將store注入到vue組件
4、使用dispatch調用action方法
框架核心流程
在分析源代碼之前,我們先來看一下整個vuex的運行流程,下面這張圖是官方文檔中提供的核心思想圖

Vue Components:Vue組件。HTML頁面上,負責接收用戶操作等交互行為,執行dispatch方法觸發對應action進行回應。
dispatch:操作行為觸發方法,是唯一能執行action的方法。
actions:操作行為處理模塊。負責處理Vue Components接收到的所有交互行為。包含同步/異步操作,支持多個同名方法,按照注冊的順序依次觸發。向后台API請求的操作就在這個模塊中進行,包括觸發其他action以及提交mutation的操作。
commit:狀態改變提交操作方法。對mutation進行提交,是唯一能執行mutation的方法。
mutations:狀態改變操作方法。是Vuex修改state的唯一推薦方法
state:頁面狀態管理容器對象。集中存儲Vue components中data對象的零散數據,全局唯一,以進行統一的狀態管理。頁面顯示所需的數據從該對象中進行讀取,利用Vue的細粒度數據響應機制來進行高效的狀態更新。
整體流程:
用戶在組件上的交互觸發的action,action通過調用(commit)mutations的方法去改變state的狀態,而state由於加入了vue的數據響應機制,會導致對應的vue組件更新視圖,完成整個狀態更新。
目錄結構
源代碼分析前,首先我們看一下目錄結構

module
module-collection.js:創建模塊樹
Module.js:創建模塊
plugins
devtool.js:開發環境工具插件,針對chorme安裝 vue-tool 插件
logger.js:state狀態改變日志查詢
helpers.js:提供mapSate,mapMutations,mapActions,mapGetters 等操作api函數
index.js
index.exm.js:都是入口文件
mixin.js:混入方法,提供將store 倉庫掛載到每個vue實例的方法
store.js:整個代碼核心,創建store對象
util.js:工具類
Vuex的安裝
Vue 通過Vue.use(Vuex)安裝vuex, use通過調用vuex對象的install方法將vuex載入,我們看一下install方法的實現:
let Vue // 安裝的時候增加vue對象,避免引入vue包
//安裝插件
export function install(_Vue) {
//Vue已經被初始化,說明已經安裝過,給出提示
if (Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
//安裝vuex
applyMixin(Vue)
}
// 自動安裝模塊
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
這里用了一個比較巧的方法,vue作為參數傳入vuex,所以我們不用通過導入vue,代碼打包的時候就不會將vue包含進來。
來看下applyMixin方法內部代碼
const version = Number(Vue.version.split('.')[0])
//2.0以上用混入
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
//重寫init方法
const _init = Vue.prototype._init
Vue.prototype._init = function(options = {}) {
options.init = options.init ? [vuexInit].concat(options.init) :
vuexInit
_init.call(this, options)
}
}
function vuexInit() {
const options = this.$options
// store 注入
if (options.store) { //根組件
this.$store = typeof options.store === 'function' ?
options.store() : //模塊重用
options.store
} else if (options.parent && options.parent.$store) { //其他非根組件注入,向上傳遞$store,保證任何組件都可以訪問到
this.$store = options.parent.$store
}
}
根據不同版本,2.x.x以上版本,使用 hook 的形式進行注入,或使用封裝並替換Vue對象原型的_init方法,實現注入。
將初始化Vue根組件時傳入的store設置到this對象的$store屬性上,子組件從其父組件引用$store屬性,層層嵌套進行設置。在任意組件中執行 this.$store 都能找到裝載的那個store對象
接下來我們來看狀態管理庫的創建
我們從構造函數一步一步分析
ModuleCollection構建
if (process.env.NODE_ENV !== 'production') {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) //沒有vue對象說明沒有注入
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 = [], //這個選項暴露出每次 mutation 的鈎子。Vuex 插件就是一個函數,它接收 store 作為唯一參數
strict = false //嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函數引起的,將會拋出錯誤
} = options
let {
state = {} //狀態
} = options
if (typeof state === 'function') {
state = state() //狀態復用,使用一個函數來聲明模塊狀態
}
// store internal state
this._committing = false //是否在提交狀態
this._actions = Object.create(null) //action對象存儲
this._mutations = Object.create(null) //mutation對象存儲
this._wrappedGetters = Object.create(null) //裝后的getters集合對象
this._modules = new ModuleCollection(options) //封裝module集合對象
this._modulesNamespaceMap = Object.create(null) //創建命名空間
this._subscribers = []
this._watcherVM = new Vue() //vue實例,Vue組件用於watch監視變化,相當於bus
首先對執行環境做判斷,vuex必須已經安裝並且支持Promise語法,接下來對輸入的參數進行簡單的處理以及創建一些屬性,用於后續操作,其中有一步操作this._modules = new ModuleCollection(options),對傳入的options進行處理,生成模塊集合。來看一下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)
}
// 注冊嵌套的模塊
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
//this.register(['cart',{cartmodules},runtime])
})
}
}
ModuleCollection主要將傳入的options對象整個構造為一個module對象,並循環調用 this.register([key], rawModule, false) 為其中的modules屬性進行模塊注冊,使其都成為module對象,最后options對象被構造成一個完整的狀態樹。ModuleCollection類還提供了modules的更替功能
modules屬性中每一個模塊都是一個模塊對象,而傳入的option會被當成是整個狀態樹的根模塊。
我們接下來看一下了解一下module對象
constructor(rawModule, runtime) {
this.runtime = runtime //運行時間
this._children = Object.create(null) //children子模塊
this._rawModule = rawModule //模塊內容
const rawState = rawModule.state //模塊狀態
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} //state
}
對象是對傳入的每一個module參數進行封裝,添加了存儲的子模塊,已達到狀態樹的構建目的,同時提供一些基本操作的方法。
我們來看一下官網提供的購物車的例子構建完的moduleCollection對象

這個利用樹形結構,清晰地表達出了整個數據倉庫數據的關系,moduleCollection將成為后期數據和操作重新組織和查找基礎。
dispatch和commit函數設置
回到store的構造函數
const store = this
const { dispatch, commit } = this
//dispatch 方法
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
//commit 方法
this.commit = function boundCommit(type, payload, options) {
return commit.call(store, type, payload, options)
}
封裝dispatch 和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] //變化有沒有注冊這個類型 commit導致mutation
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
//處理commit
this._withCommit(() => {
entry.forEach(function commitIterator(handler) {
handler(payload) //mutation操作
})
})
// 訂閱者函數遍歷執行,傳入當前的mutation對象和當前的state
this._subscribers.forEach(sub => sub(mutation, this.state))
//新版本silent去掉了
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'
)
}
}
//觸發action
dispatch(_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const entry = this._actions[type] //觸發action配置表
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
return entry.length > 1 ?
Promise.all(entry.map(handler => handler(payload))) :
entry[0](payload)
}
dispatch的功能是觸發並傳遞一些參數(payload)給對應type的action。Dispatch提供了兩種方法的調用:
所以使用了unifyObjectStyle對傳入的參數做了統一化處理。接着找到對應type的action,並逐個執行。

commit方法和dispatch方法大同小異,但是比dispatch復雜一點。每次調用都要對訂閱者遍歷執行,這個操作的原因后面再解析。同時每次執行mutation方法集合時,都要調用函數_withCommit方法。
_withCommit是一個代理方法,所有觸發mutation的進行state修改的操作都經過它,由此來統一管理監控state狀態的修改。實現代碼如下。
_withCommit(fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
每次提交前保存當前commit狀態,然后置為true后進行本次操作,操作完成后再還原本次修改之前的狀態。我們上面說過mutation是Vuex修改state的唯一推薦方法。這樣設置的作用有利於狀態的跟蹤,state的修改來自於mutation。那是如何跟蹤的呢?我們看下面一段代碼:
function enableStrictMode(store) {
store._vm.$watch(function() { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
//如果不是通過commit函數,提示
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
這里通過vue $watch 方法監控每一次data的變化,如果在開發環境並且committing為false的話,則表明不是通過mutation操作狀態變化,提示非法。同時這也解釋了官網要求mutation必須為同步操作state。因為如果異步的話,異步函數調用完成后committing已經還原狀態,而回調函數還沒調用,$watch還沒有接收到data的變化,當回調成功時,committing已經被置為false,不利於狀態的跟蹤。
模塊的安裝
接下來,構造函數進行模塊的安裝
// init root module. 初始化根模塊 // this also recursively registers all sub-modules 遞歸所有子模塊 // and collects all module getters inside this._wrappedGetters 把所有的getters對象都收集到Getter里面 installModule(this, state, [], this._modules.root)
我們看一下installModule的具體內容
function installModule(store, rootState, path, module, hot) {
const isRoot = !path.length //是否為根元素
const namespace = store._modules.getNamespace(path) //命名空間
// register in namespace map 如果是命名空間模塊,推送到map對象里面
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// set state
//將模塊的state添加到state鏈中,是的可以按照 state.moduleName 訪問
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
//確保屬性被創建后也是響應式的
Vue.set(parentState, moduleName, module.state)
})
}
//模塊上下文
/*local {
dispatch,
commit,
getters,
state
}
*/
const local = module.context = makeLocalContext(store, namespace, path)
// 注冊對應模塊的mutation,供state修改使用
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 注冊對應模塊的action,供數據操作、提交mutation等異步操作使用
module.forEachAction((action, key) => {
const namespacedType = namespace + key
registerAction(store, namespacedType, action, local)
})
// 注冊對應模塊的getters,供state讀取使用
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
}
判斷是否是根目錄,以及是否設置了命名空間,若存在則在namespace中進行module的存儲,在不是根組件且不是 hot 條件的情況下,通過getNestedState方法拿到該module父級的state,拿到其所在moduleName,調用 Vue.set(parentState, moduleName, module.state) 方法將其state設置到父級state對象的moduleName屬性中,由此實現該模塊的state注冊。這里建立了一條可觀測的data數據鏈。
接下來定義local變量和module.context的值,執行makeLocalContext方法,為該module設置局部的 dispatch、commit方法以及getters和state,為的是在局部的模塊內調用模塊定義的action和mutation。也就是官網提供的這種調用方式

定義local環境后,循環注冊我們在options中配置的action以及mutation等,這種注冊操作都是大同小異,我們來看一下注冊mutation的步驟:
/***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)
})
}
type作為id,把對應的值函數封裝后存儲在數組里面,然后作為store._mutations的屬性。store._mutations收集了我們傳入的所有mutation函數
action和getter的注冊也是同理的,只是action handler比mutation handler以及getter wrapper多拿到dispatch和commit操作方法,因此action可以進行dispatch action和commit mutation操作。
注冊完了根組件的actions、mutations以及getters后,遞歸調用自身,為子組件注冊其state,actions、mutations以及getters等。
/***子模塊代碼****/
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
/***子模塊代碼end****/
Store._vm組件安裝
執行完各module的install后,執行resetStoreVM方法,進行store組件的初始化。
resetStoreVM(this, state)
function resetStoreVM(store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
// 循環所有處理過的getters,並新建computed對象進行存儲,通過Object.defineProperty方法為getters對象建立屬性,
//使得我們通過this.$store.getters.xxxgetter能夠訪問到該getters
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => 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
// 設置新的storeVm,將當前初始化的state以及getters作為computed屬性(剛剛遍歷生成的)
store._vm = new Vue({
data: {
$$state: state //相當於總線
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
// 該方法對state執行$watch以禁止從mutation外部修改state
if (store.strict) {
enableStrictMode(store)
}
// 若不是初始化過程執行的該方法,將舊的組件state設置為null,
//強制更新所有監聽者(watchers),待更新生效,DOM更新完成后,執行vm組件的destroy方法進行銷毀,減少內存的占用
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())
}
}
resetStoreVm方法創建了當前store實例的_vm組件,至此store就創建完畢了
plugin注入
最后執行plugin插件
//打印變化前后的值
plugins.forEach(plugin => plugin(this))
//如果配置了開發者工具
if (Vue.config.devtools) {
devtoolPlugin(this)
}
Plugins.foreach 默認執行了createlogger方法,我們來看createLogger的實現代碼:
export default function createLogger({
collapsed = true,
filter = (mutation, stateBefore, stateAfter) => true,
transformer = state => state,
mutationTransformer = mut => mut
} = {}) {
return store => {
let prevState = deepCopy(store.state)
store.subscribe((mutation, state) => {
if (typeof console === 'undefined') {
return
}
const nextState = deepCopy(state)
if (filter(mutation, prevState, nextState)) {
const time = new Date()
const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
const formattedMutation = mutationTransformer(mutation)
const message = `mutation ${mutation.type}${formattedTime}`
const startMessage = collapsed ?
console.groupCollapsed :
console.group
// render
try {
startMessage.call(console, message)
} catch (e) {
console.log(message)
}
console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))
try {
console.groupEnd()
} catch (e) {
console.log('—— log end ——')
}
}
prevState = nextState
})
}
}
createLogger通過store.subscribe將方法添加到訂閱,待觸發的時候調用。這里通過前后state狀態的打印對比,跟蹤狀態變更。
devtoolPlugin則是對vue-tool控制台信息的添加監控
mapSate,mapMutations,mapActions,mapGetters函數實現
Vuex還提供了mapSate,mapMutations,mapActions,mapGetters輔助函數來幫助我們生成計算屬性和對應方法。下面用mapMutations為例來分析一下實現過程
首先看調用的方式:
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 將 `this.increment()` 映射為 `this.$store.commit('increment')`
// `mapMutations` 也支持載荷:
'incrementBy' // 將 `this.incrementBy(amount)` 映射為 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 將 `this.add()` 映射為 `this.$store.commit('increment')`
}),
...mapMutations('some/nested/module', [ //在模塊some/nested/module查看
'foo', //將 `this.increment()` 映射為 `this.$store.commit('some/nested/module/foo')`
'bar'
])
}
}
看一下源代碼:
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
val = namespace + val
res[key] = function mappedMutation(...args) {
if (namespace && !getModuleByNamespace(this.$store, 'mapMutations', namespace)) {
return
}
return this.$store.commit.apply(this.$store, [val].concat(args))
}
})
return res
})
//返回對象數組 key-value
function normalizeMap(map) {
return Array.isArray(map) ?
map.map(key => ({ key, val: key })) :
Object.keys(map).map(key => ({ key, val: map[key] }))
}
//處理命名空間參數問題
function normalizeNamespace(fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
//通過命名空間找模塊
function getModuleByNamespace(store, helper, namespace) {
const module = store._modulesNamespaceMap[namespace]
if (process.env.NODE_ENV !== 'production' && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
其實就是用normalizeNamespace和normalizeMap去統一化參數,生成key-value的對象組,然后對函數驚醒封裝,調用commit方法。
至此,整個vuex的源碼核心就分析完成。源碼中還有一些工具函數類似registerModule、unregisterModule、hotUpdate、watch以及subscribe等,這些方法都是對上面代碼行為的封裝調用,然后暴露出來的接口,都比較容易理解。
鏈接:https://www.cnblogs.com/caizhenbo/p/7380200.html
