深刻了解VUEX原理

vuex做爲Vue全家桶不可或缺的一部分,學習並理解其源碼,不只能夠學習到做者的優秀開發思路和代碼編寫技巧,還能夠幫助咱們在開發過程當中寫出更好更規範的代碼,知其然,知其因此然前端

源碼版本是3.1.2,在調試源碼時儘可能不要直接使用console.log,由於有些時候其輸出並非你指望的數據,建議使用debugger進行調試閱讀源碼,接下來的文章中會適當的將源碼中一些兼容性和健壯性處理忽略,只看主要的流程vue

use

vue中使用插件時,會調用Vue.use(Vuex)將插件進行處理,此過程會經過mixin在各個組件中的生命鉤子beforeCreate中爲每一個實例增長$store屬性vuex

install

vue項目中,使用vuex進行數據管理,首先作的就是將vuex引入並Vue.use(Vuex)數組

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

Vue.use(Vuex)
複製代碼

在執行Vue.use(Vuex)時,會觸發vuex中暴露出來的方法install進行初始化,並缺會將Vue做爲形參傳遞,全部的vue插件都會暴露一個install方法,用於初始化一些操做,方法在/src/store.js中暴露微信

let Vue // bind on install

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)
}
複製代碼

首先會在store中定義一個變量Vue,用來接受Vue實例數據結構

install函數中,首先會判斷是否已經調用了Vue.use(Vuex),而後調用applyMixin方法進行初始化的一些操做app

總結:install方法僅僅作了一個容錯處理,而後調用applyMixinVue賦值異步

applyMixin

applyMixin方法在/src/mixin中暴露,該方法只作了一件事情,就是將全部的實例上掛載一個$store對象函數

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

  // 如果2以上的vue版本,直接經過mixin進行掛載$store
  if (version >= 2) {
    // 在每一個實例beforeCreate的時候進行掛載$store
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // vue 1.x版本處理 省略...
  }

  function vuexInit () {
    // 1. 獲取每一個組件實例的選項
    const options = this.$options

		// 2. 檢測options.store是否存在
    if (options.store) {
      // 下面詳細說明
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 檢測當前組件實例的父組件是夠存在,而且其父組件存在$store
      // 存在,則爲當前組件實例掛載$store屬性
      this.$store = options.parent.$store
    }
  }
}
複製代碼

整個mixin文件的難點在於理解this.$store = typeof options.store === 'function' ? options.store() : options.store作了什麼事學習

在使用vuex的時候,會將store掛載在根組件之上

import Vue from 'vue'
import Counter from './Counter.vue'
import store from './store'

new Vue({
  el: '#app',
  store,
  render: h => h(Counter)
})
複製代碼

在第一次調用vuexInit函數時,options.store就是根選項的store,所以會判斷其類型是否是function,如果則執行函數並將結果賦值給根實例的$store中,不然直接賦值。

總結:整個mixin文件作的事情,就是利用mixin在各個實例的生命鉤子beforeCreate中爲其增長屬性$store併爲其賦值,保證在每一個實例中均可以直接經過this.$store獲取數據和行爲。

Module

module模塊主要的功能:是將咱們定義的store根據必定的規則轉化爲一顆樹形結構,在實例化Store的時候執行,會將其獲得的樹形結構賦值給this._modules,後續會基於這顆樹進行操做。

樹形結構

首先是咱們在vuex中定義一些狀態和模塊,觀察其轉化的樹形結構爲什麼物

const state = {
  count: 0
}

const getters = {
}

const mutations = {
}

const actions = {
}

const modules = {
  moduleA:{
    state: {
      a: 'module a'
    },
    modules: {
      moduleB:{
        state: {
          b: 'module b'
        }
      }
    }
  },
  moduleC: {
    state: {
      c: 'module c'
    }
  }
}

export default new Vuex.Store({
  modules,
  state,
  getters,
  actions,
  mutations
})
複製代碼

vuex在獲取到定義的狀態和模塊,會將其格式化成一個樹形結構,後續的不少操做都是基於這顆樹形結構進行操做和處理,能夠在任意一個使用的組件中打印this.$store._modules.root觀察其結構

格式化以後的樹形結構,每一層級都會包含state_rawModule_children三個主要屬性

樹形節點結構

{
  state:{},
  _rawModule:{},
  _children: {}
}
複製代碼

state

