【Vue】Vuex 從使用到原理分析(中篇)

前言

該系列分爲上中下三篇:vue

在上一篇中,咱們大體瞭解了Vuex的概念以及食用方式,這裏主要是從源碼的角度來詳細揭開Vuex的神祕面紗。vuex

咱們以入門版食用方式加一些模塊爲例:api

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);
const vm = new Vue();

// module a
const moduleA = {
  namespaced: true,
  state: {
    number: 0,
  },
  getters: {
    getNumberPlusOne(state) {
      return state.number + 1;
    },
  },
  mutations: {
    setNumber(state, num) {
      state.number = num;
    },
  },
  actions: {
    async setNumberAsync({ commit }) {
      const { data } = await vm.$http('/api/get-number');
      commit('setNumber', data);
    },
  },
};

// main
export default new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    setCount(state, count) {
      state.count = count;
    },
  },
  getters: {
    getCountPlusOne(state) {
      return state.count + 1;
    },
  },
  actions: {
    async setCountAsync({ commit }, count) {
      const { data: count } = await vm.$http('/api/example', { count });
      commit('setCount', count);
    },
  },
  modules: {
    moduleA,
  },
});
複製代碼

下面一步步分析。數組

PS:未特殊說明路徑,代碼內容則在當前文件內。緩存

1. import Vuex from 'vuex'

打開 vuex 源碼,咱們這裏採用的是 3.1.1 版本進行分析,找到 index.js,內容以下:app

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}
複製代碼

因而可知,咱們引入 Vuex 對象,包含了輔助函數以及 Store 對象、install 方法以及 createNamespacedHelpers 輔助函數。異步

2. Vue.use(Vuex)

咱們知道 Vue.use 方法會注入一個插件,調用這個對象的install方法,咱們打開 store.js找到最後的install方法:async

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
複製代碼
  1. 對 Vue 對象作了一層校驗,防止重複安裝
  2. 將外層 Vue 賦值給本地 Vue
  3. 調用 applyMixin 方法初始化 vuex store

mixin.js 中的 applyMixin 方法:ide

咱們精簡一下,核心內容以下:函數

export default function (Vue) {
  
  Vue.mixin({ beforeCreate: vuexInit })

  function vuexInit () {
    const options = this.$options
    // store injection
    this.$store = typeof options.store === 'function'
      ? options.store()
      : options.store
  }
}
複製代碼

這樣看來,其實就是在 Vue beforeCreate 生命週期鉤子函數裏執行了 vuexInit 方法,將實例化的 Store 對象掛載到 $store 上,這也是爲何咱們能在 vue 組件中直接經過 this.$store 就能夠進行相關操做的緣由。

3. new Vuex.Store()

當咱們實例化一個 Store 的時候,進行了什麼操做呢?

咱們找到store.js裏的 Store 類,省略一些不過重要的代碼,主要看到以下部分:

咱們省略一些不過重要的代碼,主要看到以下部分:

export class Store {
  constructor (options = {}) {
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `store must be called with the new operator.`)
    }
    
    // ...
    
    this._modules = new ModuleCollection(options)
    
    // ...
    const state = this._modules.root.state
    installModule(this, state, [], this._modules.root)
    
    resetStoreVM(this, state)
  }
}
複製代碼

上面幾個部分也就是主要的邏輯

  1. 安裝 Vuex 插件
  2. 獲取模塊
  3. 初始化 store vm

首先是若是沒有傳入 Vue,那麼自動安裝一下插件,若是是開發環境則會報錯。

store 能夠拆分紅各類小的 store,那麼整個看起來就是一個樹形結構。store 自己至關於根節點,每個 module 都是一個子節點,那麼首先要作的就是獲取到這些子節點。

以後再將子節點裏的數據以及 getter、state 進行關聯。

4. 獲取模塊

咱們打開module/module-collection.js文件,找到 ModuleCollection 類,以下:

export default class ModuleCollection {
  constructor (rawRootModule) {
    this.register([], rawRootModule, false)
  }
}
複製代碼

能夠看到實例化的過程就是執行了 register 方法。

register (path, rawModule, runtime = true) {
  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)
    })
  }
}
複製代碼
  • path:module 的路徑
  • rowModule:export default 的對象,模塊的配置項
  • runtime:是不是運行時建立的模塊

這裏第一步是實例化了一個 Module 對象,這個類定義在 module/module.js裏:

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    
    this._children = Object.create(null)
    
    this._rawModule = rawModule
    const rawState = rawModule.state

    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
}
複製代碼

_children 是該模塊的子模塊,_rawModule 是該模塊的配置,state 是該模塊的 state。

返回到 register 方法繼續,實例化 Module 後,接下來第一次進入 path 是空數組,因此這就是根 store,賦值當前配置到 root 上。

接着判斷是否有 modules 項,若是有,則執行下面代碼:

