Vuex源碼分析

Vuex 源碼學(xué)習(xí)

注釋

源碼目錄

src:.
│  helpers.js
│  index.esm.js
│  index.js
│  mixin.js
│  store.js
│  util.js
│
├─module
│      module-collection.js
│      module.js
│
└─plugins
        devtool.js
        logger.js

Vuex 核心 API:

// 初始化實例
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
const store = new Vuex.Store({
  state,
  mutations,
  actions,
  getters,
  modules,
  plugins
})

const vm = new Vue({
  store, // inject store to all children
  el: '#app',
  render: h => h(App)
})

// 實例方法
commit dispatch

// 輔助函數(shù)
mapState mapGetters mapActions mapMutations

插件安裝

import Vuex from 'vuex'

Vue.use(Vuex)

引入了 src/index.js 暴露的對象:

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

其中包含一個 install 方法,這也是 Vue 官方開發(fā)插件的方式。 install 方法位于 src/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)
}

這里將傳入的 Vue(_Vue) 賦值給 Vue崖面,便于后續(xù)的使用券膀。然后調(diào)用 src/mixin.js 中暴露的方法 applyMinxin(Vue) 浆竭,主要作用就是混入 beforeCreate 鉤子函數(shù)入蛆,保證每個組件的 this.$store 都是初始化實例的 store

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  // Vue 2.x版本直接通過Vue.mixin混淆執(zhí)行vuexInit方法
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 1.x版本覆寫原_init 方法
    // 加入vuexInit方法
    // 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)
    }
  }

  /**
   * Vuex 初始化鉤子
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    // options.store 代表根組件
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
        // 在每個子組件上面掛載store的引用
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

Store 構(gòu)造函數(shù)

const store = new Vuex.Store({
  state,
  mutations,
  actions,
  getters,
  modules,
  plugins
})

const vm = new Vue({
  store, // inject store to all children
  el: '#app',
  render: h => h(App)
})

在上邊代碼中烁设,調(diào)用Store構(gòu)造函數(shù)創(chuàng)建store實例。 這里主要是創(chuàng)建一些 store 實例內(nèi)部的屬性钓试,module注冊以及 mutations, actions, getters的注冊和通過 store._vm 觀測 state, getters 的變化装黑。下邊分析一下store.js 中相對核心的代碼:

this._modules

如果我們在實例化store對象時,添加了 mod1 模塊

  modules: {
    mod1: {}
  }
this._modules = new ModuleCollection(options)

_modules

現(xiàn)在根據(jù)生成的屬性對象弓熏,來進(jìn)行代碼學(xué)習(xí) src/module-collection.js:

關(guān)鍵代碼:

 constructor (rawRootModule) {
    this.register([], rawRootModule, false)
  }

  register (path, rawModule, runtime = true) {
    // 實例化一個module
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      // 如果是根module就綁定到root屬性
      this.root = newModule
    } else {
      // 子module添加到父module的 _children屬性上
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    // 注冊嵌套模塊(modules屬性存在)
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

第一次調(diào)用恋谭,path = [], 進(jìn)入path.length === 0 的邏輯中,實例化 Module 賦值給 this.root(_modules.root)挽鞠。先不分析 else 的邏輯疚颊,先看下 Module 構(gòu)造函數(shù)做了什么?

  constructor (rawModule, runtime) {
    // 初始化時runtime為false
    this.runtime = runtime
    // Store some children item
    // _children:保存子模塊
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    // 保存原始對象,傳入的信认,也就是父級Module
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    // 保存state 材义,是函數(shù)就執(zhí)行
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

下邊回到剛才src/module-collection.js, 執(zhí)行到options含有 modules 屬性時,執(zhí)行以下操作來遞歸注冊模塊

   if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }

這時就不符合path.length === 0嫁赏,進(jìn)入 else 邏輯:

     // 子module添加到父module的 _children屬性上
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)

installModule()

installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) {
  // 是否為根Module
  const isRoot = !path.length
  // 獲取module的完整Namespace   (傳入完整的路徑) ["cart", "cart_child"]  --> 獲得 cart/cart_child/
  const namespace = store._modules.getNamespace(path)

  // 如果namespaced為true則在_modulesNamespaceMap中注冊
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  // 非根設(shè)置state
  if (!isRoot && !hot) {
    // 根據(jù)path獲取父state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 當(dāng)前module
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      // 通過Vue.set將state設(shè)置為響應(yīng)式
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 設(shè)置module上下文
  // store cart/ ["cart"]
  const local = module.context = makeLocalContext(store, namespace, path)

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

  // 遍歷注冊action
  module.forEachAction((action, key) => {
    // {root: true} -> 在帶命名空間的模塊注冊全局 action
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

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

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

這里主要就是注冊mutation action getter, 根據(jù)_modules生成namespace其掂,分別注冊state mutation action getter,最后遞歸注冊子模塊潦蝇。
先看 makeLocalContext 清寇,它根據(jù) namespace 創(chuàng)建局部 context喘漏,分別注冊state mutation action getter。其實這里namespace: true 會讓state mutation action getter都擁有自己的全名华烟。這樣可以減少命名沖突配名。

注意:module.context屬性,輔助函數(shù)方法中會使用到

在進(jìn)行子module的注冊時,是遍歷module._children屬性丁屎。會執(zhí)行

  // 非根設(shè)置state
  if (!isRoot && !hot) {
    // 根據(jù)path獲取父state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 當(dāng)前module
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      // 通過Vue.set將state設(shè)置為響應(yīng)式
      Vue.set(parentState, moduleName, module.state)
    })
  }

state

再看下 installModule過程中的其它 3 個重要方法:registerMutation瘫筐、registerAction 和 registerGetter:

registerMutation
// 處理mutation === handler
function registerMutation (store, type, handler, local) {
  // store._mutations[type]判斷,不存在就賦值空數(shù)組
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 將mutation的包裝函數(shù)push到對應(yīng)的mutation對象數(shù)組
  entry.push(function wrappedMutationHandler (payload) {
    // 調(diào)用我們設(shè)置的mutation的回調(diào)函數(shù) --> commit觸發(fā)
    handler.call(store, local.state, payload)
  })
}

mutation的回調(diào)函數(shù)的調(diào)用是通過commit觸發(fā)的喂链。這里需要通過commit函數(shù)進(jìn)行了解:

  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
    }

    // 遍歷這個 type 對應(yīng)的 mutation 對象數(shù)組返十,執(zhí)行 handler(payload)
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })

    // 通知所有訂閱者 (_subscribers: 訂閱(注冊監(jiān)聽) store 的 mutation)
    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'
      )
    }
  }

commit 支持 3 個參數(shù),type 表示 mutation 的類型椭微,payload 表示額外的參數(shù)洞坑,options 表示一些配置。commit 根據(jù) type 去查找對應(yīng)的 mutation蝇率,如果找不到迟杂,則輸出一條錯誤信息,否則遍歷這個 type 對應(yīng)的 mutation 對象數(shù)組本慕,執(zhí)行 handler(payload) 方法排拷,這個方法就是之前定義的 wrappedMutationHandler,執(zhí)行它就相當(dāng)于執(zhí)行了 registerMutation 注冊的回調(diào)函數(shù)锅尘。注意這里我們依然使用了 this._withCommit 的方法提交 mutation监氢。

registerAction
// 處理action
function registerAction (store, type, handler, local) {
  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對象就包裝成一個Promise對象
    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
    }
  })
}

mutation類似,action的注冊比mutation多了一步藤违,將函數(shù)進(jìn)行了Promise包裝浪腐,這也是為什么action可以異步的原因。action是通過dispatch觸發(fā)的顿乒。

registerGetter
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

這里保存 getter 到store._wrappedGetters上牛欢。

resetStoreVM

// 設(shè)置一個新的vue實例,用來保存state和getter
function resetStoreVM (store, state, hot) {
  // 保存之前的vm對象
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // this.$store.getters.xxxgetters -> store._vm[xxxgetters]
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure enviroment.
    computed[key] = partial(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
  // 在new一個Vue實例的過程中不會報出一切警告
  Vue.config.silent = true
  // new一個vue實例, 響應(yīng)式 state->state, computed->getter
  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)
  }

  // 函數(shù)每次都會創(chuàng)建新的 Vue 實例并賦值到 store._vm
  // 這里將舊的 _vm 對象的狀態(tài)設(shè)置為 null淆游,并調(diào)用 $destroy 方法銷毀這個舊的 _vm 對象
  if (oldVm) {
    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())
  }
}

利用 store._vm 保存了一個 Vue 實例傍睹,通過 Vue 實例來保留 state 樹,以及用計算屬性的方式存儲了 store 的 getters犹菱。

輔助函數(shù)

mapState

官方示例:

computed: mapState({
    count: state => state.count,
    countAlias: 'count',
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
// 當(dāng)映射的計算屬性的名稱與 state 的子節(jié)點(diǎn)名稱相同時,可以傳遞數(shù)組
computed: mapState(['count'])
// 命名空間
...mapState({
  a: state => state.some.nested.module.a,
  b: state => state.some.nested.module.b
})
...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
normalizeNamespace
function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

mapState首先通過normalizeNamespace對傳入的參數(shù)進(jìn)行有沒有 namespace 的處理拾稳,而后執(zhí)行 fn(namespace, map)。

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

這里通過normalizeMap將傳入的數(shù)組或者對象這兩種方式進(jìn)行處理:

/**
 * Normalize the map
 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
 * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
 * @param {Array|Object} map
 * @return {Object}
 */