根模塊會將自身還有其包含的所有子模塊state數據按照模塊的層級按照樹級結構放置,根模塊的state會包含自身以及全部的子模塊數據,子模塊的state會包含自身以及其子模塊的數據

{
  state: {
		count: 0,
    moduleA: {
      a: 'module a',
        moduleB: {
          b: 'module b'
        }
    },
    moduleC: {
      c: 'module c'
    }
  }
}
複製代碼

_rawModule

每一層樹形結構都會包含一個_rawModule節點,就是在調用store初始化的時候傳入的options,根上的_rawModule就是初始化時的全部選項,子模塊上就是各自初始化時使用的options

{
  modules:{},
  state:{},
  getters:{},
  actions:{},
  mutations:{}
}
複製代碼

_children

_children會將當前模塊以及其子模塊按照約定的樹形結構進行格式化,放在其父或者跟組件的_children中,鍵名就是其模塊名

{
  moduleA:{
    state: {},
    _rawModule:{},
    _children:{
      moduleB:{
        state: {},
        _rawModule:{},
        _children:{}
      },
    }
  },
  moduleC:{
    state: {},
    _rawModule:{},
    _children:{}
  }
}
複製代碼

總結:根據調用store初始化時傳入的參數,在其內部將其轉化爲一個樹形結構,能夠經過this.$store._modules.root查看

轉化

知道了轉化處理以後的樹形結構,接下來看看vuex中是如何經過代碼處理的,在src/module文件夾中,存在module-collection.jsmodule.js兩個文件,主要經過ModuleCollectionModule兩個類進行模塊收集

Module

Module的主要做用就是根據設計好的樹形節點結構生成對應的節點結構,實例化以後會生成一個基礎的數據結構,並在其原型上定一些操做方法供實例調用

import { forEachValue } from '../util'

export default class Module {
  constructor (rawModule, runtime) {
    // 是否爲運行時 默認爲true
    this.runtime = runtime
    // _children 初始化是一個空對象
    this._children = Object.create(null)
    // 將初始化vuex的時候 傳遞的參數放入_rawModule
    this._rawModule = rawModule
    // 將初始化vuex的時候 傳遞的參數的state屬性放入state
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  // _children 初始化是一個空對象,爲其增長子模塊
  addChild (key, module) {
    this._children[key] = module
  }
  
  // 根據 key,獲取對應的模塊
  getChild (key) {
    return this._children[key]
  }
}
複製代碼

ModuleCollection

結合Module用來生成樹形結構

import Module from './module'
import { forEachValue } from '../util'

export default class ModuleCollection {
  constructor (rawRootModule) {
    // 根據options 註冊模塊
    this.register([], rawRootModule, false)
  }
	
  // 利用 reduce,根據 path 找到此時子模塊對應的父模塊
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }

  register (path, rawModule, runtime = true) {
    // 初始化一個節點
    const newModule = new Module(rawModule, runtime)
    
    if (path.length === 0) { // 根節點, 此時 path 爲 []
      this.root = newModule
    } else { // 子節點處理
      // 1. 找到當前子節點對應的父
      // path ==> [moduleA, moduleC]
      // path.slice(0, -1) ==> [moduleA]
      // get ==> 獲取到moduleA
      const parent = this.get(path.slice(0, -1))
      // 2. 調用 Module 的 addChild 方法,爲其 _children 增長節點
      parent.addChild(path[path.length - 1], newModule)
    }

    // 如果存在子模塊,則會遍歷遞歸調用 register
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}
複製代碼
  1. 初始化ModuleCollection時傳遞的實參爲new Vuex.Store({....options})中的options,此時的rawRootModule就是options,接下來的操做都是基於rawRootModule進行操鎖

options的數據結構簡寫

{
  modules: {
    moduleA:{
      modules: {
        moduleB:{
        }
      }
    },
    moduleC: {
    }
  }
}
複製代碼
  1. 執行this.register([], rawRootModule, false)

    []對應形參path,保存的是當前模塊的層級路徑,例如moduleB對應的路徑["moduleA", "moduleB"]

    rawRootModule對應形參rawModule,表明在初始化參數options中對應的數據,例如moduleA對應的rawModule爲:

    moduleA:{
      state: {
        a: 'module a'
      },
      mutations:{
        incrementA: ({ commit }) => commit('increment'),
        decrementA: ({ commit }) => commit('decrement'),
      },
      modules: {
        moduleB:{
          state: {
            b: 'module b'
          }
        }
      }
    }
    複製代碼
  2. 每次執行register時都會實例化Module,生成一個樹形的節點newModule,以後即是經過判斷path的長度來決定newModule放置的位置,第一次執行registerpath[],則直接將newModule賦值給this.root,其他狀況,即是經過path找到當前節點對應的父節點並將其放置在_children

  3. 判斷rawModule.modules是否存在,如果存在子模塊,便遍歷rawModule.modules進行遞歸調用register進行遞歸處理,最終會生成一個指望的樹形結構

Store

經歷了前面的鋪墊,終於到了vuex的核心類store,在store中會對定義的statemutationsactionsgetters等進行處理

首先看看Store的總體結構

class Store {
  constructor (options = {}) {}

  get state () {}

  set state (v) {}

  commit (_type, _payload, _options) {}

  dispatch (_type, _payload) {}

  subscribe (fn) {}

  subscribeAction (fn) {}

  watch (getter, cb, options) {}

  replaceState (state) {}

  registerModule (path, rawModule, options = {}) {}

  unregisterModule (path) {}

  hotUpdate (newOptions) {}

  _withCommit (fn) {}
}
複製代碼

在使用vuex中,會看到經常使用的方法和屬性都定義在store類中,接下來經過完善類中的內容逐步的實現主要功能

State

在模塊中定義的state經過vux以後處理以後,即可以在vue中經過$store.state.xxx使用,且當數據變化時會驅動視圖更新

首先會在store中進行初始化

class Store {
  constructor(options) {
    // 定義一些內部的狀態 ....
    this._modules = new ModuleCollection(options)
    const state = this._modules.root.state
    // 初始化根模塊,會遞歸註冊全部的子模塊
    installModule(this, state, [], this._modules.root)
    // 初始化 store、vm
    resetStoreVM(this, state)
  }

  // 利用類的取值函數定義state,其實取的值是內部的_vm傷的數據,代理模式
  get state() {
    return this._vm._data.$$state
  }

  _withCommit (fn) {
    fn()
  }
}
複製代碼

首先會執行installModule,遞歸調用,會將全部的子模塊的數據進行註冊,函數內部會進行遞歸調用自身進行對子模塊的屬性進行便利,最終會將全部子模塊的模塊名做爲鍵,模塊的state做爲對應的值,模塊的嵌套層級進行嵌套,最終生成所指望的數據嵌套結構

{
  count: 0,
  moduleA: {
    a: "module a",
    moduleB: {
      b: "module b"
    }
  },
  moduleC: {
    c: "module c"
  }
}
複製代碼

installModule關於處理state的核心代碼以下

/** * @param {*} store 整個store * @param {*} rootState 當前的根狀態 * @param {*} path 爲了遞歸使用的,路徑的一個數組 * @param {*} module 從根模塊開始安裝 * @param {*} hot */
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length // 是否是根節點

