Vue 源碼深入解析之 Vuex 和 Vuex 初始化

一、Vuex 的理解

  1. Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。

  2. 狀態管理模式,讓我們從一個簡單的 Vue 計數應用開始:

new Vue({
  // state
  data () {
    return {
      count: 0
    }
  },
  // view
  template: `
    <div>{{ count }}</div>
  `,
  // actions
  methods: {
    increment () {
      this.count++
    }
  }
})
  1. 這個狀態自管理應用包含以下幾個部分:
  • state,驅動應用的數據源;
  • view,以聲明方式將 state 映射到視圖;
  • actions,響應在 view 上的用戶輸入導致的狀態變化。
  1. 單向數據流,但是,當我們的應用遇到多個組件共享狀態時,單向數據流的簡潔性很容易被破壞:
  • 多個視圖依賴於同一狀態。
  • 來自不同視圖的行爲需要變更同一狀態。
  1. 對於問題一,傳參的方法對於多層嵌套的組件將會非常繁瑣,並且對於兄弟組件間的狀態傳遞無能爲力。對於問題二,我們經常會採用父子組件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的代碼。因此,我們爲什麼不把組件的共享狀態抽取出來,以一個全局單例模式管理呢?在這種模式下,我們的組件樹構成了一個巨大的“視圖”,不管在樹的哪個位置,任何組件都能獲取狀態或者觸發行爲。

  2. Vuex 核心思想,Vuex 應用的核心就是 store(倉庫)。store 基本上就是一個容器,它包含着你的應用中大部分的狀態 (state)。定義一個全局對象,再去上層封裝了一些數據存取的接口不也可以麼?Vuex 和單純的全局對象有以下兩點不同:

  • Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的組件也會相應地得到高效更新。
  • 你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地瞭解我們的應用。
  • 通過定義和隔離狀態管理中的各種概念並強制遵守一定的規則,我們的代碼將會變得更結構化且易維護。

二、Vuex 初始化

  1. 分析 Vuex 的初始化過程,它包括安裝、Store 實例化過兩個方面。

  2. 安裝,當我們在代碼中通過 import Vuex from 'vuex' 的時候,實際上引用的是一個對象,它的定義在 src/index.js 中:

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

和 Vue-Router 一樣,Vuex 也同樣存在一個靜態的 install 方法,它的定義在 src/store.js 中:

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)
}

install 的邏輯很簡單,把傳入的 _Vue 賦值給 Vue 並執行了 applyMixin(Vue) 方法,它的定義在 src/mixin.js 中:

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}
  1. applyMixin 就是這個 export default function,它還兼容了 Vue 1.0 的版本,這裏我們只關注 Vue 2.0 以上版本的邏輯,它其實就全局混入了一個 beforeCreate 鉤子函數,它的實現非常簡單,就是把 options.store 保存在所有組件的 this.$store 中,這個 options.store 就是我們在實例化 Store 對象的實例,這也是爲什麼我們在組件中可以通過 this.$store 訪問到這個實例。

  2. Store 實例化,我們在 import Vuex 之後,會實例化其中的 Store 對象,返回 store 實例並傳入 new Vueoptions 中,也就是我們剛纔提到的 options.store。舉個簡單的例子,如下:

export default new Vuex.Store({
  actions,
  getters,
  state,
  mutations,
  modules
  // ...
})
  1. Store 對象的構造函數接收一個對象參數,它包含 actionsgettersstatemutationsmodulesVuex 的核心概念,它的定義在 src/store.js 中:
