Vuex 2.0 學(xué)習(xí)筆記(二):源碼閱讀

上一章總結(jié)了 Vuex 的框架原理,這一章我們將從 Vuex 的入口文件開(kāi)始,分步驟閱讀和解析源碼。由于 Vuex 的源碼是基于 Vue 和 ES6 的,因此這兩部分不熟悉的小伙伴可以先去熟悉一下它們的基礎(chǔ)知識(shí),這對(duì)理解 Vuex 源碼的實(shí)現(xiàn)原理幫助是很大的。

在閱讀源碼之前,我們需要到 Vuex 的 Github 地址下載源碼及示例代碼:

git clone https://github.com/vuejs/vuex.git

1. 目錄結(jié)構(gòu)介紹

雖然下載下來(lái)的代碼有很多個(gè)文件夾,但是源碼只被存在./src目錄下,具體的源碼目錄結(jié)構(gòu)如下圖:

Vuex 源碼目錄結(jié)構(gòu)

Vuex提供了非常強(qiáng)大的狀態(tài)管理功能,源碼代碼量卻不多,目錄結(jié)構(gòu)劃分也很清晰。先大體介紹下各個(gè)目錄文件的功能:

  • module:提供 module 對(duì)象與 module 對(duì)象樹(shù)的創(chuàng)建功能;
  • plugins:提供開(kāi)發(fā)輔助插件,如 “時(shí)光穿梭” 功能,state 修改的日志記錄功能等;
  • helpers.js:提供 action、mutations 以及 getters 的查找 API;
  • index.js:是源碼主入口文件,提供 store 的各 module 構(gòu)建安裝;
  • mixin.js:提供了 store 在 Vue 實(shí)例上的裝載注入;
  • util.js:提供了工具方法如 find、deepCopy、forEachValue 以及 assert 等方法。

2. 初始化裝載與注入

在基本了解了源碼的目錄結(jié)構(gòu)之后,就可以開(kāi)始源碼的閱讀了。我們先從入口文件index.js開(kāi)始入手,index.js中包含了所有的核心代碼的引用。

2.1 裝載實(shí)例

在將 Vuex 注入到一個(gè) Vue 項(xiàng)目時(shí),我們經(jīng)常會(huì)在項(xiàng)目的store.js文件中加載 Vuex 框架,并且創(chuàng)建出一個(gè) store 對(duì)象實(shí)例:

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

Vue.use(Vuex)

export default new Vuex.Store()

然后在index.js中,正常初始化一個(gè)頁(yè)面根級(jí)別的 Vue 組件,傳入這個(gè)自定義的 store 對(duì)象:

import Vue from 'vue'
import App from './../pages/app.vue'
import store from './store.js'

new Vue({
  el: '#root',
  router,
  store, 
  render: h => h(App)
})

那么問(wèn)題來(lái)了:使用 Vuex 只需執(zhí)行Vue.use(Vuex),并在Vue的配置中傳入一個(gè) store 對(duì)象的示例,store 是如何實(shí)現(xiàn)注入的 ?

2.2 裝載分析

store.js文件的代碼開(kāi)頭中,let Vue語(yǔ)句定義了局部 Vue 變量,用于判斷是否已經(jīng)裝載和減少全局作用域查找。

接著判斷是否已經(jīng)加載過(guò) Vue 對(duì)象,如果處于瀏覽器環(huán)境下且加載過(guò) Vue,則執(zhí)行 install 方法。install 方法將 Vuex 裝載到 Vue 對(duì)象上,Vue.use(Vuex)也是通過(guò)它執(zhí)行:

if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)  
}

若是首次加載,將局部 Vue 變量賦值為全局的 Vue 對(duì)象,并執(zhí)行mixin.js文件中定義的 applyMixin 方法:

function install (_Vue) {
  if (Vue) { //保證install方法只執(zhí)行一次
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue) // => ./minxin.js
}

點(diǎn)開(kāi)mixin.js文件,看一下 applyMixin 方法內(nèi)部代碼,由于 Vue 2.0 以后引入了一些生命鉤子,因此這里也對(duì) Vue 的版本進(jìn)行了判斷:如果是 2.x.x 以上版本,可以使用 hook 的形式進(jìn)行注入,或使用封裝并替換 Vue 對(duì)象原型的 _init 方法,實(shí)現(xiàn)注入:

const version = Number(Vue.version.split('.')[0])

if (version >= 2) {
  Vue.mixin({ beforeCreate: vuexInit })
} else {
  const _init = Vue.prototype._init
  Vue.prototype._init = function (options = {}) {
    options.init = options.init
      ? [vuexInit].concat(options.init)
      : vuexInit
    _init.call(this, options)
  }
}

我們?cè)倏匆幌戮唧w的注入方法:

// 初始化鉤子前插入一段 Vuex 初始化代碼 => 給 Vue 實(shí)例注入一個(gè) $store 屬性 => this.$store.xxx
function vuexInit () {
  const options = this.$options
  if (options.store) {
    this.$store = options.store
  } else if (options.parent && options.parent.$store) {
    this.$store = options.parent.$store
  }
}

從代碼中可以看出來(lái),vuexInit()方法將初始化 Vue 根組件時(shí)傳入的 store 設(shè)置到 this 對(duì)象的 $store 屬性上,子組件從其父組件引用 $store 屬性,層層嵌套進(jìn)行設(shè)置。在任意組件中執(zhí)行 this.$store 都能找到裝載的那個(gè) store 對(duì)象。

如果覺(jué)得這么說(shuō)看起來(lái)有些抽象,那就畫個(gè)對(duì)應(yīng) store 的流向圖吧:

store 流向圖

3. store 對(duì)象構(gòu)造

上面對(duì)Vuex框架的裝載以及注入自定義store對(duì)象進(jìn)行分析,接下來(lái)詳細(xì)分析store對(duì)象的內(nèi)部功能和具體實(shí)現(xiàn)。

3.1 環(huán)境判斷

開(kāi)始分析store的構(gòu)造函數(shù),分小節(jié)逐函數(shù)逐行的分析其功能。

assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

通過(guò)這兩行的判斷語(yǔ)句,我們可以看出在 store 構(gòu)造函數(shù)中執(zhí)行環(huán)境判斷,以下都是Vuex工作的必要條件:

  • 已經(jīng)執(zhí)行安裝函數(shù)進(jìn)行裝載;
  • 支持Promise語(yǔ)法。

assert 函數(shù)是一個(gè)簡(jiǎn)單的斷言函數(shù)的實(shí)現(xiàn),類似于console.assert(),它的具體實(shí)現(xiàn)在util.js文件中:

function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}

3.2 數(shù)據(jù)初始化、module 樹(shù)的構(gòu)造

環(huán)境判斷后,根據(jù)new構(gòu)造傳入的options或默認(rèn)值,初始化內(nèi)部數(shù)據(jù)。

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

// store internal state
this._committing = false // 是否在進(jìn)行提交狀態(tài)標(biāo)識(shí)
this._actions = Object.create(null) // acitons操作對(duì)象
this._mutations = Object.create(null) // mutations操作對(duì)象
this._wrappedGetters = Object.create(null) // 封裝后的getters集合對(duì)象
this._modules = new ModuleCollection(options) // Vuex支持store分模塊傳入,存儲(chǔ)分析后的modules
this._modulesNamespaceMap = Object.create(null) // 模塊命名空間map
this._subscribers = [] // 訂閱函數(shù)集合,Vuex提供了subscribe功能
this._watcherVM = new Vue() // Vue組件用于watch監(jiān)視變化

調(diào)用new Vuex.store(options)時(shí)傳入的 options 對(duì)象,用于構(gòu)造 ModuleCollection 類,下面看看其功能:

this.root = new Module(rawRootModule, false)

if (rawRootModule.modules) {
  forEachValue(rawRootModule.modules, (rawModule, key) => {
    this.register([key], rawModule, false)
  })
}

ModuleCollection 主要將傳入的 options 對(duì)象整個(gè)構(gòu)造為一個(gè)module對(duì)象,并循環(huán)調(diào)用this.register([key], rawModule, false)為其中的 modules 屬性進(jìn)行模塊注冊(cè),使其都成為 module 對(duì)象,最后 options 對(duì)象被構(gòu)造成一個(gè)完整的組件樹(shù)。ModuleCollection 類還提供了modules的更替功能,詳細(xì)實(shí)現(xiàn)可以查看源文件module-collection.js

3.3 dispatch與commit設(shè)置

在 store 的構(gòu)造函數(shù)中:

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

