前言 基於 vuex 3.1.2 按如下流程進行分析: Vue.use(Vuex) Vue.use() 會執行插件的 install 方法,並把插件放入緩存數組中。 而 Vuex 的 install 方法很簡單,保證只執行一次,以及使用 applyMixin 初始化。 applyMixin 方法定義 ...
前言
基於 vuex 3.1.2 按如下流程進行分析:
Vue.use(Vuex);
const store = new Vuex.Store({
actions,
getters,
state,
mutations,
modules
// ...
});
new Vue({store});
Vue.use(Vuex)
Vue.use() 會執行插件的 install 方法,並把插件放入緩存數組中。
而 Vuex 的 install 方法很簡單,保證只執行一次,以及使用 applyMixin 初始化。
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)
}
applyMixin 方法定義在 vuex/src/mixin.js
,vuex 還相容了 vue 1 的版本,這裡只關註 vue 2 的處理。
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 混入一個 beforeCreate 方法
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 這裡是 vue 1 的相容處理
}
// 這裡的邏輯就是將 options.store 保存在 vue 組件的 this.$store 中,
// options.store 就是 new Vue({...}) 時傳入的 store,
// 也就是 new Vuex.Store({...}) 生成的實例。
function vuexInit () {
const options = this.$options
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
new Vuex.Store({...})
先看 Store 的構造函數:
constructor (options = {}) {
const { plugins = [], strict = false } = options
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
// 構建 modules
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
// store.watch
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
// bind commit and dispatch to self
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)
}
// strict mode
this.strict = strict
// 安裝模塊
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
// 實現狀態的響應式
resetStoreVM(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
this._modules
vuex 為了讓結構清晰,允許我們將 store
分割成模塊,每個模塊擁有自己的 state
、mutation
、action
、getter
,而且模塊自身也可以擁有子模塊。
const moduleA = {...}
const moduleB = {...}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態
而模塊在 vuex 的構造函數中通過new ModuleCollection(options)
生成,依然只看構造函數:
// vuex/src/module/module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
register (path, rawModule, runtime = true) {
// 生成一個 module
const newModule = new Module(rawModule, runtime)
// new Vuex.Store() 生成的是根模塊
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)
})
}
}
}
register
接收 3 個參數,其中 path
表示路徑,即模塊樹的路徑,rawModule
表示傳入的模塊的配置,runtime
表示是否是一個運行時創建的模塊。
register
首先 new Module() 生成一個模塊。
// vuex/src/module/module.js
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// 子模塊
this._children = Object.create(null)
// module 原始配置
this._rawModule = rawModule
const rawState = rawModule.state
// state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
實例化一個 module 後,判斷當前的 path 的長度,如果為 0,就是是一個根模塊,所以把 newModule 賦值給 this.root,而 new Vuex.Store() 生成的就是根模塊。
如果不為 0,就建立父子模塊的關係:
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
首先根據路徑獲取父模塊,然後再調用父模塊的 addChild 方法將子模塊加入到 this._children 中,以此建立父子關係。
register 最後會檢測是否有嵌套的模塊,然後進行註冊嵌套的模塊:
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
遍歷當前模塊定義中的所有 modules,根據 key 作為 path,遞歸調用 register 方法進行註冊。
installModule
註冊完模塊就會開始安裝模塊:
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
installModule 方法支持 5 個參數: store,state,path(模塊路徑),module(根模塊),hot(是否熱更新)。
預設情況下,模塊內部的 action、mutation 和 getter 是註冊在全局命名空間的,這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。但是如果有同名的 mutation 被提交,會觸發所有同名的 mutation。
因此 vuex 提供了 namespaced: true
讓模塊成為帶命名空間的模塊,當模塊被註冊後,它的所有 action、mutation 和 getter 都會自動根據模塊註冊的路徑調整命名。例如:
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
// 進一步嵌套命名空間
posts: {
namespaced: true,
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
})
回到 installModule 方法本身:
function installModule (store, rootState, path, module, hot) {
// 是否為根模塊
const isRoot = !path.length
// 獲取命名空間,
// 比如說 { a: moduleA } => 'a/',
// root 沒有 namespace
const namespace = store._modules.getNamespace(path)
// 將有 namespace 的 module 緩存起來
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
if (!isRoot && !hot) {
// 給 store.state 添加屬性,
// 假如有 modules: { a: moduleA },
// 則 state: { a: moduleA.state }
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
// local 上下文,
// 本質上是重寫 dispatch 和 commit 函數,
// 舉個例子,modules: { a: moduleA },
// moduleA 中有名為 increment 的 mutation,
// 通過 makeLocalContext 函數,會將 increment 變成
// a/increment,這樣就可以在 moduleA 中找到定義的函數
const local = module.context = makeLocalContext(store, namespace, path)
// 下麵這幾個遍歷迴圈函數,
// 都是在註冊模塊中的 mutation、action 等等,
// moduleA 中有名為 increment 的 mutation,
// 那麼會將 namespace + key 拼接起來,
// 變為 'a/increment'
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
// 會用 this._mutations 將每個 mutation 存儲起來,
// 因為允許同一 namespacedType 對應多個方法,
// 所以同一 namespacedType 是用數組存儲的,
// store._mutations[type] = []
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
// 和上面一樣,用 this._actions 將 action 存儲起來
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
// 會用 this._wrappedGetters 存儲起來
// getters 有一點不一樣,
// 這裡保存的是一個返回 getters 的函數,
// 而且同一 namespacedType 只能定義一個。
registerGetter(store, namespacedType, getter, local)
})
// 遞歸安裝子模塊
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
makeLocalContext
function makeLocalContext (store, namespace, path) {
// 判斷是否有 namespace
const noNamespace = namespace === ''
const local = {
// 重寫 dispatch
// 為什麼要重寫,
// 舉個例子 modules: { a: moduleA }
// 在 moduleA 的 action 中使用 dispatch 時,
// 並不會傳入完整的 path,
// 只有在 vue 實例里調用 store.dispatch 才會傳入完整路徑
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
}
return store.dispatch(type, payload)
},
// 重寫 commit 方法
// 同上
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
}
store.commit(type, payload, options)
}
}
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
// local.getters 本質上是通過匹配 namespace,
// 從 store.getters[type] 中獲取
: () => makeLocalGetters(store, namespace)
},
state: {
// local.state 本質上是通過解析 path,
// 從 store.state 中獲取
get: () => getNestedState(store.state, path)
}
})
return local
}
resetStoreVM(this, state)
初始化 store._vm,利用 Vue 將 store.state 進行響應式處理,並且將 getters 當作 Vue 的計算屬性來進行處理:
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍歷 store._wrappedGetters 屬性
forEachValue(wrappedGetters, (fn, key) => {
// 這裡的 partial 其實就是創建一個閉包環境,
// 保存 fn, store 兩個變數,並賦值給 computed
computed[key] = partial(fn, store)
// 重寫 get 方法,
// store.getters.key 其實是訪問了 store._vm[key],
// 也就是去訪問 computed 中的屬性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// 實例化一個 Vue 實例 store._vm,
// 用它來保存 state,computed(getters),
// 也就是利用 Vue 的進行響應式處理
const silent = Vue.config.silent
Vue.config.silent = true
// 訪問 store.state 時,
// 其實是訪問了 store._vm._data.$$state
store._vm = new Vue({
data: {
$$state: state
},
// 這裡其實就是上面的 getters
computed
})
Vue.config.silent = silent
// 開啟 strict mode,
// 只能通過 commit 的方式改變 state
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// 熱重載
store._withCommit(() => {
oldVm._data.$$state = null
})
}
// 這裡其實是動態註冊模塊,
// 將新的模塊內容加入後生成了新的 store._vm,
// 然後將舊的銷毀掉
Vue.nextTick(() => oldVm.$destroy())
}
}
partial
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
常用 api
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]
if (!entry) {
// 沒有 mutation 會報錯並退出
return
}
this._withCommit(() => {
// 允許同一 type 下,有多個方法,
// 所以迴圈數組執行 mutation,
// 實際上執行的就是安裝模塊時註冊的 mutation
// handler.call(store, local.state, payload)
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 觸發訂閱了 mutation 的所有函數
this._subscribers
.slice()
.forEach(sub => sub(mutation, this.state))
}
dispatch
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
// 從 Store._actions 獲取
const entry = this._actions[type]
if (!entry) {
// 找不到會報錯
return
}
// 在 action 執行之前,
// 觸發監聽了 action 的函數
try {
this._actionSubscribers
.slice()
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
console.error(e)
}
// action 用 Promise.all 非同步執行,
// 實際上執行的就是安裝模塊時註冊的 action
/*
handler.call(store, {dispatch, commit, getters, state, rootGetters, rootState}, payload, cb)
*/
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return result.then(res => {
// 在 action 執行之後,
// 觸發監聽了 action 的函數
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
console.error(e)
}
return res
})
}
watch
watch (getter, cb, options) {
// new Vuex.Store() 時創建 _watcherVM,
// this._watcherVM = new Vue(),
// 本質就是調用 vue 的 api
return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}
registerModule
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
// 註冊模塊
this._modules.register(path, rawModule)
// 安裝模塊
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// 重新生成 vue 實例掛載到 store 上,
// 然後銷毀舊的
resetStoreVM(this, this.state)
}
## 備註
希望疫情儘快過去吧。——2020/02/08 元宵