export class Store {
  constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    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.`)
    }

    const {
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    const state = this._modules.root.state

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins
    plugins.forEach(plugin => plugin(this))

    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }
}  

我們把 Store 的實例化過程拆成三個部分,分別是初始化模塊,安裝模塊和初始化 store._vm,接下來我們來分析這三部分的實現。

  1. 初始化模塊,在分析模塊初始化之前,我們先來了解一下模塊對於 Vuex 的意義:由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象,當應用變得非常複雜時,store 對象就有可能變得相當臃腫。爲了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 statemutationactiongetter,甚至是嵌套子模塊——從上至下進行同樣方式的分割:
const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... },
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態
  1. 從數據結構上來看,模塊的設計就是一個樹型結構,store 本身可以理解爲一個 root module,它下面的 modules 就是子模塊,Vuex 需要完成這顆樹的構建,構建過程的入口就是:
this._modules = new ModuleCollection(options)

ModuleCollection 的定義在 src/module/module-collection.js 中:

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

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

  getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

  update (rawRootModule) {
    update([], this.root, rawRootModule)
  }

  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)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  unregister (path) {
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]
    if (!parent.getChild(key).runtime) return

    parent.removeChild(key)
  }
}
  1. ModuleCollection 實例化的過程就是執行了 register 方法,
    register 接收三個參數,其中 path 表示路徑,因爲我們整體目標是要構建一顆模塊樹,path 是在構建樹的過程中維護的路徑;rawModule 表示定義模塊的原始配置;runtime 表示是否是一個運行時創建的模塊。

  2. register 方法首先通過 const newModule = new Module(rawModule, runtime) 創建了一個 Module 的實例,Module 是用來描述單個模塊的類,它的定義在 src/module/module.js 中:

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  get namespaced () {
    return !!this._rawModule.namespaced
  }

  addChild (key, module) {
    this._children[key] = module
  }

  removeChild (key) {
    delete this._children[key]
  }

  getChild (key) {
    return this._children[key]
  }

  update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }

  forEachChild (fn) {
    forEachValue(this._children, fn)
  }

  forEachGetter (fn) {
    if (this._rawModule.getters) {
      forEachValue(this._rawModule.getters, fn)
    }
  }

  forEachAction (fn) {
    if (this._rawModule.actions) {
      forEachValue(this._rawModule.actions, fn)
    }
  }

  forEachMutation (fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
}
  1. 來看一下 Module 的構造函數,對於每個模塊而言,this._rawModule 表示模塊的配置,this._children 表示它的所有子模塊,this.state 表示這個模塊定義的 state。回到 register,那麼在實例化一個 Module 後,判斷當前的 path 的長度如果爲 0,則說明它是一個根模塊,所以把 newModule 賦值給了 this.root,否則就需要建立父子關係了:
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
  1. 我們先大體上了解它的邏輯:首先根據路徑獲取到父模塊,然後再調用父模塊的 addChild 方法建立父子關係。register 的最後一步,就是遍歷當前模塊定義中的所有 modules,根據 key 作爲 path,遞歸調用 register 方法,這樣我們再回過頭看一下建立父子關係的邏輯,首先執行了 this.get(path.slice(0, -1) 方法:
get (path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}
  1. 傳入的 path 是它的父模塊的 path,然後從根模塊開始,通過 reduce 方法一層層去找到對應的模塊,查找的過程中,執行的是 module.getChild(key) 方法:
getChild (key) {
  return this._children[key]
}
  1. 其實就是返回當前模塊的 _children 中對應 key 的模塊,那麼每個模塊的 _children 是如何添加的呢,是通過執行 parent.addChild(path[path.length - 1], newModule) 方法:
addChild (key, module) {
  this._children[key] = module
}
  1. 對於 root module 的下一層 modules 來說,它們的 parent 就是 root module,那麼他們就會被添加的 root module_children 中。每個子模塊通過路徑找到它的父模塊,然後通過父模塊的 addChild 方法建立父子關係,遞歸執行這樣的過程,最終就建立一顆完整的模塊樹。

  2. 安裝模塊,初始化模塊後,執行安裝模塊的相關邏輯,它的目標就是對模塊中的 stategettersmutationsactions 做初始化工作,它的入口代碼是:

const state = this._modules.root.state
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
  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)
  })
}
  1. installModule 方法支持五個參數,store 表示 root storestate 表示 root statepath 表示模塊的訪問路徑;module 表示當前的模塊,hot 表示是否是熱更新。接下來看函數邏輯,這裏涉及到了命名空間的概念,默認情況下,模塊內部的 actionmutationgetter 是註冊在全局命名空間的——這樣使得多個模塊能夠對同一 mutationaction 作出響應。如果我們希望模塊具有更高的封裝度和複用性,可以通過添加 namespaced: true 的方式使其成爲帶命名空間的模塊。當模塊被註冊後,它的所有 getteractionmutation 都會自動根據模塊註冊的路徑調整命名。例如:
const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模塊內容(module assets)
      state: { ... }, // 模塊內的狀態已經是嵌套的了,使用 `namespaced` 屬性不會對其產生影響
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模塊
      modules: {
        // 繼承父模塊的命名空間
        myPage: {
          state: { ... },
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 進一步嵌套命名空間
        posts: {
          namespaced: true,

          state: { ... },
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

回到 installModule 方法,我們首先根據 path 獲取 namespace

const namespace = store._modules.getNamespace(path)

getNamespace 的定義在 src/module/module-collection.js 中:

getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}
  1. root module 開始,通過 reduce 方法一層層找子模塊,如果發現該模塊配置了 namespacedtrue,則把該模塊的 key 拼到 namesapce 中,最終返回完整的 namespace 字符串。回到 installModule 方法,接下來把 namespace 對應的模塊保存下來,爲了方便以後能根據 namespace 查找模塊:
if (module.namespaced) {
  store._modulesNamespaceMap[namespace] = module
}
  1. 接下來判斷非 root module 且非 hot 的情況執行一些邏輯,我們稍後再看。接着是很重要的邏輯,構造了一個本地上下文環境:
const local = module.context = makeLocalContext(store, namespace, path)

來看一下 makeLocalContext 實現:

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)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
  1. makeLocalContext 支持三個參數相關,store 表示 root storenamespace 表示模塊的命名空間,path 表示模塊的 path。該方法定義了 local 對象,對於 dispatchcommit 方法,如果沒有 namespace,它們就直接指向了 root storedispatchcommit 方法,否則會創建方法,把 type 自動拼接上 namespace,然後執行 store 上對應的方法。對於 getters 而言,如果沒有 namespace,則直接返回 root storegetters,否則返回 makeLocalGetters(store, namespace) 的返回值:
function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    // skip if the target getter is not match this namespace
    if (type.slice(0, splitPos) !== namespace) return

    // extract local getter type
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}
  1. makeLocalGetters 首先獲取了 namespace 的長度,然後遍歷 root store 下的所有 getters,先判斷它的類型是否匹配 namespace,只有匹配的時候我們從 namespace 的位置截取後面的字符串得到 localType,接着用 Object.defineProperty 定義了 gettersProxy,獲取 localType 實際上是訪問了 store.getters[type]。回到 makeLocalContext 方法,再來看一下對 state 的實現,它的獲取則是通過 getNestedState(store.state, path) 方法:
function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}
  1. getNestedState 邏輯很簡單,從 root state 開始,通過 path.reduce 方法一層層查找子模塊 state,最終找到目標模塊的 state。那麼構造完 local 上下文後,我們再回到 installModule 方法,接下來它就會遍歷模塊中定義的 mutationsactionsgetters,分別執行它們的註冊工作,它們的註冊邏輯都大同小異,如下所示:
  • registerMutation
module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, 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)
  })
}

首先遍歷模塊中的 mutations 的定義,拿到每一個 mutationkey,並把 key 拼接上 namespace,然後執行 registerMutation 方法。該方法實際上就是給 root store 上的 _mutations[types] 添加 wrappedMutationHandler 方法,該方法的具體實現我們之後會提到。注意,同一 type_mutations 可以對應多個方法。

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

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
    }
  })
}

首先遍歷模塊中的 actions 的定義,拿到每一個 actionkey,並判斷 action.root,如果否的情況把 key 拼接上 namespace,然後執行 registerAction 方法。該方法實際上就是給 root store 上的 _actions[types] 添加 wrappedActionHandler 方法,該方法的具體實現我們之後會提到。注意,同一 type_actions 可以對應多個方法。

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


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
    )
  }
}

首先遍歷模塊中的 getters 的定義,拿到每一個 getterkey,並把 key 拼接上 namespace,然後執行 registerGetter 方法。該方法實際上就是給 root store 上的 _wrappedGetters[key] 指定 wrappedGetter 方法,該方法的具體實現我們之後會提到。注意,同一 type_wrappedGetters 只能定義一個。

再回到 installModule 方法,最後一步就是遍歷模塊中的所有子 modules,遞歸執行 installModule 方法:

module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})
  1. 之前忽略了非 root module 下的 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)
  })
}
  1. 之前我們提到過 getNestedState 方法,它是從 root state 開始,一層層根據模塊名能訪問到對應 pathstate,那麼它每一層關係的建立實際上就是通過這段 state 的初始化邏輯。store._withCommit 方法我們之後再介紹。所以 installModule 實際上就是完成了模塊下的 stategettersactionsmutations 的初始化工作,並且通過遞歸遍歷的方式,就完成了所有子模塊的安裝工作。

  2. 初始化 store._vmStore 實例化的最後一步,就是執行初始化 store._vm 的邏輯,它的入口代碼是:

resetStoreVM(this, state)

來看一下 resetStoreVM 的定義:

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

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

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }

  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())
  }
}
  1. resetStoreVM 的作用實際上是想建立 gettersstate 的聯繫,因爲從設計上 getters 的獲取就依賴了 state ,並且希望它的依賴能被緩存起來,且只有當它的依賴值發生了改變纔會被重新計算。因此這裏利用了 Vue 中用 computed 計算屬性來實現。

  2. resetStoreVM 首先遍歷了 _wrappedGetters 獲得每個 getter 的函數 fnkey,然後定義了 computed[key] = () => fn(store)。我們之前提到過 _wrappedGetters 的初始化過程,這裏 fn(store) 相當於執行如下方法:

store._wrappedGetters[type] = function wrappedGetter (store) {
  return rawGetter(
    local.state, // local state
    local.getters, // local getters
    store.state, // root state
    store.getters // root getters
  )
}
  1. 返回的就是 rawGetter 的執行函數,rawGetter 就是用戶定義的 getter 函數,它的前兩個參數是 local statelocal getters,後兩個參數是 root stateroot getters。接着實例化一個 Vue 實例 store._vm,並把 computed 傳入:
store._vm = new Vue({
  data: {
    $$state: state
  },
  computed
})

我們發現 data 選項裏定義了 $$state 屬性,而我們訪問 store.state 的時候,實際上會訪問 Store 類上定義的 stateget 方法:

get state () {
  return this._vm._data.$$state
}

它實際上就訪問了 store._vm._data.$$state。那麼 gettersstate 是如何建立依賴邏輯的呢,我們再看這段代碼邏輯:

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
    })
  })
  1. 當我根據 key 訪問 store.getters 的某一個 getter 的時候,實際上就是訪問了 store._vm[key],也就是 computed[key],在執行 computed[key] 對應的函數的時候,會執行 rawGetter(local.state,...) 方法,那麼就會訪問到 store.state,進而訪問到 store._vm._data.$$state,這樣就建立了一個依賴關係。當 store.state 發生變化的時候,下一次再訪問 store.getters 的時候會重新計算。我們再來看一下 strict mode 的邏輯:
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 })
}
  1. 當嚴格模式下,store._vm 會添加一個 wathcer 來觀測 this._data.$$state 的變化,也就是當 store.state 被修改的時候, store._committing 必須爲 true,否則在開發階段會報警告。store._committing 默認值是 false,那麼它什麼時候會 true 呢,Store 定義了 _withCommit 實例方法:
_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}

它就是對 fn 包裝了一個環境,確保在 fn 中執行任何邏輯的時候 this._committing = true。所以外部任何非通過 Vuex 提供的接口直接操作修改 state 的行爲都會在開發階段觸發警告。

  1. 總結:Vuex 的初始化過程就分析完畢了,除了安裝部分,我們重點分析了 Store 的實例化過程。我們要把 store 想象成一個數據倉庫,爲了更方便的管理倉庫,我們把一個大的 store 拆成一些 modules,整個 modules 是一個樹型結構。每個 module 又分別定義了 stategettersmutationsactions,我們也通過遞歸遍歷模塊的方式都完成了它們的初始化。爲了 module 具有更高的封裝度和複用性,還定義了 namespace 的概念。最後我們還定義了一個內部的 Vue 實例,用來建立 stategetters 的聯繫,並且可以在嚴格模式下監測 state 的變化是不是來自外部,確保改變 state 的唯一途徑就是顯式地提交 mutation。我們已經建立好 store,接下來就是對外提供了一些 API 方便我們對這個 store 做數據存取的操作,後面我們就來從源碼角度來分析 Vuex 提供的一系列 API
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章