vuex 源代碼簡析

說明

  • 如下內容均是以 Vuex 2.0.0 版本展開分析。
  • 此篇文章,是本身對 Vuex 實現分析的記錄性文章,若有任何錯誤,歡迎指正交流。

Vuex 示意圖

在說以前先來看一張官方文檔提供的一張圖html

vuex

  1. 首先組件渲染依賴了部分全局的 State 內部預先定義的屬性。
  2. 組件內部須要更新以前依賴的 State 內部屬性,則須要調度(Dispatch)觸發 Action 響應狀態變化(也可直接提交 mutation)。
  3. 響應狀態變化函數內部必須提交 mutation 去更改狀態(相似發事件)。
  4. State 內部屬性的更改,觸發依賴其屬性的組件從新渲染。

基礎示例剖析

瞭解了大體流程,接下來咱們就以基礎示例入手,剖析其實現了哪些功能,根據其實現的功能逐步去捋清其代碼實現vue

import Vue from 'vue'
import Vuex from 'vuex'

// 註冊插件
Vue.use(Vuex)

// 根狀態對象。每一個Vuex實例只是一個狀態樹。
const state = { count: 0 }

// mutations 其實是改變狀態的操做。每一個 mutation 處理程序都將整個狀態樹做爲第一個參數,而後是附加的有效負載參數。
// mutations 必須是同步的,而且能夠經過插件記錄下來,以便調試。
const mutations = {
  increment (state) {
    state.count++
  },
  decrement (state) {
    state.count--
  }
}

// actions 是致使反作用並可能涉及異步操做的函數。
const actions = {
  increment: ({ commit }) => commit('increment'),
  decrement: ({ commit }) => commit('decrement'),
  incrementIfOdd ({ commit, state }) {
    if ((state.count + 1) % 2 === 0) {
      commit('increment')
    }
  },
  incrementAsync ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('increment')
        resolve()
      }, 1000)
    })
  }
}

// getters are functions
const getters = {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}

// 模塊
const moduleDemo = {
  state: { moduleCount: 1 },
  mutations: {
    // state: 模塊的局部狀態對象。
    moduleIncrement(state) {
      state.moduleCount++
    },
  },
  actions: {
    moduleIncrement: ({ commit }) => commit('moduleIncrement'),
  },
  getters: {
    moduleCountPlus: ({ moduleCount }) => moduleCount++
  }
}

// Vuex實例是經過組合 state、 mutations 、actions 和 getter 建立的。
const store = new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  modules: {
    moduleDemo
  },
})

new Vue({
  el: '#app',
  store
})

複製代碼
  • 根據上述基礎使用,咱們大概能夠梳理出如下幾點:
  • 註冊插件。
  • 定義 store 所需配置參數。
  • 實例化 Store 類.
  • 實例化 Vue 並傳入 store。

在看具體的代碼實現以前,咱們大體的先了解一下整個 Vuex 入口文件內的大體內容:git

import devtoolPlugin from './plugins/devtool'
import applyMixin from './mixin'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'
import { isObject, isPromise, assert } from './util'

let Vue // 綁定安裝

// Store 全局單例模式管理
class Store { ... }

// 更新模塊
function updateModule(targetModule, newModule) { ... }

// 重置 Store
function resetStore(store) { ... }

// 重置 Store 上 Vue 實例
function resetStoreVM(store, state) { ... }

// 安裝模塊
function installModule(store, rootState, path, module, hot) { ... }

// 註冊 mutations 構造器選項
function registerMutation(store, type, handler, path = []) { ... }

// 註冊 action
function registerAction(store, type, handler, path = []) { ... }

// 包裝 getters
function wrapGetters(store, moduleGetters, modulePath) { ... }

// 啓用嚴格模式
function enableStrictMode(store) {}

// 獲取嵌套的狀態
function getNestedState(state, path) {}

// 插件註冊方法
function install(_Vue) {}

// 自動註冊插件
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}
複製代碼

在瞭解了其內部構造,咱們就根據上述梳理,逐點分析其實現。github

註冊插件

咱們知道,Vue 的插件都須要給 Vue 提供一個註冊鉤子函數 installl, 執行 Vue.use(Vuex) 實際內部走的是 install 函數的內部調用。vuex

install

