vue源碼解讀(三)Vuex源碼解析:Store構造類詳解

歡迎star個人github倉庫,共同窗習~目前vue源碼學習系列已經更新了6篇啦~

https://github.com/yisha0307/...

快速跳轉:javascript

Vuex

首先,vuex是什麼?vue

根據官方的解釋,java

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

vue的整個狀態管理主要包括如下幾部分:react

  • state: 驅動應用的數據源;
  • view: 以聲明方式將 state 映射到視圖;
  • actions: 響應在 view 上的用戶輸入致使的狀態變化。

看一下官網提供的單向數據流示意:
git

可是,若是咱們要在兄弟組件之間傳值,或者要多個組件共享一個狀態,這個單向數據流就會遭到破壞。在不使用vuex的時候,須要在父組件中定義好state,再用props傳遞給子組件,若是子組件要修改父組件的狀態,就須要使用$emit事件。稍微進階一點,可使用busEvent, 就是new一個Vue實例出來,經過在target component上$on註冊上一個事件,在source component上$emit觸發這個事件,引發target component的狀態改變。可是,一旦應用變得大型,這兩種方式就變得很是脆弱。因此,vuex就誕生啦~github

仍是看一下官網的vuex示意圖:
vuex

Vuex實現了一個單項數據流,在全局擁有一個state存放數據,全部修改state的操做必須經過mutation來進行執行,同時mutation只能同步修改state,若是要異步修改就須要調用action, 而action內部也是要使用mutation進行修改state的操做。最後,根據state的變化,渲染到視圖上。api

Vuex運行依賴Vue內部雙向綁定機制,須要new Vue的實例來實現,所以,vuex只能和vue搭配使用。數組

更多vuex的詳細介紹請參閱Vuex官方教程promise

this.$store注入

首先,看一下store是如何install到vue上的,如下是vuex的install代碼:

/*暴露給外部的插件install方法,供Vue.use調用安裝插件*/
export function install (_Vue) {
  if (Vue) {
    /*避免重複安裝(Vue.use內部也會檢測一次是否重複安裝同一個插件)*/
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  /*保存Vue,同時用於檢測是否重複安裝*/
  Vue = _Vue
  applyMixin(Vue)
}

這段代碼主要作了兩件事:

  • 檢查有沒有重複安裝vuex
  • 若是沒有安裝的話,就去調用applyMixin()

接下來看一下applyMixin的代碼:

export default function (Vue) {
  /*獲取Vue版本,鑑別Vue1.0仍是Vue2.0*/
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    /*經過mixin將vuexInit混淆到Vue實例的beforeCreate鉤子中*/
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    /*將vuexInit放入_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 hook, injected into each instances init hooks list.
   */
   /*Vuex的init鉤子,會存入每個Vue實例等鉤子列表*/
  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      /*存在store其實表明的就是Root節點,直接執行store(function時)或者使用store(非function)*/
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
        // 從父組件裏獲取this.$store
      this.$store = options.parent.$store
    }
  }
}

上面的代碼解釋一下,就是先去檢查vue的版本,若是是2.0就在beforeCreate的時候調用VueInit這個方法,若是是1.0就放入Vue的_init中;

