為什麼需要Vuex
通常 Vue
專案中的資料通訊,我們通過以下三種方式就可以解決,但是隨著專案多層巢狀的元件增加,兄弟元件間的狀態傳遞非常繁瑣,導致不斷的通過事件來變更狀態,同步狀態多份拷貝,最後程式碼難以維護。於是尤大大開發了 Vuex
來解決這個問題。
- 父傳子
props
; - 子傳父
$emit
; eventBus
事件匯流排。
當然中小 Vue
專案可以不使用 Vuex
,當出現下面這兩種情況的時候我們就應該考慮使用 Vuex
統一管理狀態了。
- 多個檢視依賴於同一狀態;
- 來自不同檢視的行為需要變更同一狀態。
使用Vuex
的優點也很明顯:
- 方便全域性通訊;
- 方便狀態快取;
- 方便通過
vue-devtools
來進行狀態相關的bug排查。
Vuex初使用
官方 Vuex
上有一張用於解釋 Vuex
的圖,但是並沒有給於清晰明確的註釋。這裡簡單說下每塊的功能和作用,以及整個流程圖的單向資料量的流向。
-
Vue Components
:Vue元件。HTML頁面上,負責接收使用者操作等互動行為,執行dispatch
方法觸發對應action
進行回應。 -
dispatch
:操作行為觸發方法,是唯一能執行action的方法。 -
actions
:操作行為處理模組。負責處理Vue Components接收到的所有互動行為。包含同步/非同步操作,支援多個同名方法,按照註冊的順序依次觸發。向後臺API請求的操作就在這個模組中進行,包括觸發其他action
以及提交mutation
的操作。該模組提供了Promise的封裝,以支援action的鏈式觸發。 -
commit
:狀態改變提交操作方法。對mutation
進行提交,是唯一能執行mutation的方法。 -
mutations
:狀態改變操作方法。是Vuex修改state的唯一推薦方法,其他修改方式在嚴格模式下將會報錯。該方法只能進行同步操作,且方法名只能全域性唯一。操作之中會有一些hook暴露出來,以進行state的監控等。 -
state
:頁面狀態管理容器物件。集中儲存Vue components
中data
物件的零散資料,全域性唯一,以進行統一的狀態管理。頁面顯示所需的資料從該物件中進行讀取,利用Vue的細粒度資料響應機制來進行高效的狀態更新。 -
Vue元件
接收互動行為,呼叫dispatch
方法觸發action
相關處理,若頁面狀態需要改變,則呼叫commit
方法提交mutation
修改state
,通過getters
獲取到state
新值,重新渲染Vue Components
,介面隨之更新。
總結:
-
state
裡面就是存放的我們上面所提到的狀態。 -
mutations
就是存放如何更改狀態。 -
getters
就是從state
中派生出狀態,比如將state
中的某個狀態進行過濾然後獲取新的狀態。 -
actions
就是mutation
的加強版,它可以通過commit
mutations中的方法來改變狀態,最重要的是它可以進行非同步操作。 -
modules
顧名思義,就是當用這個容器來裝這些狀態還是顯得混亂的時候,我們就可以把容器分成幾塊,把狀態和管理規則分類來裝。這和我們建立js模組是一個目的,讓程式碼結構更清晰。
關於Vuex的疑問
我們做的專案中使用Vuex,在使用Vuex的過程中留下了一些疑問,發現在使用層面並不能解答我的疑惑。於是將疑問簡單羅列,最近在看了 Vuex
原始碼才明白。
- 如何保證
state
的修改只能在mutation
的回撥函式中? mutations
裡的方法,為什麼可以修改state
?- 為什麼可以通過
this.commit
來呼叫mutation
函式? actions
函式中context物件
,為什麼不是store例項
本身?- 為什麼在
actions函式
裡可以呼叫dispatch
或者commit
? - 通過
this.$store.getters.xx
,是如何可以訪問到getter
函式的執行結果的?
Vuex原始碼分析
針對以上疑問,在看Vuex原始碼的過程中慢慢解惑了。
1. 如何保證 state
的修改只能在 mutation
的回撥函式中?
在Vuex
原始碼的 Store
類中有個 _withCommit
函式:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
複製程式碼
Vuex
中所有對 state
的修改都會呼叫 _withCommit
函式的包裝,保證在同步修改 state 的過程中 this._committing
的值始終為 true
。當我們檢測到 state
變化的時候,如果 this._committing
不為 true
,則能查到這個狀態修改有問題。
2. mutations裡的方法,為什麼可以修改state?
在Vuex
例項化的時候,會呼叫 Store
,Store
會呼叫 installModule
,來對傳入的配置進行模組的註冊和安裝。對 mutations
進行註冊和安裝,呼叫了 registerMutation
方法:
/**
* 註冊mutation 作用同步修改當前模組的 state
* @param {*} store Store例項
* @param {*} type mutation 的 key
* @param {*} handler mutation 執行的函式
* @param {*} local 當前模組
*/
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)
})
}
複製程式碼
該方法對mutation方法進行再次封裝,注意 handler.call(store, local.state, payload)
,這裡改變 mutation
執行的函式的 this
指向為 Store例項
,local.state
為當前模組的 state
,payload
為額外引數。
因為改變了 mutation
執行的函式的 this
指向為 Store例項
,就方便對 this.state
進行修改。
3. 為什麼可以通過 this.commit
來呼叫 mutation
函式?
在 Vuex 中,mutation 的呼叫是通過 store 例項的 API 介面 commit 來呼叫的。來看一下 commit 函式的定義:
/**
*
* @param {*} _type mutation 的型別
* @param {*} _payload 額外的引數
* @param {*} _options 一些配置
*/
commit (_type, _payload, _options) {
// check object-style commit
// unifyObjectStyle 方法對 commit 多種形式傳參 進行處理
// 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
}
// 使用了 this._withCommit 的方法提交 mutation
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 遍歷 this._subscribers,呼叫回撥函式,並把 mutation 和當前的根 state 作為引數傳入
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'
)
}
}
複製程式碼
this.commmit()
接收mutation的型別和外部引數,在 commmit
的實現中通過 this._mutations[type]
去匹配到對應的 mutation
函式,然後呼叫。
4. actions函式中context物件
,為什麼不是store例項本身?
5. 為什麼在actions函式
裡可以呼叫 dispatch
或者 commit
?
actions的使用:
actions: {
getTree(context) {
getDepTree().then(res => {
context.commit('updateTree', res.data)
})
}
}
複製程式碼
在action的初始化函式中有這樣一段程式碼:
/**
* 註冊actions
* @param {*} store 全域性store
* @param {*} type action 型別
* @param {*} handler action 函式
* @param {*} local 當前的module
*/
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
// store._devtoolHook 是在store constructor的時候執行 賦值的
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
複製程式碼
很明顯context物件是指定的,並不是store例項, const {dispatch, commit, getters, state, rootGetters,rootState } = context
context物件上掛載了:
- dispatch, 當前模組上的dispatch函式
- commit, 當前模組上的commit函式
- getters, 當前模組上的getters
- state, 當前模組上的state
- rootGetters, 根模組上的getters
- rootState 根模組上的state
6. 通過 this.$store.getters.xx
,是如何可以訪問到getter函式的執行結果的?
在Vuex原始碼的Store例項的實現中有這樣一個方法 resetStoreVM
:
function resetStoreVM (store, state, hot) {
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]
})
})
// ...
store._vm = new Vue({
data: { state },
computed
})
// ...
}
複製程式碼
遍歷 store._wrappedGetters
物件,在遍歷過程中拿到每個 getter
的包裝函式,並把這個包裝函式執行的結果用 computed
臨時儲存。
然後例項化了一個 Vue例項
,把上面的 computed
作為計算屬性傳入,把 狀態樹state
作為 data
傳入,這樣就完成了註冊。
我們就可以在元件中訪問 this.$store.getters.xxgetter
了,相當於訪問了 store._vm[xxgetter]
,也就是在訪問 computed[xxgetter]
,這樣就訪問到 xxgetter
的回撥函式了。