該系列分爲上中下三篇:vue
在上一篇中,咱們大體瞭解了Vuex
的概念以及食用方式,這裏主要是從源碼的角度來詳細揭開Vuex
的神祕面紗。vuex
咱們以入門版食用方式加一些模塊爲例:api
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const vm = new Vue();
// module a
const moduleA = {
namespaced: true,
state: {
number: 0,
},
getters: {
getNumberPlusOne(state) {
return state.number + 1;
},
},
mutations: {
setNumber(state, num) {
state.number = num;
},
},
actions: {
async setNumberAsync({ commit }) {
const { data } = await vm.$http('/api/get-number');
commit('setNumber', data);
},
},
};
// main
export default new Vuex.Store({
state: {
count: 0,
},
mutations: {
setCount(state, count) {
state.count = count;
},
},
getters: {
getCountPlusOne(state) {
return state.count + 1;
},
},
actions: {
async setCountAsync({ commit }, count) {
const { data: count } = await vm.$http('/api/example', { count });
commit('setCount', count);
},
},
modules: {
moduleA,
},
});
複製代碼
下面一步步分析。數組
PS:未特殊說明路徑,代碼內容則在當前文件內。緩存
打開 vuex 源碼,咱們這裏採用的是 3.1.1 版本進行分析,找到 index.js
,內容以下:app
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
複製代碼
因而可知,咱們引入 Vuex 對象,包含了輔助函數以及 Store 對象、install 方法以及 createNamespacedHelpers 輔助函數。異步
咱們知道 Vue.use 方法會注入一個插件,調用這個對象的install
方法,咱們打開 store.js
找到最後的install
方法:async
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)
}
複製代碼
mixin.js 中的 applyMixin 方法:
ide
咱們精簡一下,核心內容以下:函數
export default function (Vue) {
Vue.mixin({ beforeCreate: vuexInit })
function vuexInit () {
const options = this.$options
// store injection
this.$store = typeof options.store === 'function'
? options.store()
: options.store
}
}
複製代碼
這樣看來,其實就是在 Vue beforeCreate 生命週期鉤子函數裏執行了 vuexInit 方法,將實例化的 Store 對象掛載到 $store 上,這也是爲何咱們能在 vue 組件中直接經過 this.$store 就能夠進行相關操做的緣由。
當咱們實例化一個 Store 的時候,進行了什麼操做呢?
咱們找到store.js
裏的 Store 類,省略一些不過重要的代碼,主要看到以下部分:
咱們省略一些不過重要的代碼,主要看到以下部分:
export class Store {
constructor (options = {}) {
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
if (process.env.NODE_ENV !== 'production') {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `store must be called with the new operator.`)
}
// ...
this._modules = new ModuleCollection(options)
// ...
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
}
}
複製代碼
上面幾個部分也就是主要的邏輯
首先是若是沒有傳入 Vue,那麼自動安裝一下插件,若是是開發環境則會報錯。
store 能夠拆分紅各類小的 store,那麼整個看起來就是一個樹形結構。store 自己至關於根節點,每個 module 都是一個子節點,那麼首先要作的就是獲取到這些子節點。
以後再將子節點裏的數據以及 getter、state 進行關聯。
咱們打開module/module-collection.js
文件,找到 ModuleCollection 類,以下:
export default class ModuleCollection {
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
}
複製代碼
能夠看到實例化的過程就是執行了 register 方法。
register (path, rawModule, runtime = true) {
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
複製代碼
這裏第一步是實例化了一個 Module 對象,這個類定義在 module/module.js
裏:
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
複製代碼
_children 是該模塊的子模塊,_rawModule 是該模塊的配置,state 是該模塊的 state。
返回到 register 方法繼續,實例化 Module 後,接下來第一次進入 path 是空數組,因此這就是根 store,賦值當前配置到 root 上。
接着判斷是否有 modules 項,若是有,則執行下面代碼:
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
複製代碼
這段代碼主要是遍歷 modules,遞歸調用 register 方法,將配置項裏的 modules 的 key 做爲路徑保存到 path 中,傳入子 module 和建立狀態。(以開始的例子,key 就是 'moduleA')
第二次進入 register,此時走到if (path.length === 0) {}
的判斷,因爲此時 path 已經有內容了,因此會執行 else 的邏輯:
const parent = this.get(path.slice(0, -1)) // 這裏至關於 path 彈出了最後一項
parent.addChild(path[path.length - 1], newModule)
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
// module.js 裏的 addChild、getChild
addChild (key, module) {
this._children[key] = module
}
getChild (key) {
return this._children[key]
}
複製代碼
首先獲取父模塊,這裏經過 get 方法中的 reduce,層層遞進深度搜索出當前模塊的父模塊而後返回。
經過 Module 實例的 addChild 方法給掛載到 _children 上,(例子中至關於 key: 'moduleA', value: moduleA 對象)。
這樣遞歸註冊,就對全部的模塊進行實例化,經過 _children 創建好父子關係,一顆組件樹就構建完成了。
當咱們構建好模塊樹,接下來就須要去安裝這些模塊了,截取 installModule 方法代碼以下:
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
複製代碼
這裏的主要邏輯就是初始化 state、getters、mutations、actions,這裏有 5 個參數,分別表明如下意思:
第一步定義 isRoot 變量用來判斷是不是 root store,接下來獲取咱們定義的命名空間,若是有定義命名空間(namespaced: true),則把模塊掛載到以命名空間爲 key 的 _modulesNamespaceMap 對象上。
第二步判斷非根模塊非熱更新的狀況下,獲取父模塊的 state,獲取當前模塊的名稱,經過 Vue.set 將當前模塊的 state 掛載到父模塊上,key 是模塊名稱。因此這也是 store 裏的數據都是響應式的緣由。
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
// 後面會介紹(在安裝模式的嚴格模式中)
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}
複製代碼
經過 _withCommit 的代理,咱們在修改 state 的時候,在開發環境經過 this._committing 標誌就能拋出錯誤,避免意外更改。
第三步經過makeLocalContext
方法建立本地上下文環境,接收 store(root store)、namespace(模塊命名空間)、path(模塊路徑) 三個參數。
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) {
type = namespace + type
if (process.env.NODE_ENV !== 'production' && !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) {
type = namespace + type
if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
複製代碼
這裏沒有命名空間的狀況就是直接使用 store 上的 dispatch 和 commit,若是有則使用新定義的方法,這個方法接收三個參數:
咱們在 commit、dispatch 的時候會有兩種寫法傳參:
unifyObjectStyle 函數
function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) { // 若是是對象傳參
options = payload // 第二個參數是 { root: true } 這種了
payload = type // 第一個參數就是 payload,無論裏面的 type 屬性
type = type.type // 將 type 值賦值給 type
}
// 對 type 類型的一個斷言
if (process.env.NODE_ENV !== 'production') {
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}
// 返回一個完整對象
return { type, payload, options }
}
複製代碼
因此 unifyObjectStyle 函數就是幫助咱們把參數整合成 type、payload、options 三個變量裏。
接下來判斷只要不是派發到跟模塊或者當前模塊就是根模塊,那麼 type 就須要加上命名空間(例子中就變成了 'moduleA/setNumber'),而後 commit/dispatch 出去。
最後將 getters、state 經過 defineProperties 劫持到 local 對象上,值爲當前模塊的 getters、state。
makeLocalGetters、getNestedState 函數
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// 判斷 type 前的命名空間是否匹配當前模塊的命名
// 例子中 type 是 'moduleA/getNumberPlusOne', namespace 是 'moduleA/'
if (type.slice(0, splitPos) !== namespace) return
// 獲取本地 type,也就是 getNumberPlusOne
const localType = type.slice(splitPos)
// 這一步使得 localType 實際上就是訪問了 store.getters[type]
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
})
})
// 訪問代理對象
return gettersProxy
}
// 經過 reduce 一層層獲取到當前模塊的 state,而後返回這個 state
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
複製代碼
獲取進行了各類代理數據的本地上下文後,接下來會遍歷 mutations、actions、getters,分別進行註冊,而 state 的註冊早就在以前實例化 Module 的時候就完成了。
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
複製代碼
mutations 的註冊相對簡單,遍歷 module 下的每個 mutations 屬性的值,而後獲取帶有命名空間的 type,再調用 registerMutation 方法進行註冊,傳入4個參數:
registerMutation 函數:
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)
})
}
複製代碼
entry 是一個數組,爲何是數組呢,當咱們沒有使用命名空間時,恰巧在子模塊也有一個setCount
方法,那麼這個方法就會存到 setCount 爲屬性值的一個數組中,從而容許咱們一個 type 對應多個 mutaions。
entry push 一個執行 type 的回調函數的一個包裝函數,這也是 mutaions 裏的函數支持兩個參數 state、payload 的緣由。
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
複製代碼
回調主要作了三件事:
registerAction 方法:
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)
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
}
})
}
複製代碼
忽略幾個 if 的判斷,能夠看到 actions 裏經過 Promise 實現異步過程,這也是爲何 mutaions 裏不支持異步,而能夠經過 actions 來完成了。
actions 回調的第一個參數是一個對象,裏面包含 dispatch、commit、getters、state、rootGetters、rootState 字段,第二個參數通常是咱們所傳遞的參數,第三個參數 cb 經本人驗證徹底沒有用,在dispatch
方法中 handler 也只提供了 payload 一個參數,最後根據 res 的類型返回對應的值。
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
複製代碼
這裏沒什麼好說的,看看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 key 重複,接着在 _wrappedGetters 上以 type 爲 key,掛載 wrappedGetter 函數,返回 rawGetters 函數執行的結果,這個函數就是咱們定義在 store.js getters 裏 type 對應的回調。
因此咱們在 getters 裏定義函數接收的參數有 state、getters、rootState、rootGetters 4個,在Vuex 從使用到原理分析(上篇)的高級版食用方式中有說明應用。
當上面步驟都完成後,就開始遍歷模塊的子模塊,而後遞歸安裝。
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
複製代碼
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
const wrappedGetters = store._wrappedGetters
const 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
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
複製代碼
這裏主要是將 state 與 getters 創建好關係,實例化一個 Vue 掛載到 _vm 屬性上,經過 computed 屬性將 getters 與 state 關聯起來並緩存結果。咱們訪問this.$store.getters.getCountPlusOne
的時候,其實訪問的就是this.$store._vm.getCountPlusOne
,再繼續就是訪問到的 _vm 的 computed 裏定義的數據,
在執行computed.getCountPlusOne
對應的函數時,會執行store._wrappedGetters.getCountPlusOne
方法,這個方法又是咱們在分析註冊 Getters 時有提到的wrappedGetter
的方法:
function registerGetter (store, type, rawGetter, local) {
// ...
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 對象裏的方法,這裏就會訪問到store.state
,進而訪問到store._vm._data.$$state
,經過這樣一層一層,就創建了 state 與 getters 的依賴關係,當store.state
的發生變化時,下次訪問store.getters
就得到從新計算的結果,咱們用一張圖來更爲直觀的看清楚這個過程。
接下來的就相對簡單了,一個是嚴格模式,一個是銷燬舊的實例。下面看看嚴格模式:
if (store.strict) {
enableStrictMode(store)
}
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
複製代碼
在安裝模塊中有提到 [_commit](#5. 安裝模塊) 方法,裏面有個_committing
字段,就是在這裏使用到的。
嚴格模式中,_vm
會watch
$$state
的變化,當store.state
變化時,_committing
必須爲true
,不然在開發環境拋出警告。而_committing
的值只會在_commit
方法中提mutaion
時會被短暫置爲true
,因此Vuex
經過這種操做來規避咱們在其餘地方修改了store.state
,而沒有按照預期。
這一篇主要介紹了引入 Vuex,註冊 Store,獲取模塊,構建模塊樹,安裝模塊以及給模塊註冊 mutaions、actions、getters,最後經過 store._vm 給 state 與 getters 綁定,以及經過 computed 來緩存 getters 的結果。可是還有一個方法咱們沒有說明,例如 commit、dispatch 以及 4 個輔助函數,這些內容會在Vuex 從使用到原理分析(下篇)中進行分析。