【vue-系列】深入理解Vuex

尤小小發表於2019-12-28

為什麼需要Vuex

通常 Vue 專案中的資料通訊,我們通過以下三種方式就可以解決,但是隨著專案多層巢狀的元件增加,兄弟元件間的狀態傳遞非常繁瑣,導致不斷的通過事件來變更狀態,同步狀態多份拷貝,最後程式碼難以維護。於是尤大大開發了 Vuex 來解決這個問題。

  • 父傳子 props
  • 子傳父 $emit
  • eventBus 事件匯流排。

當然中小 Vue 專案可以不使用 Vuex,當出現下面這兩種情況的時候我們就應該考慮使用 Vuex 統一管理狀態了。

  • 多個檢視依賴於同一狀態;
  • 來自不同檢視的行為需要變更同一狀態。

使用Vuex的優點也很明顯:

  • 方便全域性通訊;
  • 方便狀態快取;
  • 方便通過 vue-devtools 來進行狀態相關的bug排查。

Vuex初使用

官方 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 componentsdata物件的零散資料,全域性唯一,以進行統一的狀態管理。頁面顯示所需的資料從該物件中進行讀取,利用Vue的細粒度資料響應機制來進行高效的狀態更新。

  • Vue元件接收互動行為,呼叫 dispatch 方法觸發 action 相關處理,若頁面狀態需要改變,則呼叫 commit 方法提交 mutation 修改 state,通過 getters 獲取到 state 新值,重新渲染 Vue Components,介面隨之更新。

總結:

  1. state裡面就是存放的我們上面所提到的狀態。

  2. mutations就是存放如何更改狀態。

  3. getters 就是從 state 中派生出狀態,比如將 state 中的某個狀態進行過濾然後獲取新的狀態。

  4. actions 就是 mutation 的加強版,它可以通過 commit mutations中的方法來改變狀態,最重要的是它可以進行非同步操作。

  5. modules 顧名思義,就是當用這個容器來裝這些狀態還是顯得混亂的時候,我們就可以把容器分成幾塊,把狀態和管理規則分類來裝。這和我們建立js模組是一個目的,讓程式碼結構更清晰。

關於Vuex的疑問

我們做的專案中使用Vuex,在使用Vuex的過程中留下了一些疑問,發現在使用層面並不能解答我的疑惑。於是將疑問簡單羅列,最近在看了 Vuex 原始碼才明白。

閱讀Vuex原始碼的倉庫

image.png

  • 如何保證 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例項化的時候,會呼叫 StoreStore 會呼叫 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 為當前模組的 statepayload 為額外引數。

因為改變了 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 的回撥函式了。

參考

相關文章