前言

我覺得每個人可能都有過看原始碼的想法吧,也包括我,因為看原始碼不光能使自己對這個庫更加熟悉,還能學習到作者強大的思想,久而久之,自己的水平和思想也會有明顯的提升的,
但對于我來說,之前從來沒有閱讀過原始碼,想閱讀原始碼卻不敢邁出那一步,因為一個成熟的庫有著太多的方法、邏輯,閱讀起來可能會比較困難,但人總要勇于嘗試的嘛,于是我就準備把 Vuex 的原始碼 clone 下來,沒有別的原因,只是因為這個庫體積比較小,算上注釋,核心代碼只有1000行不到,我覺得非常適合第一次閱讀原始碼的人拿來練手
說干就干,我就先在 github 上給自己列了一個計劃表,預計 15 天看完原始碼并完成總結,然后每天記錄一下當天的識訓

不過最后的結果倒是出乎我的意料,閱讀原始碼加上整理總結只用了8天左右的時間
在閱讀原始碼之前,我是先去看了一下 Vuex 的官方檔案,算是一種回顧、查漏補缺,我也非常建議這樣做,因為你看原始碼,你就會看到這個庫里面所有的內容,那么你連這個庫都沒用明白呢,閱讀原始碼的難度無形之中又增加了嘛!即先會熟練使用這個庫的各個方法(盡管你并不知道為何這么使用),再在閱讀原始碼的程序中看到相應的代碼時聯想到那個方法的使用,兩者相互結合,對于原始碼的理解就變得容易許多了
這里放上 Vuex 官方檔案的鏈接,如果有興趣跟著我的思路閱讀 Vuex 原始碼的小伙伴可以先把檔案中提到的所有使用都熟悉一下 ?? Vuex官方檔案
文末有 總結 和 問答環節
🔥 原始碼決議
對于原始碼的所有注釋和理解我都收錄在我 github 的 Vuex-Analysis 倉庫里了,想要看更詳細的注釋的,可以 fork 下來參考一下 ?? Vuex原始碼決議倉庫地址鏈接(覺得不錯的可以點個 star 支持一下)
接下來本文就按照我當時閱讀原始碼的思路,一步一步詳細地講解,希望大家耐心看完,謝謝啦~
- 公眾號:前端印象
- 不定時有送書活動,記得關注~
- 關注后回復對應文字領取:【面試題】、【前端必看電子書】、【資料結構與演算法完整代碼】、【前端技術交流群】
一、原始碼目錄結構分析
整個 Vuex 的原始碼檔案非常多,我們直接看最主要的檔案,即 src 檔案夾中的內容,結構示例如下:
├── src
├── module // 與模塊相關的操作
│ ├── module-collection.js // 用于收集并注冊根模塊以及嵌套模塊
│ └── module.js // 定義Module類,存盤模塊內的一些資訊,例如: state...
│
├── plugins // 一些插件
│ ├── devtool.js // 開發除錯插件
│ └── logger.js //
│
├── helpers.js // 輔助函式,例如:mapState、mapGetters、mapMutations...
├── index.cjs.js // commonjs 打包入口
├── index.js // 入口檔案
├── index.mjs // es6 module 打包入口
├── mixin.js // 將vuex實體掛載到全域Vue的$store上
├── store.js // 核心檔案,定義了Store類
└── util.js // 提供一些工具函式,例如: deepCopy、isPromise、isObject...
二、原始碼閱讀
1. 查看工具函式
首先我個人覺得肯定是要看一下 util.js ,這里面存放的是原始碼中頻繁用到的工具函式,所以我覺得要最先了解一下每個函式的作用是什么
/**
* Get the first item that pass the test
* by second argument function
*
* @param {Array} list
* @param {Function} f
* @return {*}
*/
// 找到陣列list中第一個符合要求的元素
export function find (list, f) {
return list.filter(f)[0]
}
/**
* 深拷貝
*
* @param {*} obj
* @param {Array<Object>} cache
* @return {*}
*/
export function deepCopy (obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}
// if obj is hit, it is in circular structure
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive deepCopy
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
// 遍歷obj物件的每個屬性的值
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
// 判斷是否為物件(排除null)
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
// 判斷是否為Promise物件
export function isPromise (val) {
return val && typeof val.then === 'function'
}
// 斷言
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
// 保留原始引數的閉包函式
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
每個函式的作用我都寫上了注釋,稍微閱讀一下應該可以明白其作用
2. 入口檔案
最主要的代碼都在 src 目錄下,所以以下提到的檔案都是默認 src 目錄下的檔案
首先,肯定從入口檔案 index.js 開始看,但能發現的是,還有 index.cjs 和 index.mjs ,這兩者分別是 commonjs 和 es6 module 的打包入口,我們就不用管了
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
export {
Store,
install,
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
從入口檔案中可以看到,主要匯出了 Store 類 、install 方法以及一些輔助函式(mapState、mapMutations、mapGetters…)
那么我們主要看的就是 vuex 的核心代碼,即 store.js ,可以看到 Store 類就出自于這個檔案
3. Store類的實作
整個 Store 類的主要邏輯都在它的建構式 constructor 中,因此我們就從 constructor 中分步去捋邏輯、看代碼
3.1 存放類的狀態
首先是定義了一些實體狀態,用于存放模塊、mutations 、actions 、getters 快取等東西
const {
plugins = [],
strict = false
} = options // 生成Store類的入參
this._committing = false // 表示提交的狀態,當通過mutations方法改變state時,該狀態為true,state值改變完后,該狀態變為false; 在嚴格模式下會監聽state值的改變,當改變時,_committing為false時,會發出警告,即表明state值的改變不是經過mutations的
this._actions = Object.create(null) // 用于記錄所有存在的actions方法名稱(包括全域的和命名空間內的,且允許重復定義)
this._actionSubscribers = [] // 存放actions方法訂閱的回呼函式
this._mutations = Object.create(null) // 用于記錄所有存在的的mutations方法名稱(包括全域的和命名空間內的,且允許重復定義)
this._wrappedGetters = Object.create(null) // 收集所有模塊包裝后的的getters(包括全域的和命名空間內的,但不允許重復定義)
this._modules = new ModuleCollection(options) // 根據傳入的options配置,注冊各個模塊,此時只是注冊、建立好了各個模塊的關系,已經定義了各個模塊的state狀態,但getters、mutations等方法暫未注冊
this._modulesNamespaceMap = Object.create(null) // 存盤定義了命名空間的模塊
this._subscribers = [] // 存放mutations方法訂閱的回呼
this._watcherVM = new Vue() // 用于監聽state、getters
this._makeLocalGettersCache = Object.create(null) // getters的本地快取
關于各個變數狀態的作用都寫在這了,其中只有 this._modules = new ModuleCollection(option) 執行了一些操作,其作用就是進行模塊遞回收集,根據 ModuleCollection 的來源,我們移步到 ./module/module-collection.js 檔案
3.1.1 遞回收集模塊
在 Module-collection.js 檔案中定義了 ModuleCollection 類,其作用就是通過遞回遍歷 options 入參,將每個模塊都生成一個獨立的 Moudle
這里先來熟悉一下 options 的結構,如下:
import Vuex from 'vuex'
const options = {
state: {...},
getters: {...},
mutations: {...},
actions: {...},
modules: {
ModuleA: {
state: {...},
...
modules: {
ModuleA1: {...}
}
},
ModuleB: {
state: {...},
...
modules: {
ModuleB1: {...}
}
}
}
}
const store = new Vuex.Store(options)
export default store
可以看到傳入的 options 整體可以看成一個根模塊 root ,然后 root 的 modules 中嵌套著另外兩個子模塊:ModuleA 和ModuleB ,而 ModuleA 和ModuleB 內部也分別嵌套著一個子模塊,分別為 ModuleA1 、ModuleB1 ,這樣就組成了一個模塊樹,因此 ModuleCollection 類的作業就是將保留原來的模塊關系,將每個模塊封裝到一個 Module 類中
export default class ModuleCollection {
constructor (rawRootModule) {
// 遞回注冊模塊
this.register([], rawRootModule, false)
}
// 根據路徑順序,從根模塊開始遞回獲取到我們準備添加新的模塊的父模塊
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
// 遞回注冊模塊
register (path, rawModule, runtime = true) {
if (__DEV__) {
assertRawModule(path, rawModule)
}
const newModule = new Module(rawModule, runtime) // 初始化一個新的模塊
if (path.length === 0) { // 當前沒有別的模塊
this.root = newModule // 則此模塊為根模塊
} else { // 有多個模塊
const parent = this.get(path.slice(0, -1)) // 獲取到新模塊從屬的父模塊,所以是path.slice(0, -1),最后一個元素就是我們要添加的子模塊的名稱
parent.addChild(path[path.length - 1], newModule) // 在父模塊中添加新的子模塊
}
if (rawModule.modules) { // 如果有嵌套模塊
/**
* 1. 遍歷所有的子模塊,并進行注冊;
* 2. 在path中存盤除了根模塊以外所有子模塊的名稱
* */
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
函式作用:
register(path, rawModule, runtime):注冊新的模塊,并根據模塊的嵌套關系,將新模塊添加作為對應模塊的子模塊
- path:表示模塊嵌套關系,當前為根模塊時,沒有任何嵌套關系,此時
path = []; 當前不是根模塊時,存在嵌套關系,例如上述例子中的ModuleA1,它是ModuleA的子模塊 ,而ModuleA又是根模塊的子模塊,此時path = ['ModuleA', 'ModuleA1'] - rawModule:表示模塊物件,此時是一個物件型別
- runtime:表示程式運行時
get(path):根據傳入的path路徑,獲取到我們想要的Module類
ModuleCollection 的建構式中呼叫了 register 函式,前兩個引數分別為:[] 、rawRootModule ,此時肯定是從根模塊開始注冊的,所以 path 里無內容,并且 rawRootModule 指向的是根模塊
然后來看一下 register 函式里的邏輯,
-
首先將當前要注冊的模塊生成一個
Module,并將rawModule作為引數,用于存放Module的資訊 -
然后通過
if(path.length === 0)判斷是否為根模塊,是的話就將this.root指向Module; 否則就跳到第3步 -
判斷當前模塊不是根模塊,就通過
get函式找到當前模塊的父模塊,然后呼叫父模塊中的addChild方法將當前模塊添加到子模塊中 -
最后再判斷當前模塊是否還有嵌套的模塊,有的話就重新回到第1步進行遞回操作 ; 否則不做任何處理
按照上面的邏輯,就可以將所有的模塊遞回收集并注冊好了,其中有一個 Module 類還沒有具體提到,所以這里移步到 ./module/module.js
import { forEachValue } from '../util'
// 定義了Vuex中的 Module 類,包含了state、mutations、getters、actions、modules
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null) // 創建一個空物件,用于存放當前模塊的子模塊
this._rawModule = rawModule // 當前模塊的一些資訊,例如:state、mutations、getters、actions、modules
const rawState = rawModule.state // 1. 函式型別 => 回傳一個obj物件; 2. 直接獲取到obj物件
// 存盤當前模塊的state狀態
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// 判斷該模塊是否定義了namespaced,定義了則回傳true; 否則回傳false
get namespaced () {
return !!this._rawModule.namespaced
}
// 添加子模塊,名稱為key
addChild (key, module) {
this._children[key] = module
}
// 移除名稱為key的子模塊
removeChild (key) {
delete this._children[key]
}
// 獲取名稱為key的子模塊
getChild (key) {
return this._children[key]
}
// 是否存在名稱為key的子模塊
hasChild (key) {
return key in this._children
}
// 將當前模塊的命名空間更新到指定模塊的命名空間中,并同時更新一下actions、mutations、getters的呼叫來源
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
// 遍歷當前模塊的所有子模塊,并執行回呼操作
forEachChild (fn) {
forEachValue(this._children, fn)
}
// 遍歷當前模塊的所有getters,并執行回呼操作
forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
// 遍歷當前模塊的所有actions,并執行回呼操作
forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
// 遍歷當前模塊的所有mutations,并執行回呼操作
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
來看一下剛才模塊收集時,創建的 Module 類內部做了什么事情,同樣的從 constructor 中開始看
this._children 是一個物件值,用于存放該模塊嵌套的其它 Module 類 ;
this._rawModule 就是用于存放該模塊內部的一些資訊,例如:state 、mutations 、actions 、getters 、moudles ;
this.state 對應的就是 this._rawModule 中的 state ;
這是整個建構式中執行的操作,我們可以看到,在生成一個 Module 類的時候,其只定義了 state 屬性,而 mutations 、getters 、actions 、modules 都是沒有被定義的,即例如現在是無法通過 Module.mutations 獲取到該模塊所有的 mutations 方法,那么這些方法都是在何時被定義的呢?自然是等模塊全部都收集完畢以后才進行的操作,因為 vuex 中的嵌套模塊可能會存在命名空間 namespaced
3.2 注冊模塊
到此為止,各個模塊的類都創建好了,那么繼續回到 ./src/store.js 的 constructor 建構式中
// 將 dispatch 和 commit 方法系結到 Store 的實體上,避免后續使用dispatch或commit時改變了this指向
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)
}
// 判斷store是否未嚴格模式,true: 所有的state都必須經過mutations來改變
this.strict = strict
// 將根模塊的state賦值給state變數
const state = this._modules.root.state
這段代碼首先對 Store 實體上的 dispatch 和 commit 方法進行了一層包裝,即通過 call 將這兩個方法的作用物件指向當前的 Store 實體,這樣就能防止后續我們操作時,出現 this.$store.dispatch.call(obj, 1) 類似的情況而報錯
this.strict 是用于判斷是否是嚴格模式,因為 vuex 中,建議所有的 state 變數的變化都必須經過 mutations 方法,因為這樣才能被 devtool 所記錄下來,所以在嚴格模式下,未經過 mutations 而直接改變了 state 的值,開發環境下會發出警告??
const state = this._modules.root.state 獲取的是根模塊的 state ,用于后續的一些操作
一切都準備就緒了,下面就開始為每個模塊注冊資訊了
// 從根模塊開始,遞回完善各個模塊的資訊
installModule(this, state, [], this._modules.root)
呼叫了 installModule 方法,并將 store 實體物件 、state 屬性 、路徑 、根模塊物件依次作為引數進行傳遞
// 注冊完善各個模塊內的資訊
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length // 是否為根模塊
const namespace = store._modules.getNamespace(path) // 獲取當前模塊的命名空間,格式為:second/ 或 second/third/
// 如果當前模塊設定了namespaced 或 繼承了父模塊的namespaced,則在modulesNamespaceMap中存盤一下當前模塊
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
// 如果不是根模塊,將當前模塊的state注冊到其父模塊的state上
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1)) // 獲取父模塊的state
const moduleName = path[path.length - 1] // 當前模塊的名稱
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
// 將當前模塊的state注冊在父模塊的state上,并且是回應式的
Vue.set(parentState, moduleName, module.state)
})
}
// 設定當前模塊的背景關系context
const local = module.context = makeLocalContext(store, namespace, path)
// 注冊模塊的所有mutations
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key // 例如:first/second/mutations1
registerMutation(store, namespacedType, mutation, local)
})
// 注冊模塊的所有actions
module.forEachAction((action, key) => {
/**
* actions有兩種寫法:
*
* actions: {
* AsyncAdd (context, payload) {...}, // 第一種寫法
* AsyncDelete: { // 第二種寫法
* root: true,
* handler: (context, payload) {...}
* }
* }
*/
const type = action.root ? key : namespace + key // 判斷是否需要在命名空間里注冊一個全域的action
const handler = action.handler || action // 獲取actions對應的函式
registerAction(store, type, handler, local)
})
// 注冊模塊的所有getters
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)
})
}
const namespace = store._modules.getNamespace(path) 是將路徑 path 作為引數, 呼叫 ModuleCollection 類實體上的 getNamespace 方法來獲取當前注冊物件的命名空間的
/**
* 根據模塊是否有命名空間來設定一個路徑名稱
* 例如:A為父模塊,B為子模塊,C為子孫模塊
* 1. 若B模塊命名空間為second,C模塊未設定命名空間時; C模塊繼承了B模塊的命名空間,為 second/
* 2. 若B模塊未設定命名空間, B模塊命名空間為third; 則此時B模塊繼承的是A模塊的命名空間,而C模塊的命名空間路徑為 third/
*/
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key) // 獲取子模塊
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
從這可以看出,未指定命名空間的模塊會繼承父模塊的命名空間
// 如果當前模塊設定了namespaced 或 繼承了父模塊的namespaced,則在modulesNamespaceMap中存盤一下當前模塊
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
這段代碼是將所有存在命名空間的模塊記錄在 store._modulesNamespaceMap 中,便于之后的輔助函式可以呼叫(這里還未提到輔助函式,可以先不管,到時候回頭來看)
3.2.1 注冊模塊的state
// 如果不是根模塊,將當前模塊的state注冊到其父模塊的state上
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1)) // 獲取父模塊的state
const moduleName = path[path.length - 1] // 當前模塊的名稱
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
// 將當前模塊的state注冊在父模塊的state上,并且是回應式的
Vue.set(parentState, moduleName, module.state)
})
}
這段代碼主要是將非根模塊的 state 掛載到父模塊的 state 上
const parentState = getNestedState(rootState, path.slice(0, -1)) 根據當前的模塊路徑,從根模塊的 state 開始找,最終找到當前模塊的父模塊的 state,可以看一下 getNestedState 方法內部的具體實作
// 獲取到嵌套的模塊中的state
function getNestedState (state, path) {
return path.reduce((state, key) => state[key], state)
}
const moduleName = path[path.length - 1] 從路徑 path 中將當前模塊的名稱提取出來
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
// 將當前模塊的state注冊在父模塊的state上,并且是回應式的
Vue.set(parentState, moduleName, module.state)
})
這段代碼中最主要的部分就是 Vue.set(parentState, moduleName, module.state) ,作用就是呼叫了 Vue 的 set 方法將當前模塊的 state 回應式地添加到了父模塊的 state 上,這是因為在之后我們會看到 state 會被放到一個新的 Vue 實體的 data 中,所以這里不得不使用 Vue 的 set 方法來回應式地添加
同樣的,從這段代碼中我們也可以知道了為什么平時在獲取子模塊上 state 的屬性時,是通過 this.$store.state.ModuleA.name 這樣的形式來獲取的了
3.2.2 生成模塊呼叫背景關系
// 設定當前模塊的背景關系context
const local = module.context = makeLocalContext(store, namespace, path)
這行代碼也可以說是非常核心的一段代碼了,它根據命名空間為每個模塊創建了一個屬于該模塊呼叫的背景關系,并將該背景關系賦值了給了該模塊的 context 屬性
接下來看一下這個背景關系是如何創建的吧
// 若設定了命名空間則創建一個本地的commit、dispatch方法,否則將使用全域的store
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) { // 若傳入了第三個引數設定了root:true,則派發的是全域上對應的的actions方法
type = namespace + type
if (__DEV__ && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
},
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) { // 若傳入了第三個引數設定了root:true,則派發的是全域上對應的的mutations方法
type = namespace + type
if (__DEV__ && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
/**
* 若沒有設定命名空間,則直接讀取store.getters(store.getters已經掛載到vue實體的computed上了);
* 若設定了命名空間,則從本地快取_makeLocalGettersCache中讀取getters
*/
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
local 這個變數存盤的就是一個模塊的背景關系,
先來看其第一個屬性 dispatch ,當該模塊沒有設定命名空間時,呼叫該背景關系的 dispatch 方法時會直接呼叫 sotre.dispatch ,即呼叫了根模塊的 dispatch 方法 ; 而存在命名空間時,會先判斷相應的命名空間,以此來決定呼叫哪個 dispatch 方法
if (!options || !options.root) 是判斷呼叫 dispatch 方法時有沒有傳入第三個引數 {root: true} ,若有則表示呼叫全域根模塊上對應的的 dispatch 方法
那么同樣的,local 中的 commit 屬性就類似于 dispatch ,這里就不多說了
然后最后通過 Object.defineProperties 方法對 local 的 getters 屬性和 state 屬性設定了一層獲取代理,等后續對其訪問時,才會進行處理,例如,訪問 getters 屬性時,先判斷是否存在命名空間,若沒有,則直接回傳 store.getters ; 否則的話,根據命名空間創建一個本地的 getters 快取,根據這個快取來獲取對應的 getters ,來看一下代碼
// 創建本地的getters快取
function makeLocalGetters (store, namespace) {
// 若快取中沒有指定的getters,則創建一個新的getters快取到__makeLocalGettersCache中
if (!store._makeLocalGettersCache[namespace]) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// 如果store.getters中沒有與namespace匹配的getters,則不進行任何操作
if (type.slice(0, splitPos) !== namespace) return
// 獲取本地getters名稱
const localType = type.slice(splitPos)
// 對getters添加一層代理
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
})
})
// 把代理過的getters快取到本地
store._makeLocalGettersCache[namespace] = gettersProxy
}
return store._makeLocalGettersCache[namespace]
}
當存在命名空間時訪問 local.getters ,首先會去 store._makeLocalGettersCache 查找是否有對應的 getters 快取,若沒有,則創建一個 gettersProxy ,在 store.getters 上找到對應的 getters ,然后用 Object.defineProperty 對 gettersProxy 做一層處理,即當訪問 local.getters.func 時,相當于訪問了 store.getters['first/func'] ,這樣做一層快取,下一次訪問該 getters 時,就不會重新遍歷 store.getters 了 ; 若有快取,則直接從快取中獲取
背景關系已經創建好了,接下來就是注冊 mutations 、actions 、getters 了
3.2.3 注冊模塊的mutations
// 注冊模塊的所有mutations
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key // 例如:first/second/mutations1
registerMutation(store, namespacedType, mutation, local)
})
這里遍歷了模塊的所有 mutations 方法,通過命名空間 + mutations 方法名的形式生成了 namespacedType
然后跳到 registerMutations 方法看看具體是如何注冊的
// 注冊mutations方法
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = []) // 通過store._mutations 記錄所有注冊的mutations
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
首先根據我們傳入的 type 也就是上面的 namespacedType 去 store._mutations 尋找是否有入口 entry ,若有則直接獲取 ; 否則就創建一個空陣列用于存盤 mutations 方法
在獲取到 entry 以后,將當前的 mutations 方法添加到 entry 末尾進行存盤,其中 mutations 接收的引數有兩個,即 背景關系中的 state 和 我們傳入的引數 payload
從這段代碼我們可以看出,整個 store 實體的所有 mutations 方法都是存盤在 store._mutations 中的,并且是以鍵值對的形式存放的,例如:
store._mutations = {
'mutations1': [function handler() {...}],
'ModuleA/mutations2': [function handler() {...}, function handler() {...}],
'ModuleA/ModuleB/mutations2': [function handler() {...}]
}
其中鍵是由命名空間和 mutations 方法名組成的,值是一個陣列,存放著所有該鍵對應的 mutations 方法
為什么是用陣列存放呢?因為在上面說過,假設父模塊ModuleA 里有一個叫 func 的 mutations 方法,那么其在 store._mutations 中就是這個樣子的
store._mutations = {
'ModuleA/func': [function handler() {...}]
}
若子模塊沒有設定命名空間,那么他是會繼承父模塊的命名空間的,此時子模塊里也有一個叫 func 的 mutations 方法,那么在獲取 entry 時,獲取到的是 store._mutations['ModuleA/func'] ,但此時這個 entry 中已經有一個 mutations 方法了,那么為了保證之前的方法不被替換,就選擇添加到陣列的末尾,此時應該就可以猜測到了,后續如果呼叫該 mutations 方法,會先獲取到相應的陣列,然后遍歷依次執行
得出個結論:mutations 方法是可以重名的
3.2.4 注冊模塊的actions
// 注冊模塊的所有actions
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key // 判斷是否需要在命名空間里注冊一個全域的action
const handler = action.handler || action // 獲取actions對應的函式
registerAction(store, type, handler, local)
})
遍歷模塊的所有 actions 方法,其中對于 type 和 handler 的處理主要是為了兼容兩種寫法:
// 第一種寫法:
actions: {
func(context, payload) {
// 省略業務代碼...
}
}
// 第二種寫法:
actions: {
func: {
root: true,
handler(context, payload) {
// 省略業務代碼...
}
}
}
當采用第二種寫法,并且 root = true 時,就會將該 actions 方法注冊到全域上,即前面不加上任何的命名空間前綴
再來看看 registerAction 方法里具體實作了什么
// 注冊actions方法,接收兩個引數:context(包含了背景關系中的dispatch方法、commit方法、getters方法、state)、傳入的引數payload
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = []) // 通過store._actions 記錄所有注冊的actions
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物件,則包裝一層promise,并將回傳值作為then的引數
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
}
})
}
與 mutations 類似,先從 store._actions 獲取入口 entry ,然后將當前的 actions 進行包裝處理后添加到 entry 的末尾, actions 方法接收兩個引數,即 context 和我們傳入的引數 payload ,其中 context 是一個物件,里面包含了 dispatch 、commit 、getters 、state 、rootGetters 、rootState ,前4個都是在當前模塊的背景關系中呼叫的,后2個是在全域上呼叫的
最后對于 actions 的回傳值還做了一層處理,因為 actions 規定是處理異步任務的,所以我們肯定希望其值是一個 promise 物件,這樣方便后續的操作,所以這里對 actions 方法的回傳值做了一個判斷,如果本身就是 promise 物件,那么就直接回傳 ;若不是,則包裝一層 promise 物件,并將回傳值 res 作為引數回傳給 .then
同樣的,actions 方法也是可以重名的
3.2.5 注冊模塊的getters
// 注冊模塊的所有getters
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
與上面的類似,這里就不多說了,直接跳到 registerGetters 方法
// 注冊getters
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) { // 若記錄過getters了,則不再重復記錄
if (__DEV__) {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
// 在store._wrappedGetters中記錄getters
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
這里發現 getters 并不像 mutations 和 actions 一樣去獲取一個 entry ,而是直接查看 store._wrappedGetters[type] 是否有對應的 getters ,若有,則不再重復記錄 ; 否則將 getters 包裝一下存在 sotre._wrappedGetters 中,其中經過包裝后的 getters 接收4個引數,即 state 、getters 、rootState 、rootGetters ,前2個分別表示當前背景關系中的 state 和 getters ,后2個分別表示根模塊的 state 和 getters
所以我們在使用 Vuex 時,呼叫子模塊的 getters 時是這樣的:
const store = Vuex.Store({
state: {
a: 1,
b: 2
},
getters: {
addA(state) {
return state.a + 1
}
},
modules: {
// 子模塊A
ModuleA: {
state: {
c: 3
},
getters: {
sum(state, getters, rootState, rootGetters) {
console.log(state.c) // 3
console.log(getters.addC) // 4
console.log(rootState.b) // 2
console.log(rootGetters.addA) // 2
},
addC(state) {
return state.c + 1
}
}
}
}
})
最后我們再次得出一個結論,getters 是不能重名的,并且前一個命名的不會被后一個命名的所覆寫
3.2.6 遞回注冊子模塊
// 遞回注冊子模塊
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
然后就是判斷當前的模塊里有沒有嵌套的子模塊了,有的話就將子模塊的名稱添加到 path 末尾,然后把相應的引數傳入 installModule 方法,重新走一遍本文中 3.2 里所有的流程
3.3 注冊vm
上面已經將模塊的注冊完畢了,看一下 constructor 中下一行代碼是什么:
resetStoreVM(this, state)
跳到相應的方法中去看一下:
// 初始化vm
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {} // 在實體store上設定getters物件
store._makeLocalGettersCache = Object.create(null) // 清空本地快取
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍歷getters,將每一個getter注冊到store.getters,訪問對應getter時會去vm上訪問對應的computed
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(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
// 使用Vue實體來存盤Vuex的state狀態樹,并利用computed去快取getters回傳的值
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// 啟用嚴格模式的監聽警告
if (store.strict) {
enableStrictMode(store)
}
// 若存在舊的vm, 銷毀舊的vm
if (oldVm) {
if (hot) {
// 解除對舊的vm對state的參考
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
這個方法里主要做的就是生成一個 Vue 的實體 _vm ,然后將 store._makeLocalGettersCache 里的 getters 以及 store.state 交給一個 _vm 托管,即將 store.state 賦值給 _vm.data.$$state ,將 store._makeLocalGettersCache 通過轉化后賦值給 _vm.computed ,這樣一來,state 就實作了回應式,getters 實作了類似 computed 的功能
因為生成了新的 _vm ,所以最后通過 oldVm.$destory() 將舊的 _vm 給銷毀掉了
值得注意的是,其將 sotre.getters 的操作放在了這個方法里,是因為我們后續訪問某個 getters 時,訪問的其實是 _vm.computed 中的內容,因此,通過 Object.defineProperty 對 store.getters 進行了處理
3.4 訪問 state 、mutations 、actions
到此為止,已經實作了可以通過 store.getter.某個getters 來使用 getters ,那么如何訪問 state 、mutations 、actions 呢?
3.4.1 訪問 state
通過搜索,在 Store 類中定義了一個 get 函式,用于處理 store.state 的操作:
get state () {
return this._vm._data.$$state
}
可以很清楚地看到,當我們訪問 store.state 時,就是去訪問 store._vm.data.$$state ,與剛才介紹 _vm 時說的一樣
3.4.2 訪問 mutations
其實 mutations 的訪問在一開始就觸及到了,只不過當時只是提了一嘴,因為當時直接來看可能不會太明白
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)
}
在 Store 中,對 store.commit 和 store.dispatch 方法做了一層處理,將該方法的呼叫指向了 store ,先來看看 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] // 查找_mutations上是否有對應的方法
// 查找不到則不執行任何操作
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 若有相應的方法,則執行
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(sub => sub(mutation, this.state))
if (
__DEV__ &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
首先通過 unifyObjectStyle 方法對傳入的引數進行了處理,來看一下這個方法是干什么的
function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
if (__DEV__) {
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}
return { type, payload, options }
}
使用過 Vuex 的應該都知道,commit 有兩種提交方式:
// 第一種提交方式
this.$store.commit('func', 1)
// 第二種提交方式
this.$store.commit({
type: 'func',
num: 1
})
其先對第一個引數進行判斷是否為物件,是的話就當作物件提交風格處理,否則的話就直接回傳
在處理完引數以后,根據 type 從 store._mutations 上獲取到 entry ,前面分析過了,mutations 方法是以陣列形式存盤的,所以可能有多個方法,然后在 _withCommit 方法中遍歷 entry 依次執行 mutations 方法,這是因為 Vuex 規定 state 的改變都要通過 mutations 方法,store._committing 這個屬性就是用來判斷當前是否處于呼叫 mutations 方法的,當 state 值改變時,會先去判斷 store._committing 是否為 true ,若不為 true ,則表示 state 的值改變沒有經過 mutations 方法,于是會列印警告?? 資訊
而 this._subscribers 這段代碼我暫時還不清楚是干什么的,通過詞義,目測應該是一個存放訂閱的東西吧,就先放著不管了,等后續回來再看
3.4.3 訪問 actions
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type] // 查找_actions上是否有對應的方法
// 查找不到則不執行任何操作
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
try {
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return new Promise((resolve, reject) => {
result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
resolve(res)
}, error => {
try {
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in error action subscribers: `)
console.error(e)
}
}
reject(error)
})
})
}
前半部分與 commit 方法類似,就不多說了
代碼中又出現了 this._actionSubscribers ,與 commit 中的也類似,可能這里是存放 actions 的訂閱者的東西,所以這些都先不看了
其中變數 result ,先判斷 entry 的長度,若大于1,則表示有多個異步方法,所以用 Promise.all 進行包裹 ; 否則直接執行 entry[0]
最后創建并回傳了一個新的 promise ,內部判斷了 result 的狀態,成功則執行 resolve ,失敗則執行 reject
到此為止,我們已經實作了 store.state 、store.getters 、store.commit 、store.dispatch 的呼叫了
3.5 插件的呼叫
繼續看 constructor 中的代碼(這段代碼也是整個 Store 類的建構式中最后的一小段代碼了)
// 依次呼叫傳入的插件
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
// 使用vue的開發插件
if (useDevtools) {
devtoolPlugin(this)
}
首先就是遍歷創建 Store 類時傳入的引數 Plugins ,依次呼叫傳入的插件函式(當然一般我們都沒有傳入,所以 Plugins 默認是空陣列)
然后就是呼叫 devtoolPlugin 方法啦,根據匯入的路徑我們去到相應的檔案
// 檔案路徑:./plugins/devtool.js
const target = typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__
export default function devtoolPlugin (store) {
if (!devtoolHook) return
store._devtoolHook = devtoolHook
devtoolHook.emit('vuex:init', store)
devtoolHook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
}, { prepend: true })
store.subscribeAction((action, state) => {
devtoolHook.emit('vuex:action', action, state)
}, { prepend: true })
}
看了半天,搜索了半天,都沒有找到哪個檔案里有 __VUE_DEVTOOLS_GLOBAL_HOOK__ ,應該是 dev-tools 插件里定義的,為了保證 Vuex 的原始碼閱讀進度,就先舍棄閱讀 dev-tools 插件的內容了
3.6 其它方法
整個 Store 實體生成的全程序差不多就是這樣了,另外還會發現,其實有很多方法都沒有被用到,但是卻被定義出來了,這里可以稍微列舉幾個簡單地看一下
3.6.1 更新 state
// 在store._committing = true 的狀態下更新一下state
replaceState (state) {
this._withCommit(() => {
this._vm._data.$$state = state
})
}
一目了然,這是提供了一種直接修改 state 的方法,并且不會列印警告資訊
3.6.2 注冊、卸載模塊
// 注冊模塊
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
if (__DEV__) {
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)
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
resetStoreVM(this, this.state)
}
// 卸載模塊
unregisterModule (path) {
if (typeof path === 'string') path = [path]
if (__DEV__) {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}
this._modules.unregister(path)
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
}
3.6.3 重置 store 實體
// 重置store,即注冊模塊、生成vm等操作
function resetStore (store, hot) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
store._modulesNamespaceMap = Object.create(null)
const state = store.state
// init all modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}
將所有的狀態都清空,然后重新執行一邊 installModule 和 resetStoreVM ,這一般在模塊結構變化以后呼叫,例如某個模塊被卸載
4. install 注冊
Store 類的所有實作都了解完了,再來看一下入口檔案里還有什么,突然發現忘記看一下非常重要的 install 方法了,根據 install 方法的匯入路徑找到相應的函式:
// 提供install方法
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
當我們呼叫 Vue.use(vuex) 時,呼叫這個方法,先判斷 vuex 是否已被注冊,若已被注冊,則不執行任何操作 ; 若沒有被注冊,則呼叫 applyMixin 方法,現在移步到 ./mixin.js 檔案:
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
// 2.x版本直接通過全域混入Vue.mixin的方式掛載store
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 兼容1.x版本
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
// 將vuex混入到$options中
function vuexInit () {
// 獲取當前組件的 $options
const options = this.$options
// 若當前組件的$options上已存在store,則將$options.store賦值給this.$store(一般是用于根組件的)
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
}
// 當前組件的$options上沒有store,則獲取父組件上的$store,即$options.parent.$store,并將其賦值給this.$store(一般用于子組件)
else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
applyMixin 方法先判斷了 Vue 的版本號,主要做的是一個向下兼容 Vue 1.x 的版本,這里我對 Vue 1.x 的版本不太熟悉,所以就直接看 Vue 2.x 版本的處理方式吧
通過 Vue.minxin 方法做了一個全域的混入,在每個組件 beforeCreate 生命周期時會呼叫 vuexInit 方法,該方法處理得非常巧妙,首先獲取當前組件的 $options ,判斷當前組件的 $options 上是否有 sotre ,若有則將 store 賦值給當前組件,即 this.$store ,這個一般是判斷根組件的,因為只有在初始化 Vue 實體的時候我們才手動傳入了 store ; 若 $options 上沒有 store ,則代表當前不是根組件,所以我們就去父組件上獲取,并賦值給當前組件,即當前組件也可以通過 this.$store 訪問到 store 實體了
這里不得不感嘆,這個處理方式太棒了,
5. 輔助函式
store實體生成并且也 install 到 Vue 上了,看一下入口檔案中只剩下輔助函式了,它們有 mapState 、mapGetters 、mapMutations 、mapActions 、createNamespacedHelpers ,進到相應的檔案 ./helpers.js 中看一下
import { isObject } from './util.js'
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
if (__DEV__ && !isValidMap(states)) {
console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
if (__DEV__ && !isValidMap(mutations)) {
console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
}
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
})
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
if (__DEV__ && !isValidMap(getters)) {
console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
}
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (__DEV__ && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
if (__DEV__ && !isValidMap(actions)) {
console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
/**
* Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
* @param {String} namespace
* @return {Object}
*/
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})
function normalizeMap (map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
function isValidMap (map) {
return Array.isArray(map) || isObject(map)
}
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)
}
}
function getModuleByNamespace (store, helper, namespace) {
const module = store._modulesNamespaceMap[namespace]
if (__DEV__ && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
整個檔案里東西非常多,但我們很明確地知道,我們主要看的就是那幾個輔助函式,觀察發現,每個輔助函式都會先呼叫 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)
}
}
根據函式名的字面意思知道這應該是根據不同的呼叫方法,標準化命名空間的,
首先回傳一個函式,接收兩個引數,即 namespace 和 map ,這也是我們呼叫輔助函式時可以傳入的兩個引數 ;
然后判斷 namespace 是否為字串形式,若不是字串,則表示是普通的呼叫方式,例如:
mapMutations(['first/second/foo', 'first/second/bar'])
mapMutations({
foo: 'first/second/foo',
bar: 'first/second/bar',
})
這種情況,就直接將第一個引數 namespace 賦值給映射變數 map ,而 namespace 設為空
若是字串的話,則表示呼叫的是帶命名空間的系結函式的,例如:
mapState('first/second', ['foo', 'bar'])
mapState('first/second', {
foo: 'foo',
bar: 'bar',
})
處理好這兩種不同的呼叫方式以后,呼叫一下 fn ,并將 namespace 和 map 作為引數
那么就先從 mapState 開始看吧
5.1 mapState
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
if (__DEV__ && !isValidMap(states)) {
console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
這里的 namespace 是一個字串,states 是我們剛才處理好的映射變數 map
首先創建一個空物件 res ,這是我們最后處理好要回傳的變數 ;
然后通過 isValidMap 方法判斷 map 是否符合要求,即是否是陣列或物件 ;
再然后呼叫了 normalizeMap 方法處理了變數 states ,從字面意義上來看,這是用來標準化該變數的,因為畢竟有可能是陣列又有可能是物件嘛,所以要統一一下,來看一下 normalizeMap 方法的實作:
function normalizeMap (map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
首先仍然要先判斷 map 是否合法,若不合法,則回傳空陣列,避免后續的代碼報錯 ;
然后判斷 map 是否為陣列,若是陣列,則遍歷 map 進行處理:
將 [1, 2, 3] 變成 [{key: 1, val: 1}, {key: 2, val: 2}, {key: 3, val: 3}]
若 map 不是陣列,則一定為物件,那么同樣也要把其處理成跟上面一樣的格式:
將 {a: 1, b: 2, c: 3} 變成 [{key: a, val: 1}, {key: b, val: 2}, {key: c, val: 3}]
處理好了以后就直接回傳,在得到標準化以后的 map 后要對其進行 forEach 遍歷,將遍歷到的每一個物件經過處理后存放在 res 中,即 res[key] = function mappedState() {...} ,來看一下這個 mappedState 里做了什么處理
首先獲取一下根模塊上的 state 和 getters
// 獲取根模塊的 state 、getters
let state = this.$store.state
let getters = this.$store.getters
然后判斷是否存在命名空間,即 namespace 是否為空,若為空,則不做任何處理 ; 否則呼叫 getModuleByNamespace 方法獲取到 namespace 對應的模塊 module
function getModuleByNamespace (store, helper, namespace) {
const module = store._modulesNamespaceMap[namespace]
if (__DEV__ && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
可以看到 store._modulesNamespaceMap 終于派上了用場,在生成 Store 實體注冊所有模塊的時候,將帶有命名空間的模塊都存盤在了該變數上,原來是在這里用上了
然后將剛才宣告的變數 state 和 getters 替換成 module 對應背景關系中的 state 和 getters
if (namespace) {
// 獲取命名空間namespace對應的模塊
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
// 將 state 、getters 變成該模塊背景關系中的 state 、getters
state = module.context.state
getters = module.context.getters
}
這個 context 也是非常的巧妙,在注冊模塊的時候,獲取到該模塊的背景關系的同時,還將其存盤了一下,即:
const local = module.context = makeLocalContext(store, namespace, path)
之前看到的時候不知道有啥用,但在這里看到后,覺得真的非常得贊 👍
確定好了 state 和 getters 的值,最后就可以回傳值了
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
這里還做了一層處理是因為要處理兩種不同的方式,例如:
mapState({
foo: state => state.foo,
bar: 'bar'
})
在這里我又發現了一個官方檔案里沒有提及的,就是以函式形式回傳的時候,還能接收第二個引數 getters ,即:foo: (state, getters) => state.foo + getters.bar
5.2 mapMutations
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
if (__DEV__ && !isValidMap(mutations)) {
console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
}
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
})
mapMutations 與 mapState 的實作大體相似,主要的不同就在下面這段代碼:
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
這里也是像 mapState 一樣處理了函式的呼叫型別和普通的呼叫型別,例如:
mapMutations({
foo: (commit, num) => {
commit('foo', num)
},
bar: 'bar'
})
當是函式的呼叫型別時,則將 commit 作為第一個引數,并把額外的引數一并傳入,所以才有的 val.apply(this, [commit].concat(args)) 這段代碼 ;
當是普通的呼叫型別時,則直接執行 commit ,其中 val 對應的就是該命名空間下需要呼叫的 mutations 方法名,然后再接收額外的引數,即 commit.apply(this.$store, [val].concat(args))
5.3 mapGetters
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
if (__DEV__ && !isValidMap(getters)) {
console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
}
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (__DEV__ && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
這個也沒什么好說的了,拿到命名空間 namespace ,直接拼接上 val 通過 this.$store.getters[val] 進行訪問,簡單舉個例子:
第一種情況
// 第一種
mapGetters(['first/foo'])
這種情況下 namespace 被處理成了空字串,map 被處理成了 ['first/foo'] ,遍歷 map ,此時 val = 'first/foo' ,那么 val = namespace + val 處理后 val 仍然等于 first/foo ,所以最后就相當于呼叫 this.$store.getters['first/foo']
再來看第二種情況
// 第二種
mapGetters('first', ['foo'])
這種情況下 namespace 被處理成了 first/,map 被處理成了 ['foo'] ,遍歷 map ,此時 val = 'foo' ,那么 val = namespace + val 處理后 val 就等于 first/foo ,所以最后仍然是相當于呼叫 this.$store.getters['first/foo']
5.4 mapActions
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
if (__DEV__ && !isValidMap(actions)) {
console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
簡單看了一下,處理流程跟 mapMutations 幾乎一模一樣,就不多說了
5.5 createNamespacedHelpers
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})
該方法是根據傳入的命名空間 namespace 創建一組輔助函式,巧妙之處就是先通過 bind 函式把第一個引數先傳入
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('first/second')
export default {
computed: {
...mapState({
a: 'a', // 相當于 first/second/a
b: 'b', // 相當于 first/second/b
})
},
methods: {
...mapActions([
'foo', // 相當于 first/second/foo
'bar', // 相當于 first/second/bar
])
}
}
💡 心得體會
首先,我一直有一個閱讀原始碼的想法,但卻因為能力有限遲遲沒有行動,之后在一次與大佬的交流中,我發現了自己的不足,沒有深入學習,即只停留在會用的階段,卻沒有做到知其然知其所以然,說實話,這樣真的很難受,每次用某個庫時,出現了某個問題只會先看考慮是否自己呼叫的方式有問題,然后上搜索引擎找答案,長期這樣自己也很難有進步,
所以,因為以下三點原因,我準備靠自己好好看一下 Vuex 原始碼:
Vuex的核心原始碼比較少,對于像我一樣第一次閱讀原始碼的人比較友好- 深入學習了常用的庫以后,在使用的時候遇到問題,可以快速地找到問題根源
- 不能只停留在成熟的庫的表面,要學習它們的思想、技術,這樣有助于自己的成長
剛開始不知道自己能花多久時間看完 Vuex 的核心原始碼,我初步給自己定了 15 天的期限,預計每天至少看 2 小時,于是我把 Vuex 的原始碼 fork 并 clone 了下來,第一天簡單地找了一下核心代碼的位置,然后非常粗略地看了一下原始碼里的大致流程,同時,我去 Vuex 官方檔案里重新仔仔細細地回顧了一下所有的核心使用方法
接下來的時間我就按照我本文的閱讀順序進行原始碼的閱讀
這里總結幾點閱讀原始碼的心得體會吧:
- 對于這個庫的使用一定要十分熟練,即明白各種方法的使用,強烈建議把官方檔案吃透(重點)
- 找到核心代碼的位置,從入口檔案開始,一步步看
- 多看原始碼中的英文注釋,看不懂的可以用翻譯,這些注釋基本上能幫你理解這段代碼的作用
- 遇到看不懂的地方可以先打個備注,因為它可能與后面的某些代碼有所聯系,等之后回頭來看之前看不懂的代碼時,就會明白了
- 閱讀原始碼的程序中,看到某些變數或函式時,先看命名,因為這些命名的字面意思基本上就代表了它的作用,然后要學會聯想到這個正常的呼叫是什么樣的,這樣更便于理解
- 多多利用編譯器的搜索功能,因為有時你看到的函式或變數可能在別的地方也有用到,為了方便尋找,可以利用好編譯器的搜索功能(包括當前本地搜索和全域搜索)


🌱 問答環節
這里放上幾個群友對于這次閱讀原始碼問我的問題:
Q1: 你是怎么看原始碼的?有看別人的視頻或者別人的文章嗎?
A1: 沒有看別人的視頻或者文章,就當時自己思考了一下該如何看原始碼,列了一個步驟,就這樣摸索著看完了,覺得還挺有意思的
Q2: 光自己看能看懂嗎?
A2: 說實話確實有些地方挺難看懂的,但結合著原始碼自帶的英文注釋,基本上能把大致的思路理清,然后看不懂的地方就先做上記號并暫時略過,等到看了更多的代碼了以后,回過頭來就發現似乎看懂了些,最后要說的就是,原始碼真不是一遍就能看懂的,真的是要反反復復多看幾遍,才能理解其中的原理
Q3: 看完原始碼后,你能自己手寫出來嗎?
A3: emmmm…這可能有點難度,但是我覺得手寫一些核心代碼,實作一個簡陋的 Vuex 還是可以做到的吧,而且我覺得很有必要自己再去手寫一下核心代碼,因為這又是一次對原始碼的鞏固,并且我也已經開始在寫一個簡陋版的 Vuex 了,放在倉庫的 myVuex 檔案夾下
📌 最后
若本文對于 Vuex 原始碼閱讀有任何錯誤的地方,歡迎大家給我提意見,一定虛心聽取你們的指正,覺得本倉庫 ?? Vuex原始碼閱讀倉庫 不錯的,也可以點個🌟 star 🌟 支持一下我,
最后,也可以關注我的公眾號:前端印象,或是添加我的微信(Lpyexplore333)私底下進行交流
這篇文章我真的很用心了,你們忍心不給點個贊 👍 嘛~
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/254102.html
標籤:其他