封裝替換原型中的 dispatch 和 commit 方法,將 this 指向當(dāng)前 store 對(duì)象。dispatch 和 commit 方法具體實(shí)現(xiàn)如下:

dispatch (_type, _payload) {
  // check object-style dispatch
  const {
      type,
      payload
  } = unifyObjectStyle(_type, _payload) // 配置參數(shù)處理

  // 當(dāng)前type下所有action處理函數(shù)集合
  const entry = this._actions[type]
  if (!entry) {
    console.error(`[vuex] unknown action type: ${type}`)
    return
  }
  return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
}

前面提到,dispatch 的功能是觸發(fā)并傳遞一些參數(shù)(payload)給對(duì)應(yīng) type 的 action。因?yàn)槠渲С?2 種調(diào)用方法,所以在 dispatch 中,先進(jìn)行參數(shù)的適配處理,然后判斷 action type 是否存在,若存在就逐個(gè)執(zhí)行(注:上面代碼中的this._actions[type]以及 下面的this._mutations[type]均是處理過(guò)的函數(shù)集合,具體內(nèi)容留到后面進(jìn)行分析)。

commit方法和dispatch相比雖然都是觸發(fā)type,但是對(duì)應(yīng)的處理卻相對(duì)復(fù)雜。

commit (_type, _payload, _options) {
  // check object-style commit
  const {
      type,
      payload,
      options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type]
  if (!entry) {
    console.error(`[vuex] unknown mutation type: ${type}`)
    return
  }
  // 專用修改state方法,其他修改state方法均是非法修改
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  // 訂閱者函數(shù)遍歷執(zhí)行,傳入當(dāng)前的mutation對(duì)象和當(dāng)前的state
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if (options && options.silent) {
    console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
    )
  }
}

該方法同樣支持 2 種調(diào)用方法。先進(jìn)行參數(shù)適配,判斷觸發(fā) mutation type,利用 _withCommit 方法執(zhí)行本次批量觸發(fā) mutation 處理函數(shù),并傳入 payload 參數(shù)。執(zhí)行完成后,通知所有 _subscribers(訂閱函數(shù))本次操作的 mutation 對(duì)象以及當(dāng)前的 state 狀態(tài),如果傳入了已經(jīng)移除的 silent 選項(xiàng)則進(jìn)行提示警告。

3.4 state 修改方法

_withCommit是一個(gè)代理方法,所有觸發(fā)mutation的進(jìn)行state修改的操作都經(jīng)過(guò)它,由此來(lái)統(tǒng)一管理監(jiān)控state狀態(tài)的修改。

_withCommit (fn) {
  // 保存之前的提交狀態(tài)
  const committing = this._committing

  // 進(jìn)行本次提交,若不設(shè)置為true,直接修改state,strict模式下,Vuex將會(huì)產(chǎn)生非法修改state的警告
  this._committing = true

  // 執(zhí)行state的修改操作
  fn()

  // 修改完成,還原本次修改之前的狀態(tài)
  this._committing = committing
}

緩存執(zhí)行時(shí)的 committing 狀態(tài)將當(dāng)前狀態(tài)設(shè)置為 true 后進(jìn)行本次提交操作,待操作完畢后,將 committing 狀態(tài)還原為之前的狀態(tài)。

3.5 module 安裝

綁定 dispatch 和 commit 方法之后,進(jìn)行嚴(yán)格模式的設(shè)置,以及模塊的安裝(installModule)。由于占用資源較多影響頁(yè)面性能,嚴(yán)格模式建議只在開(kāi)發(fā)模式開(kāi)啟,上線后需要關(guān)閉。

// strict mode
this.strict = strict

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

3.5.1 初始化 rootState

上述代碼的備注中,提到installModule方法初始化組件樹(shù)根組件、注冊(cè)所有子組件,并將其中所有的getters存儲(chǔ)到this._wrappedGetters屬性中。

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

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

  // 非根組件設(shè)置 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)
    })
  }
  ...
}

判斷是否是根目錄,以及是否設(shè)置了命名空間,若存在則在 namespace 中進(jìn)行 module 的存儲(chǔ),在不是根組件且不是 hot 條件的情況下,通過(guò) getNestedState 方法拿到該 module 父級(jí)的 state,拿到其所在的 moduleName ,調(diào)用Vue.set(parentState, moduleName, module.state)方法將其state設(shè)置到父級(jí) state 對(duì)象的 moduleName 屬性中,由此實(shí)現(xiàn)該模塊的 state 注冊(cè)(首次執(zhí)行這里,因?yàn)槭歉夸涀?cè),所以并不會(huì)執(zhí)行該條件中的方法)。getNestedState 方法代碼很簡(jiǎn)單,分析 path 拿到 state。

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

