當咱們用vue在開發的過程當中,常常會遇到如下問題html
多個vue組件共享狀態vue
Vue組件間的通信vuex
在項目不復雜的時候,咱們會利用全局事件bus的方式解決,但隨着複雜度的提高,用這種方式將會使得代碼難以維護,所以vue官網推薦了一種更好用的解決方案Vuex。api
Vuex是什麼數組
Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。說直白一點,vuex就是把組件的共享狀態抽取出來,以一個全局單例模式管理的數據倉庫,裏面提供了對應用的數據存儲和對數據的操做,維護整個應用的公共數據。app
看源代碼以前,咱們看一下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的使用步驟以下:異步
一、安裝vuex插件ide
二、建立store狀態管理倉庫函數
三、將store注入到vue組件
四、使用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