而後再看一下vuexInit這個方法,若是options存在store的話就證實是Root節點,直接執行store(由於咱們注入store的時候,都會在new Vue的時候放進去,好比:

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

因此若是options裏有store,就直接執行便可;若是沒有,就從父組件中獲取$store,這樣就保證了全部組件都公用了全局的同一份store。

經過以上步驟,就完成了this.$store的注入,在工程的任何地方均可以應用,並且指向的都是同一個store。

Store

回想一下,咱們在定義Store的時候,一般是這樣寫的 (來自vuex官方教程舉例):

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

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

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

所以,初始化store實例的時候就註冊了modules裏的state/mutations/getters/actions。來看一下Store的代碼(由於store構造類的源碼不少,會一部分一部分講):

/*Store構造類*/
export class Store {
  constructor (options = {}) {
    // 首先會檢查一下瀏覽器環境內有沒有vue
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    const {
      plugins = [],
     // 默認嚴格模式開啓
      strict = false
    } = options

    /*從option中取出state,若是state是function則執行,最終獲得一個對象*/
    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    // store internal state
    /* 用來判斷嚴格模式下是不是用mutation修改state的 */
    this._committing = false
    // 如下是初始化actions/mutations/wrapperGetters/modules等
    /* 存放action */
    this._actions = Object.create(null)
    /* 存放mutation */
    this._mutations = Object.create(null)
    /* 存放getter */
    this._wrappedGetters = Object.create(null)
    /* module收集器 */
    this._modules = new ModuleCollection(options)
    /* 根據namespace存放module */
    this._modulesNamespaceMap = Object.create(null)
    /* 存放訂閱者 */
    this._subscribers = []
    /* 用以實現Watch的Vue實例 */
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    /*將dispatch與commit調用的this綁定爲store對象自己,不然在組件內部this.dispatch時的this會指向組件的vm*/
    const store = this
    const { dispatch, commit } = this
    /* 爲dispatch與commit綁定this(Store實例自己) */
    // dispatch對應actions, commit對應mutations (dispatch裏會有promise實現)
    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

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

    /* devtool插件 */
    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }

以上代碼是Vue構造類的第一部分,能夠看到除了初始化actions/mutations/modules...等等,還調用了installModule和resetStoreVM這兩個方法。

看一下這兩個方法分別是什麼:

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length // path是[]表示是根節點
  /* 獲取module的namespace */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  /* 若是有namespace則在_modulesNamespaceMap中註冊 */
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    /* 獲取父級的state */
    const parentState = getNestedState(rootState, path.slice(0, -1))
    /* module的name */
    const moduleName = path[path.length - 1]
    // 這邊也用_withCommit是由於state只能在_committing === true的時候進行修改
    store._withCommit(() => {
    //   Vue.set內部會用defineReactive將module.state設置成響應式的
    // defineReactive的代碼詳見第一節
      Vue.set(parentState, moduleName, module.state)
    })
  }

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

  /* 遍歷註冊mutation */
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  /* 遍歷註冊action */
  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  /* 遍歷註冊getter */
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  /* 遞歸安裝mudule */
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

installModule的做用主要是爲module加上namespace名字空間(若是有)後,註冊mutation、action以及getter,同時遞歸安裝全部子module。

/* 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed */
function resetStoreVM (store, state, hot) {
  /* 存放以前的vm對象 */
  const oldVm = store._vm 

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

  /* 經過Object.defineProperty爲每個getter方法設置get方法,好比獲取this.$store.getters.test的時候獲取的是store._vm.test,也就是Vue對象的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的目的是在new一個Vue實例的過程當中不會報出一切警告 */
  Vue.config.silent = true
  /*  這裏new了一個Vue對象,運用Vue內部的響應式實現註冊state以及computed*/
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  /* 使能嚴格模式,保證修改store只能經過mutation */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除舊vm的state的引用,以及銷燬舊的Vue對象 */
    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())
  }
}

在這段代碼中, 實際上是利用了vue.js能夠對數據進行雙向綁定的特色(具體參見Vue的雙向綁定原理),new了一個vue的實例,而且綁定了state和computed, 這樣就能夠實現data和視圖的同步更新。

繼續看下Vuex.store裏還有什麼:

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

  set state (v) {
    //   state不容許經過set進行修改,必須經過commit
    if (process.env.NODE_ENV !== 'production') {
      assert(false, `Use store.replaceState() to explicit replace store state.`)
    }
  }

get沒什麼可說的,就是返回這個state裏的值,可是set這邊除了一個斷言就沒有了,意思是state裏的數據並不能直接修改,必須用mutation。

Commit && Dispatch

再看一下store裏提供的比較重要的兩個api —— commit 和dispatch。

commit (_type, _payload, _options) {
    // unifyObjectStyle主要是統一參數的格式
    // 由於mutation 支持兩種寫法 (一種是列舉參數,一種是把參數放在{}裏,具體參見教程)
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    /* 取出type對應的mutation的方法 */
    const entry = this._mutations[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    /* 執行mutation中的全部方法 */
    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'
      )
    }
  }
  _withCommit (fn) {
    // _committing是一個標誌位,在strict模式下保證只能經過mutation來修改store的數據
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }

mutation用的這個commit方法,要注意的就是這個_withCommit,在_withCommit裏_committing是true, 所以,_committing是一個標誌位,若是在strice mode(default true)下,必定要用commit方法才能修改。看一下這個斷言:

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
    //  function assert (condition, msg) {
    //   if (!condition) throw new Error(`[vuex] ${msg}`)
    // }
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