3.5.2 module 上下文環(huán)境設(shè)置

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

命名空間和根目錄條件判斷完畢后,接下來(lái)定義 local 變量和 module.context 的值,執(zhí)行 makeLocalContext 方法,為該 module 設(shè)置局部的 dispatch、commit 方法以及 getters 和 state(由于 namespace 的存在需要做兼容處理)。

3.5.3 mutations、actions 以及 getters 注冊(cè)

定義local環(huán)境后,循環(huán)注冊(cè)我們?cè)趏ptions中配置的action以及mutation等。逐個(gè)分析各注冊(cè)函數(shù)之前,先看下模塊間的邏輯關(guān)系流程圖:

模塊間的邏輯關(guān)系流程圖

下面分析代碼邏輯:

// 注冊(cè)對(duì)應(yīng)模塊的mutation,供state修改使用
module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})

// 注冊(cè)對(duì)應(yīng)模塊的action,供數(shù)據(jù)操作、提交mutation等異步操作使用
module.forEachAction((action, key) => {
  const namespacedType = namespace + key
  registerAction(store, namespacedType, action, local)
})

// 注冊(cè)對(duì)應(yīng)模塊的getters,供state讀取使用
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})

registerMutation 方法中,獲取 store 中的對(duì)應(yīng) mutation type 的處理函數(shù)集合,將新的處理函數(shù) push 進(jìn)去。這里將我們?cè)O(shè)置在 mutations type 上對(duì)應(yīng)的 handler 進(jìn)行了封裝,給原函數(shù)傳入了state。在執(zhí)行commit('xxx', payload)的時(shí)候,type 為 xxx 的 mutation 的所有 handler 都會(huì)接收到 state 以及 payload,這就是在 handler 里面拿到 state 的原因。

function registerMutation (store, type, handler, local) {
  // 取出對(duì)應(yīng)type的mutations-handler集合
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // commit實(shí)際調(diào)用的不是我們傳入的handler,而是經(jīng)過(guò)封裝的
  entry.push(function wrappedMutationHandler (payload) {
    // 調(diào)用handler并將state傳入
    handler(local.state, payload)
  })
}

action 和 getter 的注冊(cè)也是同理的,看一下代碼(注:前面提到的this.actions以及this.mutations在此處進(jìn)行設(shè)置)。