forEachValue(rawModule.modules, (rawChildModule, key) => {
  this.register(path.concat(key), rawChildModule, runtime)
})
複製代碼

這段代碼主要是遍歷 modules,遞歸調用 register 方法,將配置項裏的 modules 的 key 做爲路徑保存到 path 中,傳入子 module 和建立狀態。(以開始的例子,key 就是 'moduleA')

第二次進入 register,此時走到if (path.length === 0) {}的判斷,因爲此時 path 已經有內容了,因此會執行 else 的邏輯:

const parent = this.get(path.slice(0, -1)) // 這裏至關於 path 彈出了最後一項
parent.addChild(path[path.length - 1], newModule)


get (path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}

// module.js 裏的 addChild、getChild
addChild (key, module) {
  this._children[key] = module
}
getChild (key) {
  return this._children[key]
}
複製代碼

首先獲取父模塊,這裏經過 get 方法中的 reduce,層層遞進深度搜索出當前模塊的父模塊而後返回。

經過 Module 實例的 addChild 方法給掛載到 _children 上,(例子中至關於 key: 'moduleA', value: moduleA 對象)。

這樣遞歸註冊,就對全部的模塊進行實例化,經過 _children 創建好父子關係,一顆組件樹就構建完成了。

5. 安裝模塊

當咱們構建好模塊樹,接下來就須要去安裝這些模塊了,截取 installModule 方法代碼以下:

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  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)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
複製代碼

這裏的主要邏輯就是初始化 state、getters、mutations、actions,這裏有 5 個參數,分別表明如下意思:

  • store:root store
  • rootState:root state
  • path:模塊訪問路徑
  • module:當前模塊
  • hot:是否熱更新

第一步定義 isRoot 變量用來判斷是不是 root store,接下來獲取咱們定義的命名空間,若是有定義命名空間(namespaced: true),則把模塊掛載到以命名空間爲 key 的 _modulesNamespaceMap 對象上。

第二步判斷非根模塊非熱更新的狀況下,獲取父模塊的 state,獲取當前模塊的名稱,經過 Vue.set 將當前模塊的 state 掛載到父模塊上,key 是模塊名稱。因此這也是 store 裏的數據都是響應式的緣由。

store._withCommit(() => {
  Vue.set(parentState, moduleName, module.state)
})


_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}

// 後面會介紹(在安裝模式的嚴格模式中)
if (process.env.NODE_ENV !== 'production') {
  assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}
複製代碼

經過 _withCommit 的代理,咱們在修改 state 的時候,在開發環境經過 this._committing 標誌就能拋出錯誤,避免意外更改。

第三步經過makeLocalContext方法建立本地上下文環境,接收 store(root store)、namespace(模塊命名空間)、path(模塊路徑) 三個參數。

function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }

  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
複製代碼

這裏沒有命名空間的狀況就是直接使用 store 上的 dispatch 和 commit,若是有則使用新定義的方法,這個方法接收三個參數:

  • _type:dispatch、commit 的 type
  • _payload:提交的參數
  • _options:其餘選項,例如 { root: true } 這個在子模塊派發到根倉庫的配置項

咱們在 commit、dispatch 的時候會有兩種寫法傳參:

  1. type, payload, options
  2. { type, payload }, options

unifyObjectStyle 函數

function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) { // 若是是對象傳參
    options = payload // 第二個參數是 { root: true } 這種了
    payload = type // 第一個參數就是 payload,無論裏面的 type 屬性
    type = type.type // 將 type 值賦值給 type
  }

  // 對 type 類型的一個斷言
  if (process.env.NODE_ENV !== 'production') {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }

  // 返回一個完整對象
  return { type, payload, options }
}
複製代碼

因此 unifyObjectStyle 函數就是幫助咱們把參數整合成 type、payload、options 三個變量裏。

接下來判斷只要不是派發到跟模塊或者當前模塊就是根模塊,那麼 type 就須要加上命名空間(例子中就變成了 'moduleA/setNumber'),而後 commit/dispatch 出去。

最後將 getters、state 經過 defineProperties 劫持到 local 對象上,值爲當前模塊的 getters、state。

makeLocalGetters、getNestedState 函數

function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    
    // 判斷 type 前的命名空間是否匹配當前模塊的命名
    // 例子中 type 是 'moduleA/getNumberPlusOne', namespace 是 'moduleA/'
    if (type.slice(0, splitPos) !== namespace) return

    // 獲取本地 type,也就是 getNumberPlusOne
    const localType = type.slice(splitPos)

    // 這一步使得 localType 實際上就是訪問了 store.getters[type]
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  // 訪問代理對象
  return gettersProxy
}

// 經過 reduce 一層層獲取到當前模塊的 state,而後返回這個 state
function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}
複製代碼