調用的vue的watch方法,在有變更的時候作一個檢驗,若是_committing不是true, 就扔出error。

繼續看給actions調用的dispatch方法:

/* 調用action的dispatch方法 */
  dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    /* actions中取出type對應的ation */
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    /* 是數組則包裝Promise造成一個新的Promise,只有一個則直接返回第0個 */
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  }

dispatch這個方法比較簡單,使用了Promise.all, 因此能夠執行異步操做。

可是咱們在實際操做mutation和action的時候,是能夠獲取到state和調用commit的,這是怎麼作到的呢?看一下registerMutationregisterAction兩個方法。

/* 遍歷註冊mutation */
function registerMutation (store, type, handler, local) {
  /* 全部的mutation會被push進一個數組中,這樣相同的mutation就能夠調用不一樣module中的同名的mutation了 */
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

/* 遍歷註冊action */
function registerAction (store, type, handler, local) {
  /* 取出type對應的action */
  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)
    /* 判斷是不是Promise */
    if (!isPromise(res)) {
      /* 不是Promise對象的時候轉化稱Promise對象 */
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      /* 存在devtool捕獲的時候觸發vuex的error給devtool */
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

能夠看到,在registerMutation裏,push進mutationHandler的時候,會塞進local.state, 而在registerAction裏,push進actionHandler的時候,第一個參數會包裝成dispatch/commit/getters/state...的對象,這樣在action裏就能夠去調用這些屬性和方法。

因而,咱們就能夠這樣使用mutation和action:

// vuex裏定義mutation:
increment (state, payload) {
    state.count += payload.amount
}

// vuex裏定義action: (使用解構)
actionB ({ dispatch, commit }) {
  return dispatch('actionA').then(() => {
    commit('someOtherMutation')
  })
}

store提供的其餘api

最後一口氣把store裏的代碼都看完吧:

/* 觀察一個getter方法 */
  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)
  }

  /* 重置state */
  replaceState (state) {
    this._withCommit(() => {
      this._vm._data.$$state = state
    })
  }
  • watch這個方法比較有趣的點在於,_watcherVM在store的構造函數裏被定義成new Vue(), 所以能夠直接採用vue的$watch去監聽getter裏的值的變化。
  • replaceState就比較簡單,替換了state的根狀態。
/* 註冊一個動態module,當業務進行異步加載的時候,能夠經過該接口進行註冊動態module */
  registerModule (path, rawModule) {
    /* 轉化稱Array */
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
      assert(path.length > 0, 'cannot register the root module by using registerModule.')
    }

    /*註冊*/
    this._modules.register(path, rawModule)
    /*初始化module*/
    installModule(this, this.state, path, this._modules.get(path))
    // reset store to update getters...
    /* 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed */
    resetStoreVM(this, this.state)
  }

  /* 註銷一個動態module */
  unregisterModule (path) {
    /* 轉化稱Array */
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
    }

    /*註銷*/
    this._modules.unregister(path)
    this._withCommit(() => {
      /* 獲取父級的state */
      const parentState = getNestedState(this.state, path.slice(0, -1))
      /* 從父級中刪除 */
      Vue.delete(parentState, path[path.length - 1])
    })
    /* 重製store */
    resetStore(this)
  }

先看一下registerModule這個api的調用方式:

// 註冊myModule
store.registerModule('myModule', {
  // ...
})
// 註冊嵌套模塊 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

因此,在調用registerModule的時候,由於能夠傳string和array,因此在一開始要統一成array;以後再調用installModuleresetStoreVM這兩個Vuex.store裏最重要的方法,初始化module並重設store。

unregisterModule的做用就是註銷掉一個動態模塊。調用的方式就是unregisterModule(path: string | Array<string>),在內部首先和registerModule同樣,把string統一成array, 若是不是這兩種數據結構的參數,就拋錯出來~以後在moudles裏unregister這個path, 經過commit的方式把該module裏的state註銷掉,同時重置store。

結語

store的代碼基本就講完啦,能夠看到,vuex的代碼其實不少都是高度依賴vue自身支持雙向綁定的特性,好比store構造函數裏的resetStoreVM(), 就是new了一個Vue的實例,運用vue內部的響應式註冊了state和computed;再好比,store提供的api-watch也是使用的vm.$watch。所以,vuex只能和vue搭配使用,不能作其餘框架的狀態管理。

相關文章
相關標籤/搜索