function registerAction (store, type, handler, local) {
  // 取出對(duì)應(yīng)type的actions-handler集合
  const entry = store._actions[type] || (store._actions[type] = [])
  // 存儲(chǔ)新的封裝過(guò)的action-handler
  entry.push(function wrappedActionHandler (payload, cb) {
    // 傳入 state 等對(duì)象供我們?cè)璦ction-handler使用
    let res = handler({
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    // action需要支持promise進(jìn)行鏈?zhǔn)秸{(diào)用,這里進(jìn)行兼容處理
    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
    }
  })
}

function registerGetter (store, type, rawGetter, local) {
  // getters只允許存在一個(gè)處理函數(shù),若重復(fù)需要報(bào)錯(cuò)
  if (store._wrappedGetters[type]) {
    console.error(`[vuex] duplicate getter key: ${type}`)
    return
  }

  // 存儲(chǔ)封裝過(guò)的getters處理函數(shù)
  store._wrappedGetters[type] = function wrappedGetter (store) {
    // 為原getters傳入對(duì)應(yīng)狀態(tài)
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

action handler 比 mutation handler 以及 getter wrapper 多拿到 dispatch 和 commit 操作方法,因此 action 可以進(jìn)行 dispatch action 和 commit mutation 操作。

3.5.4 子 module 安裝

注冊(cè)完了根組件的 actions、mutations 以及 getters 后,遞歸調(diào)用自身,為子組件注冊(cè)其 state,actions、mutations 以及 getters 等。

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

3.5.5 實(shí)例

前面介紹了 dispatch 和 commit 方法以及 actions 等的實(shí)現(xiàn),下面結(jié)合一個(gè)官方的購(gòu)物車實(shí)例中的部分代碼來(lái)加深理解。
Vuex 配置代碼:

/
 *  store-index.js store配置文件
 *
 /

import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import cart from './modules/cart'
import products from './modules/products'
import createLogger from '../../../src/plugins/logger'

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== 'production'

export default new Vuex.Store({
  actions,
  getters,
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

Vuex 組件 module 中各模塊 state 配置代碼部分:

/**
 *  cart.js
 *
 **/

const state = {
  added: [],
  checkoutStatus: null
}

/**
 *  products.js
 *
 **/

const state = {
  all: []
}

加載上述配置后,頁(yè)面 state 結(jié)構(gòu)如下圖:

state 中的屬性配置都是按照 option 配置中 module path 的規(guī)則來(lái)進(jìn)行的,下面看 action 的操作實(shí)例:

Vuecart 組件代碼部分:

/**
 *  Cart.vue 省略template代碼,只看script部分
 *
 **/

export default {
  methods: {
    // 購(gòu)物車中的購(gòu)買按鈕,點(diǎn)擊后會(huì)觸發(fā)結(jié)算。源碼中會(huì)調(diào)用 dispatch方法
    checkout (products) {
      this.$store.dispatch('checkout', products)
    }
  }
}

Vuexcart.js 組件 action 配置代碼部分:

const actions = {
  checkout ({ commit, state }, products) {
    const savedCartItems = [...state.added] // 存儲(chǔ)添加到購(gòu)物車的商品
    commit(types.CHECKOUT_REQUEST) // 設(shè)置提交結(jié)算狀態(tài)
    shop.buyProducts( // 提交api請(qǐng)求,并傳入成功與失敗的cb-func
      products,
      () => commit(types.CHECKOUT_SUCCESS), // 請(qǐng)求返回成功則設(shè)置提交成功狀態(tài)
      () => commit(types.CHECKOUT_FAILURE, { savedCartItems }) // 請(qǐng)求返回失敗則設(shè)置提交失敗狀態(tài)
    )
  }
}

Vue 組件中點(diǎn)擊購(gòu)買執(zhí)行當(dāng)前 module 的 dispatch 方法,傳入 type 值為 'checkout',payload 值為 'products',在源碼中 dispatch 方法在所有注冊(cè)過(guò)的 actions 中查找 'checkout' 的對(duì)應(yīng)執(zhí)行數(shù)組,取出循環(huán)執(zhí)行。執(zhí)行的是被封裝過(guò)的被命名為 wrappedActionHandler 的方法,真正傳入的 checkout 的執(zhí)行函數(shù)在 wrappedActionHandler 這個(gè)方法中被執(zhí)行,源碼如下(注:前面貼過(guò),這里再看一次):

function wrappedActionHandler (payload, cb) {
    let res = handler({
      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
    }
  }
  ...
}

handler 在這里就是傳入的 checkout 函數(shù),其執(zhí)行需要的 commit 以及state 就是在這里被傳入,payload 也傳入了,在實(shí)例中對(duì)應(yīng)接收的參數(shù)名為 products。commit 的執(zhí)行也是同理的,實(shí)例中 checkout 還進(jìn)行了一次 commit 操作,提交一次 type 值為 types.CHECKOUT_REQUEST 的修改,因?yàn)?mutation 名字是唯一的,這里進(jìn)行了常量形式的調(diào)用,防止命名重復(fù),執(zhí)行跟源碼分析中一致,調(diào)用function wrappedMutationHandler (payload) { handler(local.state, payload) }封裝函數(shù)來(lái)實(shí)際調(diào)用配置的 mutation 方法。

看到完源碼分析和上面的小實(shí)例,應(yīng)該能理解 dispatch action 和 commit mutation 的工作原理了。接著看源碼,看看 getters 是如何實(shí)現(xiàn) state 實(shí)時(shí)訪問(wèn)的。

3.6 store._vm 組件設(shè)置

執(zhí)行完各 module 的 install 后,執(zhí)行 resetStoreVM 方法,進(jìn)行 store 組件的初始化。

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

resetStoreVM(this, state)

綜合前面的分析可以了解到,Vuex 其實(shí)構(gòu)建的就是一個(gè)名為 store 的 vm 組件,所有配置的 state、actions、mutations 以及 getters 都是其組件的屬性,所有的操作都是對(duì)這個(gè) vm 組件進(jìn)行的。

一起看下resetStoreVM方法的內(nèi)部實(shí)現(xiàn)。

function resetStoreVM (store, state) {
  const oldVm = store._vm // 緩存前vm組件

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  // 循環(huán)所有處理過(guò)的getters,并新建computed對(duì)象進(jìn)行存儲(chǔ),通過(guò)Object.defineProperty方法為getters對(duì)象建立屬性,使得我們通過(guò)this.$store.getters.xxxgetter能夠訪問(wèn)到該getters
  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

  // 暫時(shí)將Vue設(shè)為靜默模式,避免報(bào)出用戶加載的某些插件觸發(fā)的警告
  Vue.config.silent = true   
  // 設(shè)置新的storeVm,將當(dāng)前初始化的state以及getters作為computed屬性(剛剛遍歷生成的)
  store._vm = new Vue({
    data: { state },
    computed
  })

  // 恢復(fù)Vue的模式
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    // 該方法對(duì)state執(zhí)行$watch以禁止從mutation外部修改state
    enableStrictMode(store)
  }

  // 若不是初始化過(guò)程執(zhí)行的該方法,將舊的組件state設(shè)置為null,強(qiáng)制更新所有監(jiān)聽(tīng)者(watchers),待更新生效,DOM更新完成后,執(zhí)行vm組件的destroy方法進(jìn)行銷毀,減少內(nèi)存的占用
  if (oldVm) {
    // dispatch changes in all subscribed watchers
    // to force getter re-evaluation.
    store._withCommit(() => {
      oldVm.state = null
    })
    Vue.nextTick(() => oldVm.$destroy())
  }
}

resetStoreVm 方法創(chuàng)建了當(dāng)前 store 實(shí)例的 _vm 組件,至此 store 就創(chuàng)建完畢了。上面代碼涉及到了嚴(yán)格模式的判斷,看一下嚴(yán)格模式如何實(shí)現(xiàn)的。

function enableStrictMode (store) {
  store._vm.$watch('state', () => {
    assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
  }, { deep: true, sync: true })
}

很簡(jiǎn)單的應(yīng)用,監(jiān)視 state 的變化,如果沒(méi)有通過(guò)this._withCommit()方法進(jìn)行 state 修改,則報(bào)錯(cuò)。

3.7 plugin注入

最后執(zhí)行plugin的植入:

plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))

devtoolPlugin 提供的功能有 3 個(gè):

// 1. 觸發(fā)Vuex組件初始化的hook
devtoolHook.emit('vuex:init', store)

// 2. 提供“時(shí)空穿梭”功能,即state操作的前進(jìn)和倒退
devtoolHook.on('vuex:travel-to-state', targetState => {
  store.replaceState(targetState)
})

// 3. mutation被執(zhí)行時(shí),觸發(fā)hook,并提供被觸發(fā)的mutation函數(shù)和當(dāng)前的state狀態(tài)
store.subscribe((mutation, state) => {
  devtoolHook.emit('vuex:mutation', mutation, state)
})

源碼分析到這里,Vuex 框架的實(shí)現(xiàn)原理基本都已經(jīng)分析完畢。

總結(jié)

講道理,讀源碼是個(gè)比較痛苦的過(guò)程,不過(guò)在通讀源碼之后,也會(huì)有豁然開(kāi)朗的感覺(jué)。我在這里放五個(gè)美團(tuán)點(diǎn)評(píng)團(tuán)隊(duì)關(guān)于 Vuex 的問(wèn)題思考,希望能起到一個(gè)總結(jié)的作用吧。

  1. 問(wèn):使用 Vuex 只需執(zhí)行Vue.use(Vuex),并在Vue的配置中傳入一個(gè) store 對(duì)象的示例,store 是如何實(shí)現(xiàn)注入的?

Vue.use(Vuex)方法執(zhí)行的是 install 方法,它實(shí)現(xiàn)了 Vue 實(shí)例對(duì)象的 init 方法封裝和注入,使傳入的 store 對(duì)象被設(shè)置到 Vue 上下文環(huán)境的 $store 中。因此在 Vue Component 任意地方都能夠通過(guò)this.$store訪問(wèn)到該 store。

  1. 問(wèn):state 內(nèi)部支持模塊配置和模塊嵌套,如何實(shí)現(xiàn)的?

:在 store 構(gòu)造方法中有 makeLocalContext 方法,所有 module 都會(huì)有一個(gè) local context,根據(jù)配置時(shí)的 path 進(jìn)行匹配。所以執(zhí)行如dispatch('submitOrder', payload)這類 action 時(shí),默認(rèn)的拿到都是 module的 local state,如果要訪問(wèn)最外層或者是其他 module 的 state,只能從 rootState 按照 path 路徑逐步進(jìn)行訪問(wèn)。

  1. 問(wèn):在執(zhí)行 dispatch 觸發(fā) action (commit 同理)的時(shí)候,只需傳入 (type, payload),action 執(zhí)行函數(shù)中第一個(gè)參數(shù) store 從哪里獲取的?

:store 初始化時(shí),所有配置的 action 和 mutation 以及 getters 均被封裝過(guò)。在執(zhí)行如dispatch('submitOrder', payload)的時(shí)候, actions 中 type 為 submitOrder 的所有處理方法都是被封裝后的,其第一個(gè)參數(shù)為當(dāng)前的 store 對(duì)象,所以能夠獲取到{ dispatch, commit, state, rootState }等數(shù)據(jù)。

  1. 問(wèn):Vuex 如何區(qū)分 state 是外部直接修改,還是通過(guò) mutation 方法修改的?

:Vuex 中修改 state 的唯一渠道就是執(zhí)行commit('xx', payload)方法,其底層通過(guò)執(zhí)行this._withCommit(fn)設(shè)置 _committing 標(biāo)志變量為 true,然后才能修改 state,修改完畢還需要還原 _committing 變量。外部修改雖然能夠直接修改 state,但是并沒(méi)有修改 _committing 標(biāo)志位,所以只要 watch 一下 state,state change 時(shí)判斷是否 _committing 值為 true,即可判斷修改的合法性。

  1. 問(wèn):調(diào)試時(shí)的 "時(shí)空穿梭" 功能是如何實(shí)現(xiàn)的?

:devtoolPlugin 中提供了此功能。因?yàn)?dev 模式下所有的 state change 都會(huì)被記錄下來(lái),'時(shí)空穿梭' 功能其實(shí)就是將當(dāng)前的 state 替換為記錄中某個(gè)時(shí)刻的 state 狀態(tài),利用store.replaceState(targetState)方法將執(zhí)行this._vm.state = state實(shí)現(xiàn)。

