Vuex 源碼深度解析
來自專欄前端之道8 人贊了文章
該文章內容節選自團隊的開源項目 InterviewMap。項目目前內容包含了 JS、網路、瀏覽器相關、小程序、性能優化、安全、框架、Git、數據結構、演算法等內容,無論是基礎還是進階,亦或是源碼解讀,你都能在本圖譜中得到滿意的答案,希望這個面試圖譜能夠幫助到大家更好的準備面試。
Vuex 思想
在解讀源碼之前,先來簡單了解下 Vuex 的思想。
Vuex 全局維護著一個對象,使用到了單例設計模式。在這個全局對象中,所有屬性都是響應式的,任意屬性進行了改變,都會造成使用到該屬性的組件進行更新。並且只能通過 commit
的方式改變狀態,實現了單向數據流模式。
Vuex 解析
Vuex 安裝
在看接下來的內容前,推薦本地 clone 一份 Vuex 源碼對照著看,便於理解。
在使用 Vuex 之前,我們都需要調用 Vue.use(Vuex)
。在調用 use
的過程中,Vue 會調用到 Vuex 的 install
函數
install
函數作用很簡單 - 確保 Vuex 只安裝一次 - 混入 beforeCreate
鉤子函數,可以在組件中使用 this.$store
export function install (_Vue) { // 確保 Vuex 只安裝一次 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)}// applyMixinexport default function (Vue) { // 獲得 Vue 版本號 const version = Number(Vue.version.split(.)[0]) // Vue 2.0 以上會混入 beforeCreate 函數 if (version >= 2) { Vue.mixin({ beforeCreate: vuexInit }) } else { // ... } // 作用很簡單,就是能讓我們在組件中 // 使用到 this.$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 } }}
Vuex 初始化
this._modules
本小節內容主要解析如何初始化 this._modules
export class Store { constructor (options = {}) { // 引入 Vue 的方式,自動安裝 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.`) } // 獲取 options 中的屬性 const { plugins = [], strict = false } = options // store 內部的狀態,重點關注 this._modules this._committing = false this._actions = Object.create(null) this._actionSubscribers = [] this._mutations = Object.create(null) this._wrappedGetters = Object.create(null) this._modules = new ModuleCollection(options) this._modulesNamespaceMap = Object.create(null) this._subscribers = [] this._watcherVM = new Vue() const store = this const { dispatch, commit } = this // bind 以下兩個函數上 this 上 // 便於 this.$store.dispatch 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) }}
接下來看 this._modules
的過程,以 以下代碼為例
const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... }}const moduleB = { state: { ... }, mutations: { ... }, actions: { ... }}const store = new Vuex.Store({ state: { ... }, modules: { a: moduleA, b: moduleB }})
對於以上代碼,store
可以看成 root
。在第一次執行時,會初始化一個 rootModule
,然後判斷 root
中是否存在 modules
屬性,然後遞歸註冊 module
。對於 child 來說,會獲取到他所屬的 parent
, 然後在 parent
中添加 module
。
export default class ModuleCollection { constructor (rawRootModule) { // register root module (Vuex.Store options) this.register([], rawRootModule, false) } register (path, rawModule, runtime = true) { // 開發環境斷言 if (process.env.NODE_ENV !== production) { assertRawModule(path, rawModule) } // 初始化 Module const newModule = new Module(rawModule, runtime) // 對於第一次初始化 ModuleCollection 時 // 會走第一個 if 條件,因為當前是 root if (path.length === 0) { this.root = newModule } else { // 獲取當前 Module 的 parent const parent = this.get(path.slice(0, -1)) // 添加 child,第一個參數是 // 當前 Module 的 key 值 parent.addChild(path[path.length - 1], newModule) } // 遞歸註冊 if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime) }) } }}export default class Module { constructor (rawModule, runtime) { this.runtime = runtime // 用於存儲 children this._children = Object.create(null) // 用於存儲原始的 rawModule this._rawModule = rawModule const rawState = rawModule.state // 用於存儲 state this.state = (typeof rawState === function ? rawState() : rawState) || {} }}
installModule
接下來看 installModule
的實現
// installModule(this, state, [], this._modules.root)function installModule (store, rootState, path, module, hot) { // 判斷是否為 rootModule const isRoot = !path.length // 獲取 namespace,root 沒有 namespace // 對於 modules: {a: moduleA} 來說 // namespace = a/ const namespace = store._modules.getNamespace(path) // 為 namespace 緩存 module if (module.namespaced) { store._modulesNamespaceMap[namespace] = module } // 設置 state if (!isRoot && !hot) { // 以下邏輯就是給 store.state 添加屬性 // 根據模塊添加 // state: { xxx: 1, a: {...}, b: {...} } const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) } // 該方法其實是在重寫 dispatch 和 commit 函數 // 你是否有疑問模塊中的 dispatch 和 commit // 是如何找到對應模塊中的函數的 // 假如模塊 A 中有一個名為 add 的 mutation // 通過 makeLocalContext 函數,會將 add 變成 // a/add,這樣就可以找到模塊 A 中對應函數了 const local = module.context = makeLocalContext(store, namespace, path) // 以下幾個函數遍歷,都是在 // 註冊模塊中的 mutation、action 和 getter // 假如模塊 A 中有名為 add 的 mutation 函數 // 在註冊過程中會變成 a/add 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) }) // 這裡會生成一個 _wrappedGetters 屬性 // 用於緩存 getter,便於下次使用 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) })}
resetStoreVM
接下來看 resetStoreVM
的實現,該屬性實現了狀態的響應式,並且將 _wrappedGetters
作為 computed
屬性。
// resetStoreVM(this, state)function resetStoreVM (store, state, hot) { const oldVm = store._vm // 設置 getters 屬性 store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} // 遍歷 _wrappedGetters 屬性 forEachValue(wrappedGetters, (fn, key) => { // 給 computed 對象添加屬性 computed[key] = () => fn(store) // 重寫 get 方法 // store.getters.xx 其實是訪問了 // store._vm[xx] // 也就是 computed 中的屬性 Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) // 使用 Vue 來保存 state 樹 // 同時也讓 state 變成響應式 const silent = Vue.config.silent Vue.config.silent = true // 當訪問 store.state 時 // 其實是訪問了 store._vm._data.$$state store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent // 確保只能通過 commit 的方式改變狀態 if (store.strict) { enableStrictMode(store) }}
常用 API
commit 解析
如果需要改變狀態的話,一般都會使用 commit
去操作,接下來讓我們來看看 commit
是如何實現狀態的改變的
commit(_type, _payload, _options) { // 檢查傳入的參數 const { type, payload, options } = unifyObjectStyle( _type, _payload, _options ) const mutation = { type, payload } // 找到對應的 mutation 函數 const entry = this._mutations[type] // 判斷是否找到 if (!entry) { if (process.env.NODE_ENV !== production) { console.error(`[vuex] unknown mutation type: ${type}`) } return } // _withCommit 函數將 _committing // 設置為 TRUE,保證在 strict 模式下 // 只能 commit 改變狀態 this._withCommit(() => { entry.forEach(function commitIterator(handler) { // entry.push(function wrappedMutationHandler(payload) { // handler.call(store, local.state, payload) // }) // handle 就是 wrappedMutationHandler 函數 // wrappedMutationHandler 內部就是調用 // 對於的 mutation 函數 handler(payload) }) }) // 執行訂閱函數 this._subscribers.forEach(sub => sub(mutation, this.state))}
dispatch 解析
如果需要非同步改變狀態,就需要通過 dispatch 的方式去實現。在 dispatch 調用的 commit
函數都是重寫過的,會找到模塊內的 mutation 函數。
dispatch(_type, _payload) { // 檢查傳入的參數 const { type, payload } = unifyObjectStyle(_type, _payload) const action = { type, payload } // 找到對於的 action 函數 const entry = this._actions[type] // 判斷是否找到 if (!entry) { if (process.env.NODE_ENV !== production) { console.error(`[vuex] unknown action type: ${type}`) } return } // 觸發訂閱函數 this._actionSubscribers.forEach(sub => sub(action, this.state)) // 在註冊 action 的時候,會將函數返回值 // 處理成 promise,當 promise 全部 // resolve 後,就會執行 Promise.all // 里的函數 return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload)}
各種語法糖
在組件中,如果想正常使用 Vuex 的功能,經常需要這樣調用 this.$store.state.xxx
的方式,引來了很多的不便。為此,Vuex 引入了語法糖的功能,讓我們可以通過簡單的方式來實現上述的功能。以下以 mapState
為例,其他的幾個 map 都是差不多的原理,就不一一解析了。
function normalizeNamespace(fn) { return (namespace, map) => { // 函數作用很簡單 // 根據參數生成 namespace if (typeof namespace !== string) { map = namespace namespace = } else if (namespace.charAt(namespace.length - 1) !== /) { namespace += / } return fn(namespace, map) }}// 執行 mapState 就是執行// normalizeNamespace 返回的函數export const mapState = normalizeNamespace((namespace, states) => { const res = {} // 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 } ] // function normalizeMap(map) { // return Array.isArray(map) // ? map.map(key => ({ key, val: key })) // : Object.keys(map).map(key => ({ key, val: map[key] })) // } // states 參數可以參入數組或者對象類型 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 } // 返回 State return typeof val === function ? val.call(this, state, getters) : state[val] } // mark vuex getter for devtools res[key].vuex = true }) return res})
最後
以上是 Vue 的源碼解析,雖然 Vuex 的整體代碼並不多,但是卻是個值得閱讀的項目。如果你在閱讀的過程中有什麼疑問或者發現了我的錯誤,歡迎在評論中討論。
推薦閱讀: