基于 vuex 3.1.2 按如下流程進行分析:
Vue.use(Vuex);const store = new Vuex.Store({ actions, getters, state, mutations, modules // ...}); new Vue({store});
Vue.use() 會執(zhí)行插件的 install 方法,并把插件放入緩存數(shù)組中。
而 Vuex 的 install 方法很簡單,保證只執(zhí)行一次,以及使用 applyMixin 初始化。
export function install (_Vue) { // 保證只執(zhí)行一次 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 } }}
先看 Store 的構造函數(shù):
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) // 實現(xiàn)狀態(tài)的響應式 resetStoreVM(this, state) // apply plugins plugins.forEach(plugin => plugin(this)) const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools if (useDevtools) { devtoolPlugin(this) }}
vuex 為了讓結構清晰,允許我們將 store
分割成模塊,每個模塊擁有自己的 state
、mutation
、action
、getter
,而且模塊自身也可以擁有子模塊。
const moduleA = {...}const moduleB = {...}const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB }})store.state.a // -> moduleA 的狀態(tài)store.state.b // -> moduleB 的狀態(tài)
而模塊在 vuex 的構造函數(shù)中通過new ModuleCollection(options)
生成,依然只看構造函數(shù):
// vuex/src/module/module-collection.jsexport 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 個參數(shù),其中 path
表示路徑,即模塊樹的路徑,rawModule
表示傳入的模塊的配置,runtime
表示是否是一個運行時創(chuàng)建的模塊。
register
首先 new Module() 生成一個模塊。
// vuex/src/module/module.jsexport 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)
首先根據(jù)路徑獲取父模塊,然后再調(diào)用父模塊的 addChild 方法將子模塊加入到 this._children 中,以此建立父子關系。
register 最后會檢測是否有嵌套的模塊,然后進行注冊嵌套的模塊:
if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime) })}
遍歷當前模塊定義中的所有 modules,根據(jù) key 作為 path,遞歸調(diào)用 register 方法進行注冊。
注冊完模塊就會開始安裝模塊:
const state = this._modules.root.stateinstallModule(this, state, [], this._modules.root)
installModule 方法支持 5 個參數(shù): store,state,path(模塊路徑),module(根模塊),hot(是否熱更新)。
默認情況下,模塊內(nèi)部的 action、mutation 和 getter 是注冊在全局命名空間的,這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。但是如果有同名的 mutation 被提交,會觸發(fā)所有同名的 mutation。
因此 vuex 提供了 namespaced: true
讓模塊成為帶命名空間的模塊,當模塊被注冊后,它的所有 action、mutation 和 getter 都會自動根據(jù)模塊注冊的路徑調(diào)整命名。例如:
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 函數(shù), // 舉個例子,modules: { a: moduleA }, // moduleA 中有名為 increment 的 mutation, // 通過 makeLocalContext 函數(shù),會將 increment 變成 // a/increment,這樣就可以在 moduleA 中找到定義的函數(shù) const local = module.context = makeLocalContext(store, namespace, path) // 下面這幾個遍歷循環(huán)函數(shù), // 都是在注冊模塊中的 mutation、action 等等, // moduleA 中有名為 increment 的 mutation, // 那么會將 namespace + key 拼接起來, // 變?yōu)?'a/increment' module.forEachMutation((mutation, key) => { const namespacedType = namespace + key // 會用 this._mutations 將每個 mutation 存儲起來, // 因為允許同一 namespacedType 對應多個方法, // 所以同一 namespacedType 是用數(shù)組存儲的, // 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 的函數(shù), // 而且同一 namespacedType 只能定義一個。 registerGetter(store, namespacedType, getter, local) }) // 遞歸安裝子模塊 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })}
function makeLocalContext (store, namespace, path) { // 判斷是否有 namespace const noNamespace = namespace === '' const local = { // 重寫 dispatch // 為什么要重寫, // 舉個例子 modules: { a: moduleA } // 在 moduleA 的 action 中使用 dispatch 時, // 并不會傳入完整的 path, // 只有在 vue 實例里調(diào)用 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}
初始化 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 其實就是創(chuàng)建一個閉包環(huán)境, // 保存 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 }) } // 這里其實是動態(tài)注冊模塊, // 將新的模塊內(nèi)容加入后生成了新的 store._vm, // 然后將舊的銷毀掉 Vue.nextTick(() => oldVm.$destroy()) }}
partial
export function partial (fn, arg) { return function () { return fn(arg) }}
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 下,有多個方法, // 所以循環(huán)數(shù)組執(zhí)行 mutation, // 實際上執(zhí)行的就是安裝模塊時注冊的 mutation // handler.call(store, local.state, payload) entry.forEach(function commitIterator (handler) { handler(payload) }) }) // 觸發(fā)訂閱了 mutation 的所有函數(shù) this._subscribers .slice() .forEach(sub => sub(mutation, this.state))}
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 執(zhí)行之前, // 觸發(fā)監(jiān)聽了 action 的函數(shù) try { this._actionSubscribers .slice() .filter(sub => sub.before) .forEach(sub => sub.before(action, this.state)) } catch (e) { console.error(e) } // action 用 Promise.all 異步執(zhí)行, // 實際上執(zhí)行的就是安裝模塊時注冊的 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 執(zhí)行之后, // 觸發(fā)監(jiān)聽了 action 的函數(shù) try { this._actionSubscribers .filter(sub => sub.after) .forEach(sub => sub.after(action, this.state)) } catch (e) { console.error(e) } return res }) }
watch (getter, cb, options) { // new Vuex.Store() 時創(chuàng)建 _watcherVM, // this._watcherVM = new Vue(), // 本質就是調(diào)用 vue 的 api return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)}
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)}
希望疫情盡快過去吧?!?020/02/08 元宵