  // 設置 state
  // 非根節點的時候 進入,
  if (!isRoot && !hot) {
    // 1. 獲取到當前模塊的父模塊
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 2. 獲取當前模塊的模塊名
    const moduleName = path[path.length - 1]
    // 3. 調用 _withCommit ,執行回調
    store._withCommit(() => {
      // 4. 利用Vue的特性,使用 Vue.set使剛設置的鍵值也具有響應式,不然Vue監控不到變化
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 遞歸處理子模塊的state
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
複製代碼

state處理成指望的結構以後,會結合resetStoreVMstate進行處理,如果直接在Store中定義變量state,外面能夠獲取到,可是當修改了以後並不能利用的vue的數據綁定驅動視圖的更行,因此利用vue的特性,將vue的實例放置在_vm上,而後利用類的取值函數獲取

當使用$store.state.count的時候,會先根據類的取值函數get state進行取值,取值函數內部返回的就是resetStoreVM所賦值_vm,結合vue進行響應適處理

function resetStoreVM(store, state) {
  store._vm = new Vue({
    data: {
      $$state: state
    }
  })
}
複製代碼

Mutations

vuex中,對於同步修改數據狀態時,推薦使用mutations進行修改,不推薦直接使用this.$store.state.xxx = xxx進行修改,能夠開啓嚴格模式strict: true進行處理

vuex對於mutations的處理,分爲兩部分,第一步是在installModule時將全部模塊的mutations收集訂閱,第二步在Store暴露commit方法發佈執行所對應的方法

訂閱

首先在Store中處理增長一個_mutations屬性

constructor(options){
	// 建立一個_mutations 空對象,用於收集各個模塊中的 mutations
	this._mutations = Object.create(null)
}
複製代碼

installModule中遞歸調用的處理全部的mutations

const local = module.context = makeLocalContext(store, '', path)
// 處理 mutations
module.forEachMutation((mutation, key) => {
  registerMutation(store, key, mutation, local)
})
複製代碼

registerMutation函數中進行對應的nutations收集

// 註冊 mutations 的處理 -- 訂閱
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都會被訂閱在_mutations中,只須要在調用執行時找到對應的mutations進行遍歷執行,這裏使用一個數組收集訂閱,由於在vuex中,定義在不一樣模塊中的同名mutations都會被依次執行,因此須要使用數組訂閱,並遍歷調用,所以也建議在使用vuex的時候,若項目具備必定的複雜度和體量,建議使用命名空間namespaced: true,能夠減小沒必要要的重名mutations所有被執行,致使不可控的問題出現

makeLocalContext函數將vuex的選項進行處理,省略開啓命名空間的代碼,主要是將gettersstate進行劫持處理

function makeLocalContext (store, namespace, path) {
  const local = {
    dispatch: store.dispatch,
    commit: store.commit
  }

  // getters 和 state 必須是懶獲取,由於他們的修改會經過vue實例的更新而變化
  Object.defineProperties(local, {
    getters: {
      get: () => store.getters
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
複製代碼

發佈

收集訂閱完成以後,須要Store暴露一個方法用於觸發發佈,執行相關的函數修改數據狀態

首先在Store類上定義一個commit方法

{
  // 觸發 mutations
  commit (_type, _payload, _options) {
    // 1. 區分不一樣的調用方式 進行統一處理
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    // 2. 獲取到對應type的mutation方法,便利調用
    const entry = this._mutations[type]
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
  }
}
複製代碼

可是在源碼中,外界調用時並非直接調用調用類上的commit方法,而是在構造constructor中重寫的commit

constructor(options){
	// 1. 獲取 commit 方法
  const { commit } = this
  // 2. 使用箭頭函數和call 保證this的指向
  this.commit = (type, payload, options) => {
  	return commit.call(this, type, payload, options)
  }
} 
複製代碼

unifyObjectStyle方法作了一個參數格式化的處理,調用 mutations 可使用this.$store.commit('increment', payload)this.$store.commit({type: 'increment', payload})兩種方式,unifyObjectStyle函數就是爲了將不一樣的參數格式化成一種狀況,actions同理

function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  return { type, payload, options }
}
複製代碼

Actions

actions用於處理異步數據改變,mutations用於處理同步數據改變,二者的區別主要在因而否是異步處理數據,所以二者在實現上具有不少的共通性,首先將全部的actions進行訂閱收集,而後暴露方法發佈執行

訂閱

首先在Store中處理增長一個_actions屬性

constructor(options){
	// 建立一個 _actions 空對象,用於收集各個模塊中的 actions
	this._actions = Object.create(null)
}
複製代碼

installModule中遞歸調用的處理全部的actions

const local = module.context = makeLocalContext(store, '', path)
// 處理actions
module.forEachAction((action, key) => {
	const type = action.root ? key : '' + key
	const handler = action.handler || action
	registerAction(store, type, handler, local)
})
複製代碼

registerAction函數中進行對應的actions收集

// 註冊 actions 的處理
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
 	// actions 和 mutations 在執行時,第一個參數接受到的不同 
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    
    // 判斷是否時Promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    
    return res
  })
}
複製代碼

發佈

actions的發佈執行,和mutations處理方式一致,區別在於dispatch方法須要多作一些處理

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

  const action = { type, payload }
  const entry = this._actions[type]

	// 如果多個,則使用Promise.all(),不然執行一次
  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
	
  // 拿到執行結果 進行判斷處理
  return result.then(res => {
    try {
      this._actionSubscribers
        .filter(sub => sub.after)
        .forEach(sub => sub.after(action, this.state))
    } catch (e) {}
    return res
  })
}
複製代碼

求內推

此文章還沒寫做完成,可是想借這個平臺獲取一些內推的前端職位,因此提早發了出來,後續會繼續編寫,😄 上海前端有合適崗位的小夥伴能夠聯繫我,杭州的機會也在看,失業中.... 微信號: Nnordon_Wang

相關文章
相關標籤/搜索