// map 處理
function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

mapState 的作用是把全局的 state 和 getters 映射到當(dāng)前組件的 computed 計算屬性中腊脱,Vue 中 每個計算屬性都是一個函數(shù)访得, mapState 函數(shù)的返回值是這樣一個對象:

computed: {
    count() {
          return this.$store.state.count
        }
}

其他就不再一一分析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市悍抑,隨后出現(xiàn)的幾起案子鳄炉,更是在濱河造成了極大的恐慌,老刑警劉巖搜骡,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拂盯,死亡現(xiàn)場離奇詭異,居然都是意外死亡记靡,警方通過查閱死者的電腦和手機(jī)谈竿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摸吠,“玉大人空凸,你說我怎么就攤上這事〈缌。” “怎么了呀洲?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長啼止。 經(jīng)常有香客問我道逗,道長,這世上最難降的妖魔是什么族壳? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮趣些,結(jié)果婚禮上仿荆,老公的妹妹穿的比我還像新娘。我一直安慰自己坏平,他們只是感情好拢操,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舶替,像睡著了一般令境。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上顾瞪,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天舔庶,我揣著相機(jī)與錄音,去河邊找鬼陈醒。 笑死惕橙,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钉跷。 我是一名探鬼主播弥鹦,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼爷辙!你這毒婦竟也來了彬坏?” 一聲冷哼從身側(cè)響起朦促,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎栓始,沒想到半個月后务冕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡混滔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年洒疚,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坯屿。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡油湖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出领跛,到底是詐尸還是另有隱情乏德,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布吠昭,位于F島的核電站喊括,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏矢棚。R本人自食惡果不足惜郑什,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蒲肋。 院中可真熱鬧蘑拯,春花似錦、人聲如沸兜粘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孔轴。三九已至剃法,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間路鹰,已是汗流浹背贷洲。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晋柱,地道東北人恩脂。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像趣斤,于是被迫代替她去往敵國和親俩块。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348