獲取進行了各類代理數據的本地上下文後,接下來會遍歷 mutations、actions、getters,分別進行註冊,而 state 的註冊早就在以前實例化 Module 的時候就完成了。

Mutations 註冊

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})
複製代碼

mutations 的註冊相對簡單,遍歷 module 下的每個 mutations 屬性的值,而後獲取帶有命名空間的 type,再調用 registerMutation 方法進行註冊,傳入4個參數:

  • store:store 實例
  • namespacedType:帶命名空間的 type。例子中是 'moduleA/setNumber'
  • handler:type 處理函數,也就是 setNumber 函數
  • local:上下文環境,root 爲 store,module 爲 local

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)
  })
}
複製代碼

entry 是一個數組,爲何是數組呢,當咱們沒有使用命名空間時,恰巧在子模塊也有一個setCount方法,那麼這個方法就會存到 setCount 爲屬性值的一個數組中,從而容許咱們一個 type 對應多個 mutaions。

entry push 一個執行 type 的回調函數的一個包裝函數,這也是 mutaions 裏的函數支持兩個參數 state、payload 的緣由。

Actions 註冊

module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
複製代碼

回調主要作了三件事:

  1. 獲取 action type
  2. 獲取 action 回調
  3. 調用 registerAction 註冊方法

registerAction 方法:

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}
複製代碼

忽略幾個 if 的判斷,能夠看到 actions 裏經過 Promise 實現異步過程,這也是爲何 mutaions 裏不支持異步,而能夠經過 actions 來完成了。

actions 回調的第一個參數是一個對象,裏面包含 dispatch、commit、getters、state、rootGetters、rootState 字段,第二個參數通常是咱們所傳遞的參數,第三個參數 cb 經本人驗證徹底沒有用,在dispatch方法中 handler 也只提供了 payload 一個參數,最後根據 res 的類型返回對應的值。

Getters 註冊

module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
複製代碼

這裏沒什麼好說的,看看registerGetter方法:

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
    )
  }
}
複製代碼

首先拋個錯,防止 getter key 重複,接着在 _wrappedGetters 上以 type 爲 key,掛載 wrappedGetter 函數,返回 rawGetters 函數執行的結果,這個函數就是咱們定義在 store.js getters 裏 type 對應的回調。

因此咱們在 getters 裏定義函數接收的參數有 state、getters、rootState、rootGetters 4個,在Vuex 從使用到原理分析(上篇)的高級版食用方式中有說明應用。

安裝子模塊

當上面步驟都完成後,就開始遍歷模塊的子模塊,而後遞歸安裝。

module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
複製代碼

6. 初始化 store._vm

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const 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
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
複製代碼

這裏主要是將 state 與 getters 創建好關係,實例化一個 Vue 掛載到 _vm 屬性上,經過 computed 屬性將 getters 與 state 關聯起來並緩存結果。咱們訪問this.$store.getters.getCountPlusOne的時候,其實訪問的就是this.$store._vm.getCountPlusOne,再繼續就是訪問到的 _vm 的 computed 裏定義的數據,

在執行computed.getCountPlusOne對應的函數時,會執行store._wrappedGetters.getCountPlusOne方法,這個方法又是咱們在分析註冊 Getters 時有提到的wrappedGetter的方法:

function registerGetter (store, type, rawGetter, local) {
  // ...
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
複製代碼

因此最後是執行了咱們定義的 getters 對象裏的方法,這裏就會訪問到store.state,進而訪問到store._vm._data.$$state,經過這樣一層一層,就創建了 state 與 getters 的依賴關係,當store.state的發生變化時,下次訪問store.getters就得到從新計算的結果,咱們用一張圖來更爲直觀的看清楚這個過程。

getetr 與 state

接下來的就相對簡單了,一個是嚴格模式,一個是銷燬舊的實例。下面看看嚴格模式:

if (store.strict) {
  enableStrictMode(store)
}

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}
複製代碼

在安裝模塊中有提到 [_commit](#5. 安裝模塊) 方法,裏面有個_committing字段,就是在這裏使用到的。

嚴格模式中,_vmwatch $$state的變化,當store.state變化時,_committing必須爲true,不然在開發環境拋出警告。而_committing的值只會在_commit方法中提mutaion時會被短暫置爲true,因此Vuex經過這種操做來規避咱們在其餘地方修改了store.state,而沒有按照預期。

總結

這一篇主要介紹了引入 Vuex,註冊 Store,獲取模塊,構建模塊樹,安裝模塊以及給模塊註冊 mutaions、actions、getters,最後經過 store._vm 給 state 與 getters 綁定,以及經過 computed 來緩存 getters 的結果。可是還有一個方法咱們沒有說明,例如 commit、dispatch 以及 4 個輔助函數,這些內容會在Vuex 從使用到原理分析(下篇)中進行分析。

相關文章
相關標籤/搜索