該文章內容節選自團隊的開源項目 InterviewMap。項目目前內容包含了 JS、網絡、瀏覽器相關、小程序、性能優化、安全、框架、Git、數據結構、算法等內容,不管是基礎仍是進階,亦或是源碼解讀,你都能在本圖譜中獲得滿意的答案,但願這個面試圖譜可以幫助到你們更好的準備面試。前端
在解讀源碼以前,先來簡單瞭解下 Vuex 的思想。vue
Vuex 全局維護着一個對象,使用到了單例設計模式。在這個全局對象中,全部屬性都是響應式的,任意屬性進行了改變,都會形成使用到該屬性的組件進行更新。而且只能經過 commit
的方式改變狀態,實現了單向數據流模式。面試
在看接下來的內容前,推薦本地 clone 一份 Vuex 源碼對照着看,便於理解。算法
在使用 Vuex 以前,咱們都須要調用 Vue.use(Vuex)
。在調用 use
的過程當中,Vue 會調用到 Vuex 的 install
函數vuex
install
函數做用很簡單小程序
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)
}
// applyMixin
export 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
}
}
}
複製代碼
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
。promise
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)
}
}
複製代碼
若是須要改變狀態的話,通常都會使用 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 調用的 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 的總體代碼並很少,可是倒是個值得閱讀的項目。若是你在閱讀的過程當中有什麼疑問或者發現了個人錯誤,歡迎在評論中討論。
若是你想學習到更多的前端知識、面試技巧或者一些我我的的感悟,能夠關注個人公衆號一塊兒學習