// 插件註冊:vue 內部在調用會把 Vue 透傳過來
function install(_Vue) {
  // 避免重複註冊
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  // 綁定安裝
  Vue = _Vue
  // 應用全局 Vue.mixins
  applyMixin(Vue)
}

// 自動註冊機制
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

複製代碼

applyMixin

export default function (Vue) {
  // 獲取 Vue 版本號
  const version = Number(Vue.version.split('.')[0])

  // 版本號爲 2.x
  if (version >= 2) {
    // 若存在 init 鉤子則把 VuexInit 混入 初始化階段
    // 其它混入 beforeCreate 階段
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // 覆蓋 init 併爲 1.x 注入 vuex init 過程。 向後兼容性。
    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鉤子,注入到每一個實例init鉤子列表中。 */
  function vuexInit() {
    // 獲取實例配置參數
    const options = this.$options
    // 注入 store 實例
    if (options.store) {
      this.$store = options.store
      // 若不存在,則去尋找父級 store 實例
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

複製代碼

注:_lifecycleHooksapi

  • Vue 內部配置項,引用其生命週期相關鉤子函數的函數名,因爲遺留緣由而暴露的配置項。數組

    • Vue 2.0.0 ⬆️ 其引用爲:
    [
        'beforeCreate',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated', // 激活
        'deactivated', // 停用
        'errorCaptured' // 捕獲錯誤
      ]
    複製代碼
    • Vue v2.0.0-alpha.6 ⬇️ 其引用爲:
    /** * List of lifecycle hooks. */
      _lifecycleHooks: [
        'init',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated',
        'deactivated'
      ]
    複製代碼

若對此有興趣,能夠研究一下 Vue.js 版本的更迭。promise

根據上述分析咱們知道,在註冊插件時,根據Vue的不一樣版本選擇合適的混入時機,使得建立的每一個 Vue 實例在 「初始化階段」 作些預處理(在實例添加$store 屬性,其值爲 Store 實例)。那麼接下來咱們就具體來看看 Store 內部作了些什麼?瀏覽器

Store

/** * Store * * @class Store 全局單例模式管理 */
class Store {
  constructor(options = {}) {
    assert(Vue, `在建立商店實例以前必須調用 Vue.use(Vuex)`)
    assert(typeof Promise !== 'undefined', `vuex 須要一個 promise polyfill 在這個瀏覽器。`)

    const {
      state = {}, // Object | Function Vuex store 實例的根 state 對象
      plugins = [], // Array<Function> 一個數組,包含應用在 store 上的插件方法。這些插件直接接收 store 做爲惟一參數,能夠監聽 mutation
      strict = false // 嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤。
    } = options // Vuex.Store 構造器選項

    // store 內部狀態
    this._options = options
    // 是否正在提交
    this._committing = false
    // 存儲着 全部 actions
    this._actions = Object.create(null)
    // 存儲着 全部 mutations
    this._mutations = Object.create(null)
    // 存儲着 全部 Getters
    this._wrappedGetters = Object.create(null)
    this._runtimeModules = Object.create(null)
    // 訂閱函數池
    this._subscribers = []
    // 存儲着 Vue 實例
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    // 實例方法 - 分發 action 返回一個解析全部被觸發的 action 處理器的 Promise。
    this.dispatch = function boundDispatch(type, payload) {
      return dispatch.call(store, type, payload)
    }
    // 實例方法 - 提交 mutation
    this.commit = function boundCommit(type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // 嚴格模式
    this.strict = strict

    // init root 模塊。這還遞歸地註冊全部子模塊,並在 this._wrappedgechers 中收集全部模塊 getter
    installModule(this, state, [], options)

    // 初始化負責反應性的存儲vm(也將_wrappedgechers註冊爲計算屬性)
    resetStoreVM(this, state)

    // 注入應用插件
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }

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

  set state(v) {
    assert(false, `使用 store.replacestate() 顯式替換存儲狀態。`)
  }

  /** * 更改 Vuex 的 store 中的狀態的惟一方法是提交 mutation。 * 如:store.commit('increment') * 每一個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。 * 這個回調函數就是咱們實際進行狀態更改的地方,而且它會接受 state 做爲第一個參數 * * @param {String} type * @param {Object} payload * @param {Object} options * @memberof Store */
  commit(type, payload, options) { ... }

  // 分發 action
  dispatch(type, payload) { ... }

  subscribe(fn) { ... }

  watch(getter, cb, options) { ... }

  // 替換 State
  replaceState(state) { ... }

  // 註冊模塊
  registerModule(path, module) { ... }

  // 註銷模塊
  unregisterModule(path) { ... }

  hotUpdate(newOptions) { ... }

  // 提交 mutation。
  _withCommit(fn) {
    // 存儲當前提交狀態
    const committing = this._committing
    // 置爲提交狀態
    this._committing = true
    // 調用更改狀態函數
    fn()
    // 把提交狀態置回原來的狀態
    this._committing = committing
  }
}

複製代碼

簡單分析梳理:緩存

  • constructor:

    • 首先在構造函數內部定義了一些屬性(這裏註釋比較清楚)
    • 執行了一些初始化 installModuleresetStoreVM 等方法,下面將着重看一下其內部實現。
  • state 定義了取值函數(getter)和存值函數(setter),作一層代理(防止意外的修改)。

  • 定義了一些實例方法 commitdispatch 等(以後根據實際調用,具體分析)。

具體實現:

installModule - 安裝模塊

init root 模塊。這還遞歸地註冊全部子模塊,並在 this._wrappedgechers 中收集全部模塊 getter

/** * 安裝模塊 * * @param {Object} store Store 實例 * @param {Object | Function} rootState Vuex store 實例的根 state 對象 * @param {Array} path * @param {Object} module Vuex.Store 構造器選項 * @param {*} hot */
function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length // 是不是根仍是模塊
  // 從 Vuex.Store 構造器選項解構出相關選項
  const {
    state, // Vuex store 實例的根 state 對象。
    actions, // 在 store 上註冊 action。處理函數老是接受 context 做爲第一個參數,payload 做爲第二個參數(可選)。
    mutations, // 在 store 上註冊 mutation,處理函數老是接受 state 做爲第一個參數(若是定義在模塊中,則爲模塊的局部狀態),payload 做爲第二個參數(可選)。
    getters, // 在 store 上註冊 getter,getter 方法接受如下參數: state, // 若是在模塊中定義則爲模塊的局部狀態. getters, // 等同於 store.getters.
    modules  // 包含了子模塊的對象,會被合併到 store
  } = module

  // 設置 module 的 state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    // 爲根 State 添加模塊的 state
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
    })
  }

  // 若存在 mutations 構造器選項 則將其所有選項註冊
  if (mutations) {
    Object.keys(mutations).forEach(key => {
      registerMutation(store, key, mutations[key], path)
    })
  }

  // 註冊 action
  if (actions) {
    Object.keys(actions).forEach(key => {
      registerAction(store, key, actions[key], path)
    })
  }

  // 包裝 getters
  if (getters) {
    wrapGetters(store, getters, path)
  }

  // 安裝模塊
  if (modules) {
    Object.keys(modules).forEach(key => {
      // 遞歸調用註冊每個模塊
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}
複製代碼

由上述初始化調用 installModule(this, state, [], options) 可知其入參,下面就看看各個選項註冊的代碼實現。

MutationActiongetter 註冊

/** * 註冊 mutations 構造器選項 * * @param {*} store * @param {*} type * @param {*} handler * @param {*} [path=[]] */
function registerMutation(store, type, handler, path = []) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler(payload) {
    handler(getNestedState(store.state, path), payload)
  })
}

/** * 註冊 action * * @param {Object} store * @param {String} type * @param {Function} handler * @param {Array} [path=[]] */
function registerAction(store, type, handler, path = []) {
  const entry = store._actions[type] || (store._actions[type] = [])
  const { dispatch, commit } = store
  entry.push(function wrappedActionHandler(payload, cb) {
    // 注意這裏透傳的context: 不是 store 實例自己。
      dispatch,
      commit,
      getters: store.getters,
      state: getNestedState(store.state, path),
      rootState: store.state
    }, payload, cb)
    // 判斷是不是 promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    // 處理應用的 devtools 插件
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

/** * 包裝 getters * * @param {Object} store * @param {Object} moduleGetters * @param {Array modulePath} */
function wrapGetters(store, moduleGetters, modulePath) {
  Object.keys(moduleGetters).forEach(getterKey => {
    const rawGetter = moduleGetters[getterKey]
    if (store._wrappedGetters[getterKey]) {
      console.error(`[vuex] 重複的getter關鍵: ${getterKey}`)
      return
    }
    store._wrappedGetters[getterKey] = function wrappedGetter(store) {
      return rawGetter(
        getNestedState(store.state, modulePath), // local state
        store.getters, // getters
        store.state // root state
      )
    }
  })
}

/** * 獲取嵌套的 state * * @param {Object} state * @param {Array} path * @returns */
function getNestedState(state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

複製代碼
  • 上述代碼實現邏輯比較清晰,就是把註冊信息添加到 _mutations_actions_wrappedGetters 統一管理。

  • 若存在模塊則會將其state添加到 root state 中。

  • 上述基礎示例,最終安裝結果以下:

    _mutations: {
        decrement: [
          ƒ wrappedMutationHandler(payload)
        ],
        increment: [
          ƒ wrappedMutationHandler(payload)
        ],
        moduleIncrement: [
          ƒ wrappedMutationHandler(payload)
        ]
      }
    
      _actions: {
        decrement: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        increment: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        incrementAsync: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        incrementIfOdd: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        moduleIncrement: [
          ƒ wrappedActionHandler(payload, cb)
        ]
      }
    
      _wrappedGetters: {
        evenOrOdd: ƒ wrappedGetter(store),
        moduleCountPlus: ƒ wrappedGetter(store)
      }
    
      // root state
      state: {
        count: 0,
        moduleDemo: {
          moduleCount: 1
        }
      }
    
    複製代碼

resetStoreVM - 重置 Store 上 Vue 實例

/** * 重置 Store 上 Vue 實例 * * @param {*} store * @param {*} state */
function resetStoreVM(store, state) {
  // 取以前的 vue 實例
  const oldVm = store._vm

  // 綁定存儲公共 getter
  store.getters = {}
  // 獲取 Store 中全部 getter
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 代理取值函數
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    // 利用 computed 的延遲緩存機制
    computed[key] = () => fn(store)
    // 在公共 getter 上定義以前合併的 getter,並作一層取值代理,實際上取得是計算屬性定義的 key 值。
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })

  // 使用 Vue 實例存儲狀態樹抑制警告,以防用戶添加了一些 funky global mixins
  const silent = Vue.config.silent
  // 關閉 Vue 內部的警告
  Vue.config.silent = true
  // 添加 _vm 屬性,值爲 Vue 實例
  store._vm = new Vue({
    data: { state },
    computed
  })
  // 開啓 Vue 內部的警告
  Vue.config.silent = silent

  // 啓用嚴格模式 for new vm
  // 嚴格模式下在非提交的狀況下修改 state,拋出錯誤。
  if (store.strict) {
    enableStrictMode(store)
  }

  // 若存在以前的Vue實例
  if (oldVm) {
    // 在全部訂閱的觀察者中分派更改,以強制 getter 從新評估。
    store._withCommit(() => {
      oldVm.state = null
    })
    // 在下個更新隊列以後銷燬以前的 Vue 實例
    Vue.nextTick(() => oldVm.$destroy())
  }
}

/** * 啓用嚴格模式 * * @param {Object} store * @returns {void} * 注:使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤。 */
function enableStrictMode(store) {
  store._vm.$watch('state', () => {
    assert(store._committing, `不要在 mutation 處理程序以外對 vuex 存儲狀態進行改變;更改 Vuex 的 store 中的狀態的惟一方法是提交 mutation。`)
  }, { deep: true, sync: true })
}

複製代碼

在講解具體用例前,先來看看 dispatchcommit 的代碼實現:

dispatchcommit - 調度和提交

/** * 更改 Vuex 的 store 中的狀態的惟一方法是提交 mutation。 * 如:store.commit('increment') * 每一個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。 * 這個回調函數就是咱們實際進行狀態更改的地方,而且它會接受 state 做爲第一個參數 * * @param {*} type * @param {*} payload * @param {*} options * @memberof Store */
  commit(type, payload, options) {
    // 檢查對象樣式提交 如:
    // store.commit({ type: 'increment', amount: 10 })
    if (isObject(type) && type.type) {
      options = payload
      payload = type // 使用對象風格的提交方式,整個對象都做爲載荷傳給 mutation 函數
      type = type.type
    }
    const mutation = { type, payload }
    const entry = this._mutations[type] // 查找 mutation
    // 若不存在則拋出錯誤
    if (!entry) {
      console.error(`[vuex] 未知 mutation 類型: ${type}`)
      return
    }
    // 提交 mutation
    this._withCommit(() => {
      entry.forEach(function commitIterator(handler) {
        handler(payload)
      })
    })
    // 若知足該條件,則:調用訂閱池內全部的訂閱函數
    if (!options || !options.silent) {
      this._subscribers.forEach(sub => sub(mutation, this.state))
    }
  }

  /** * 分發 action * * @param {*} type * @param {*} payload * @returns {Promise} 解析全部被觸發的 action 處理器的 Promise * @memberof Store */
  dispatch(type, payload) {
    // check object-style dispatch
    // 同上解釋
    if (isObject(type) && type.type) {
      payload = type
      type = type.type
    }
    const entry = this._actions[type]
    if (!entry) {
      console.error(`[vuex] 未知 action 類型: ${type}`)
      return
    }
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  }
複製代碼
  • 講到這裏整個流程就已經分析的差很少了。

  • 這裏順便提一下:Mutation 必須是同步函數

    • 如果異步這會使 devtool 中的 mutation 日誌變得不可追蹤。【參閱】
  • 如下用例演示了從 dispatch(調度) action其內部觸發 commit(提交) 進而調用 mutation 狀態修改函數, 來達到更新狀態。至關清晰👍

    <template>
      <div id="app">
        <div class="root">
          Clicked: {{ $store.state.count }} times, count is {{ $store.getters.evenOrOdd }}.
          <button @click="$store.dispatch('increment')">+</button>
        </div>
        <div class="module">
          Clicked: {{ $store.state.moduleDemo.moduleCount }} times
          <button @click="$store.dispatch('moduleIncrement')">+</button>
        </div>
      </div>
    </template>
    複製代碼

    點擊 「+」 調度actions內部對應的處理函數,其內部去提交狀態改變(相似分發事件)在 mutations 內部去執行響應的函數,真正改變狀態。狀態的改變,致使依賴這些狀態的組件更新。「Clicked: 1」

    const state = {
      count: 0
    }
    
    const actions = {
      increment: ({ commit }) => commit('increment'),
      ...
    }
    
    const mutations = {
      increment (state) {
        state.count++
      },
      ...
    }
    
    const moduleDemo = {
      state: { moduleCount: 1 },
      mutations: {
        moduleIncrement(state) {
          state.moduleCount++
        },
      },
      actions: {
        moduleIncrement: ({ commit }) => commit('moduleIncrement'),
      },
      ...
    }
    
    複製代碼

其它細節

接下來咱們就來看看咱們在組件中常用的輔助函數實現如:

import {
  mapActions,
  mapActions,
  mapMutations,
  mapGetters
} from "vuex";

export default {
  computed: {
    ...mapState([
      'count' // 映射 this.count 爲 this.$store.state.count
    ]), // 或 ...mapState({ count: state => state.count })

    ...mapGetters(["evenOrOdd"]),
  },
  methods: {
    // 必須同步提交
    ...mapMutations([
      'increment', // 將 `this.increment()` 映射爲 `this.$store.commit('increment')`
      // `mapMutations` 也支持載荷:
      'decrement' // 將 `this.decrement(amount)` 映射爲 `this.$store.commit('decrement', amount)`
    ]),
    // 處理異步
    ...mapActions([
      "increment",
      "decrement",
      "incrementIfOdd",
      "incrementAsync"
    ]),
  }
};

複製代碼

mapState

/** * state 映射處理函數 * * @export * @param {Array | Object} states * @returns {Object} */
export function mapState (states) {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      return typeof val === 'function'
        ? val.call(this, this.$store.state, this.$store.getters)
        : this.$store.state[val]
    }
  })
  return res
}

/** * 規範參數類型 * * @param {*} map * @returns {Array} */
function normalizeMap(map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
複製代碼
  • 咱們能看出來,這些輔助函數,主要是作了一層映射,」解決重複和冗餘,讓你少按幾回鍵「。
  • 這裏只是提取了一個進行講解,其它思路差很少,這裏就很少說了。
  • 最後,本文着重點放在貫通流程,及經常使用實現,裏面還有許多細節沒有說起。
  • 如有興趣請參閱 vuex 。建議將其代碼拉下來,根據其用例,本地跑起來,斷點去調式,結合文檔慢慢去看,相信必定收穫巨多!

參閱

相關文章
相關標籤/搜索