vuex 2.*原始碼解析—— 花半小時就可以讀懂vuex實現原理

程式碼星空發表於2018-12-25

本文章主要是拆解vuex v2.x的原始碼,快速看懂vuex1.0與vuex2.0的實現原理。

閱讀準備

vuex的原始碼是基於es6語法,所以在閱讀文章之前,你需要先掌握這些基礎:

1.es6語法

2.vuex官方文件,像我一樣英語閱讀能力差的,還有中文文件

3.前端基礎js、css、html不用說啦,vue基礎用法掌握

下載原始碼

開啟vuex gitHub 因為到我寫這篇文章為止,vuex已經發展到了3.0, 3.*是使用ts語法編寫,所以相對來說閱讀成本又要進階一層,我自己也還沒完全閱讀完,後期將會出vuex3.x的原始碼解析,還有typescript的相關文章。

github上選擇 tag 我們選擇一個最高版本的v2.5.0,clone到本地

vuex 2.*原始碼解析—— 花半小時就可以讀懂vuex實現原理
完成後開啟檔案,可以看到:

vuex 2.*原始碼解析—— 花半小時就可以讀懂vuex實現原理
其中src目錄下就是vuex的原始碼實現,層級最多就2層,

原始碼解讀

初始化

開始的地方: index.js

閱讀的起點,當前要從index.js開始。 先看index.js主要輸出了哪些東西:

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

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

讓我們來大致認識下它們: store vuex 對外提供的狀態儲存類,用在專案中,它就是一個狀態管理器,集中管理狀態
install vuex提供的在專案中安裝自身的方法
version vue版本號
mapState 為元件建立計算屬性以返回 Vuex store 中的狀態
mapMutations 建立元件方法提交 mutation
mapGetters 為元件建立計算屬性以返回 getter 的返回值
mapActions 建立元件方法分發 action
createNamespacedHelpers 建立基於名稱空間的元件繫結輔助函式
接下來,我會以vue專案中使用vuex的順序來解析下vuex是如何發揮作用的,後面再展開vuex的其他屬性與方法

vue使用vuex第一步: vue.use(Vuex)

install 為什麼不是從store開始說,因為這個才是vuex插入到vue專案中的第一步哈,install與store都來自於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 裡面主要做了什麼: 1.判斷是否有全域性的Vue(即window.Vue), 如果沒有,就賦值,確保我們的vuex只被install一次 Vue.use(vuex)就是執行了vuex的install方法;然後把vue掛載到全域性物件上,這樣子就可以在其他地方訪問vue拉 最後一行的applyMixin 來看下具體程式碼:

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

  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
    }
  }
}
複製程式碼

判斷vue版本號,在vue的生命週期初始化鉤子(vue1.0的版本是init, vue2.*的是beforeCreated)前執行一斷程式碼,具體執行程式碼在vuexInit中。給vue例項新增一個store,判斷判斷vue例項的初始化選項this.options裡面有沒有store,有就直接賦值到例項屬性store,這樣子我們就可以在vue例項裡面通過this.store,而不用辛苦地this.options.store或者this.options.parent.$store去訪問了。

vue使用vuex第二步 new Vuex.Store

例項化store,我們來看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.`)
    }
    ...
}
複製程式碼

確保install, 接下來是一些斷言函式,不過js裡面是沒有這個斷言函式,就是模擬斷言函式。 接下來是一些屬性賦值,以及把類方法dispatch, commit的this指向當前的store例項,程式碼就不貼了

再繼續看:

installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
plugins.forEach(plugin => plugin(this))
複製程式碼

這三行程式碼做了很重要的事情。installModule安裝options(建立store例項的時候,傳入的模組) resetStoreVM 方法是初始化 store._vm,觀測 state 和 getters 的變化; plugins 使用傳入的外掛
一個一個方法來具體檢視

#### 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)
  })
}
複製程式碼

最多接收5個傳入引數
store 表示當前 Store 例項
rootState 表示根 state
path 表示當前巢狀模組的路徑陣列
module 表示當前安裝的模組
hot 當動態改變 modules 或者熱更新的時候為 true

接下來就是對初始化傳入的options進行註冊安裝。具體的什麼實現,將會在後面的文章具體說明這邊我們先了解它是什麼動作,其中對module的註冊值得關注下:

if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }
複製程式碼

因為store是單一的狀態樹,如果當需要管理的資料越來越多,這棵樹就變得難以維護,所以引入module使得其結構更規範易維護。

resetStoreVM

執行完installModule,就執行resetStoreVM

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

  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = () => 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())
  }
}
複製程式碼

給store設定一個私有變數_vm,它是vue例項,這個 _vm 物件會保留我們的 state 樹,以及用計算屬性的方式儲存了 store 的 getters。

vuex初始化總結

Vuex 的初始化主要核心就是 installModule 和 resetStoreVM 函式。通過對 mutations 、actions 和 getters 的註冊,state 的是按模組劃分的,按模組的巢狀形成一顆狀態樹。而 actions、mutations 和 getters 的全域性的。 對vuex的初始化有一個大致的瞭解之後,讓我們來對我們在專案中用到的api來具體說明下是怎麼實現的,也會將vuex初始化的時候一些未說明清楚的東西說清楚。

vue API詳解

commit

先看下commit的函式定義:

  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) {
      if (process.env.NODE_ENV !== 'production') {
            console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    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'
      )
    }
}
複製程式碼

dispatch

先看下dispatch的函式定義:

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

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    this._actionSubscribers.forEach(sub => sub(action, this.state))

    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
}

傳入的引數: 
_type action 型別  
_payload 我們要更新的值
複製程式碼

subscribe

先來看下subscribe的函式定義

 subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
  }
複製程式碼

watch

 watch (getter, cb, options) {
    if (process.env.NODE_ENV !== 'production') {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }
複製程式碼

getter 監聽屬性名
cb 對應的回撥
options 可選,如{deep: true}之類的配置
響應式的接收一個getter返回值,當值改變時呼叫回撥函式。getter接收state狀態為唯一引數,並且options作為_vm.watch的引數。

相關文章