源碼中還有一些工具函數(shù)類似 registerModule、unregisterModule、hotUpdate、watch 以及 subscribe 等,如有興趣可以打開(kāi)源碼看看,這里不再細(xì)述。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 178,640評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,957評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,682評(píng)論 6 413
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 56,011評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 43,183評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,435評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,665評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,838評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 35,251評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 36,588評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,379評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,627評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容

  • Vuex 是一個(gè)專為 Vue 服務(wù),用于管理頁(yè)面數(shù)據(jù)狀態(tài)、提供統(tǒng)一數(shù)據(jù)操作的生態(tài)系統(tǒng)。它專注于 MVC 模式中的 ...
    你的肖同學(xué)閱讀 2,146評(píng)論 7 35
  • 寫在前面 因?yàn)閷?duì)Vue.js很感興趣,而且平時(shí)工作的技術(shù)棧也是Vue.js,這幾個(gè)月花了些時(shí)間研究學(xué)習(xí)了一下Vue...
    染陌同學(xué)閱讀 1,680評(píng)論 0 12
  • Vuex 是什么? ** 官方解釋:Vuex 是一個(gè)專為 Vue.js 應(yīng)用程序開(kāi)發(fā)的狀態(tài)管理模式**。它采用集中...
    Rz______閱讀 2,316評(píng)論 1 10
  • vuex 場(chǎng)景重現(xiàn):一個(gè)用戶在注冊(cè)頁(yè)面注冊(cè)了手機(jī)號(hào)碼,跳轉(zhuǎn)到登錄頁(yè)面也想拿到這個(gè)手機(jī)號(hào)碼,你可以通過(guò)vue的組件化...
    sunny519111閱讀 8,034評(píng)論 4 111
  • 系列文章:Vue 2.0 升(cai)級(jí)(keng)之旅Vuex — The core of Vue applic...
    6ed7563919d4閱讀 4,564評(píng)論 2 58