Vuex原始碼閱讀分析
Vuex是專為Vue開發的統一狀態管理工具。當我們的專案不是很複雜時,一些互動可以通過全域性事件匯流排解決,但是這種觀察者模式有些弊端,開發時可能沒什麼感覺,但是當專案變得複雜,維護時往往會摸不著頭腦,如果是後來加入的夥伴更會覺得很無奈。這時候可以採用Vuex方案,它可以使得我們的專案的資料流變得更加清晰。本文將會分析Vuex的整個實現思路,當是自己讀完原始碼的一個總結。
目錄結構
-
module:提供對module的處理,主要是對state的處理,最後構建成一棵module tree
-
plugins:和devtools配合的外掛,提供像時空旅行這樣的除錯功能。
-
helpers:提供如mapActions、mapMutations這樣的api
-
index、index.esm:原始碼的主入口,丟擲Store和mapActions等api,一個用於commonjs的打包、一個用於es module的打包
-
mixin:提供install方法,用於注入$store
-
store:vuex的核心程式碼
-
util:一些工具函式,如deepClone、isPromise、assert
原始碼分析
先從一個簡單的示例入手,一步一步分析整個程式碼的執行過程,下面是官方提供的簡單示例
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
複製程式碼
1. Vuex的註冊
Vue官方建議的外掛使用方法是使用Vue.use方法,這個方法會呼叫外掛的install方法,看看install方法都做了些什麼,從index.js中可以看到install方法在store.js中丟擲,部分程式碼如下
let Vue // bind on install
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)
}
複製程式碼
宣告瞭一個Vue變數,這個變數在install方法中會被賦值,這樣可以給當前作用域提供Vue,這樣做的好處是不需要額外import Vue from 'vue'
不過我們也可以這樣寫,然後讓打包工具不要將其打包,而是指向開發者所提供的Vue,比如webpack的externals
,這裡就不展開了。執行install會先判斷Vue是否已經被賦值,避免二次安裝。然後呼叫applyMixin
方法,程式碼如下
// applyMixin
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
// 當我們在執行new Vue的時候,需要提供store欄位
if (options.store) {
// 如果是root,將store綁到this.$store
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 否則拿parent上的$store
// 從而實現所有元件共用一個store例項
this.$store = options.parent.$store
}
}
}
複製程式碼
這裡會區分vue的版本,2.x和1.x的鉤子是不一樣的,如果是2.x使用beforeCreate
,1.x即使用_init
。當我們在執行new Vue
啟動一個Vue應用程式時,需要給上store
欄位,根元件從這裡拿到store
,子元件從父元件拿到,這樣一層一層傳遞下去,實現所有元件都有$store
屬性,這樣我們就可以在任何元件中通過this.$store
訪問到store
接下去繼續看例子
// store.js
export default new Vuex.Store({
state: {
count: 0
},
getters: {
evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
},
actions: {
increment: ({ commit }) => commit('increment'),
decrement: ({ commit }) => commit('decrement')
},
mutations: {
increment (state) {
state.count++
},
decrement (state) {
state.count--
}
}
})
複製程式碼
// app.js
new Vue({
el: '#app',
store, // 傳入store,在beforeCreate鉤子中會用到
render: h => h(Counter)
})
複製程式碼
2.store初始化
這裡是呼叫Store建構函式,傳入一個物件,包括state、actions等等,接下去看看Store建構函式都做了些什麼
export class Store {
constructor (options = {}) {
if (!Vue && typeof window !== 'undefined' && window.Vue) {
// 掛載在window上的自動安裝,也就是通過script標籤引入時不需要手動呼叫Vue.use(Vuex)
install(window.Vue)
}
if (process.env.NODE_ENV !== 'production') {
// 斷言必須使用Vue.use(Vuex),在install方法中會給Vue賦值
// 斷言必須存在Promise
// 斷言必須使用new操作符
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.`)
}
const {
plugins = [],
strict = false
} = options
// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
// 這裡進行module收集,只處理了state
this._modules = new ModuleCollection(options)
// 用於儲存namespaced的模組
this._modulesNamespaceMap = Object.create(null)
// 用於監聽mutation
this._subscribers = []
// 用於響應式地監測一個 getter 方法的返回值
this._watcherVM = new Vue()
// 將dispatch和commit方法的this指標繫結到store上,防止被修改
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
// 這裡是module處理的核心,包括處理根module、action、mutation、getters和遞迴註冊子module
installModule(this, state, [], this._modules.root)
// 使用vue例項來儲存state和getter
resetStoreVM(this, state)
// 外掛註冊
plugins.forEach(plugin => plugin(this))
if (Vue.config.devtools) {
devtoolPlugin(this)
}
}
}
複製程式碼
首先會判斷Vue是不是掛載在window
上,如果是的話,自動呼叫install
方法,然後進行斷言,必須先呼叫Vue.use(Vuex)。必須提供Promise
,這裡應該是為了讓Vuex的體積更小,讓開發者自行提供Promise
的polyfill
,一般我們可以使用babel-runtime
或者babel-polyfill
引入。最後斷言必須使用new操作符呼叫Store函式。
接下去是一些內部變數的初始化
_committing
提交狀態的標誌,在_withCommit中,當使用mutation
時,會先賦值為true
,再執行mutation
,修改state
後再賦值為false
,在這個過程中,會用watch
監聽state的變化時是否_committing
為true,從而保證只能通過mutation
來修改state
_actions
用於儲存所有action,裡面會先包裝一次
_actionSubscribers
用於儲存訂閱action的回撥
_mutations
用於儲存所有的mutation,裡面會先包裝一次
_wrappedGetters
用於儲存包裝後的getter
_modules
用於儲存一棵module樹
_modulesNamespaceMap
用於儲存namespaced的模組
接下去的重點是
this._modules = new ModuleCollection(options)
複製程式碼
2.1 模組收集
接下去看看ModuleCollection函式都做了什麼,部分程式碼如下
// module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
// 註冊 root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
get (path) {
// 根據path獲取module
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
/*
* 遞迴註冊module path是路徑 如
* {
* modules: {
* a: {
* state: {}
* }
* }
* }
* a模組的path => ['a']
* 根模組的path => []
*/
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
// 斷言 rawModule中的getters、actions、mutations必須為指定的型別
assertRawModule(path, rawModule)
}
// 例項化一個module
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// 根module 繫結到root屬性上
this.root = newModule
} else {
// 子module 新增其父module的_children屬性上
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// 如果當前模組存在子模組(modules欄位)
// 遍歷子模組,逐個註冊,最終形成一個樹
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
// module.js
export default class Module {
constructor (rawModule, runtime) {
// 初始化時runtime為false
this.runtime = runtime
// Store some children item
// 用於儲存子模組
this._children = Object.create(null)
// Store the origin module object which passed by programmer
// 儲存原來的moudle,在Store的installModule中會處理actions、mutations等
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
// 儲存state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
addChild (key, module) {
// 將子模組新增到_children中
this._children[key] = module
}
}
複製程式碼
這裡呼叫ModuleCollection
建構函式,通過path
的長度判斷是否為根module,首先進行根module的註冊,然後遞迴遍歷所有的module,子module 新增其父module的_children屬性上,最終形成一棵樹
接著,還是一些變數的初始化,然後
2.2 繫結commit和dispatch的this指標
// 繫結commit和dispatch的this指標
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)
}
複製程式碼
這裡會將dispath和commit方法的this指標繫結為store,比如下面這樣的騷操作,也不會影響到程式的執行
this.$store.dispatch.call(this, 'someAction', payload)
複製程式碼
2.3 模組安裝
接著是store的核心程式碼
// 這裡是module處理的核心,包括處理根module、名稱空間、action、mutation、getters和遞迴註冊子module
installModule(this, state, [], this._modules.root)
複製程式碼
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
/*
* {
* // ...
* modules: {
* moduleA: {
* namespaced: true
* },
* moduleB: {}
* }
* }
* moduleA的namespace -> 'moduleA/'
* moduleB的namespace -> ''
*/
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
// 儲存namespaced模組
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
// 非根元件設定state
// 根據path獲取父state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 當前的module
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 使用Vue.set將state設定為響應式
Vue.set(parentState, moduleName, module.state)
})
console.log('end', store)
}
// 設定module的上下文,從而保證mutation和action的第一個引數能拿到對應的state getter等
const local = module.context = makeLocalContext(store, namespace, path)
// 逐一註冊mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 逐一註冊action
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 逐一註冊getter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 逐一註冊子module
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
複製程式碼
首先儲存namespaced
模組到store._modulesNamespaceMap
,再判斷是否為根元件且不是hot,得到父級module的state和當前module的name,呼叫Vue.set(parentState, moduleName, module.state)
將當前module的state掛載到父state上。接下去會設定module的上下文,因為可能存在namespaced
,需要額外處理
// 設定module的上下文,繫結對應的dispatch、commit、getters、state
function makeLocalContext (store, namespace, path) {
// namespace 如'moduleA/'
const noNamespace = namespace === ''
const local = {
// 如果沒有namespace,直接使用原來的
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
// 統一格式 因為支援payload風格和物件風格
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
// 如果root: true 不會加上namespace 即在名稱空間模組裡提交根的 action
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
}
}
// 觸發action
return store.dispatch(type, payload)
},
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
// 統一格式 因為支援payload風格和物件風格
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
// 如果root: true 不會加上namespace 即在名稱空間模組裡提交根的 mutation
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
}
}
// 觸發mutation
store.commit(type, payload, options)
}
}
// getters and state object must be gotten lazily
// because they will be changed by vm update
// 這裡的getters和state需要延遲處理,需要等資料更新後才進行計算,所以使用getter函式,當訪問的時候再進行一次計算
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace) // 獲取namespace下的getters
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// 如果getter不在該名稱空間下 直接return
if (type.slice(0, splitPos) !== namespace) return
// 去掉type上的名稱空間
const localType = type.slice(splitPos)
// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
// 給getters加一層代理 這樣在module中獲取到的getters不會帶名稱空間,實際返回的是store.getters[type] type是有名稱空間的
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
})
})
return gettersProxy
}
複製程式碼
這裡會判斷module
的namespace
是否存在,不存在不會對dispatch
和commit
做處理,如果存在,給type
加上namespace
,如果宣告瞭{root: true}
也不做處理,另外getters
和state
需要延遲處理,需要等資料更新後才進行計算,所以使用Object.defineProperties
的getter函式,當訪問的時候再進行計算
再回到上面的流程,接下去是逐步註冊mutation
action
getter
子module
,先看註冊mutation
/*
* 引數是store、mutation的key(namespace處理後的)、handler函式、當前module上下文
*/
function registerMutation (store, type, handler, local) {
// 首先判斷store._mutations是否存在,否則給空陣列
const entry = store._mutations[type] || (store._mutations[type] = [])
// 將mutation包一層函式,push到陣列中
entry.push(function wrappedMutationHandler (payload) {
// 包一層,commit執行時只需要傳入payload
// 執行時讓this指向store,引數為當前module上下文的state和使用者額外新增的payload
handler.call(store, local.state, payload)
})
}
複製程式碼
mutation
的註冊比較簡單,主要是包一層函式,然後儲存到store._mutations
裡面,在這裡也可以知道,mutation
可以重複註冊,不會覆蓋,當使用者呼叫this.$store.commit(mutationType, payload)
時會觸發,接下去看看commit
函式
// 這裡的this已經被繫結為store
commit (_type, _payload, _options) {
// 統一格式,因為支援物件風格和payload風格
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 獲取當前type對應儲存下來的mutations陣列
const entry = this._mutations[type]
if (!entry) {
// 提示不存在該mutation
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 包裹在_withCommit中執行mutation,mutation是修改state的唯一方法
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
// 執行mutation,只需要傳入payload,在上面的包裹函式中已經處理了其他引數
handler(payload)
})
})
// 執行mutation的訂閱者
this._subscribers.forEach(sub => sub(mutation, this.state))
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
// 提示silent引數已經移除
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
複製程式碼
首先對引數進行統一處理,因為是支援物件風格和載荷風格的,然後拿到當前type
對應的mutation陣列,使用_withCommit
包裹逐一執行,這樣我們執行this.$store.commit
的時候會呼叫對應的mutation
,而且第一個引數是state
,然後再執行mutation
的訂閱函式
接下去看action
的註冊
/*
* 引數是store、type(namespace處理後的)、handler函式、module上下文
*/
function registerAction (store, type, handler, local) {
// 獲取_actions陣列,不存在即賦值為空陣列
const entry = store._actions[type] || (store._actions[type] = [])
// push到陣列中
entry.push(function wrappedActionHandler (payload, cb) {
// 包一層,執行時需要傳入payload和cb
// 執行action
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
)
// 如果action的執行結果不是promise,將他包裹為promise,這樣就支援promise的鏈式呼叫
if (!isPromise(res)) {
res = Promise.resolve(res)
}
if (store._devtoolHook) {
// 使用devtool處理一次error
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
複製程式碼
和mutation
很類似,使用函式包一層然後push到store._actions
中,有些不同的是執行時引數比較多,這也是為什麼我們在寫action
時可以解構拿到commit
等的原因,然後再將返回值promisify
,這樣可以支援鏈式呼叫,但實際上用的時候最好還是自己返回promise
,因為通常action
是非同步的,比較多見是發起ajax請求,進行鏈式呼叫也是想當非同步完成後再執行,具體根據業務需求來。接下去再看看dispatch
函式的實現
// this已經繫結為store
dispatch (_type, _payload) {
// 統一格式
const { type, payload } = unifyObjectStyle(_type, _payload)
const action = { type, payload }
// 獲取actions陣列
const entry = this._actions[type]
// 提示不存在action
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
// 執行action的訂閱者
this._actionSubscribers.forEach(sub => sub(action, this.state))
// 如果action大於1,需要用Promise.all包裹
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}
複製程式碼
這裡和commit
也是很類似的,對引數統一處理,拿到action陣列,如果長度大於一,用Promise.all
包裹,不過直接執行,然後返回執行結果。
接下去是getters
的註冊和子module
的註冊
/*
* 引數是store、type(namesapce處理後的)、getter函式、module上下文
*/
function registerGetter (store, type, rawGetter, local) {
// 不允許重複定義getters
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
// 包一層,儲存到_wrappedGetters中
store._wrappedGetters[type] = function wrappedGetter (store) {
// 執行時傳入store,執行對應的getter函式
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
複製程式碼
首先對getters
進行判斷,和mutation
是不同的,這裡是不允許重複定義的,然後包裹一層函式,這樣在呼叫時只需要給上store
引數,而使用者的函式裡會包含local.state
local.getters
store.state
store.getters
// 遞迴註冊子module
installModule(store, rootState, path.concat(key), child, hot)
複製程式碼
使用vue例項儲存state和getter
接著再繼續執行resetStoreVM(this, state)
,將state
和getters
存放到一個vue例項
中,
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
複製程式碼
function resetStoreVM (store, state, hot) {
// 儲存舊vm
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
// 迴圈所有getters,通過Object.defineProperty方法為getters物件建立屬性,這樣就可以通過this.$store.getters.xxx訪問
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// getter儲存在computed中,執行時只需要給上store引數,這個在registerGetter時已經做處理
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// 使用一個vue例項來儲存state和getter
// silent設定為true,取消所有日誌警告等
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// 恢復使用者的silent設定
Vue.config.silent = silent
// enable strict mode for new vm
// strict模式
if (store.strict) {
enableStrictMode(store)
}
// 若存在oldVm,解除對state的引用,等dom更新後把舊的vue例項銷燬
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
複製程式碼
這裡會重新設定一個新的vue例項,用來儲存state
和getter
,getters
儲存在計算屬性中,會給getters
加一層代理,這樣可以通過this.$store.getters.xxx
訪問到,而且在執行getters時只傳入了store
引數,這個在上面的registerGetter
已經做了處理,也是為什麼我們的getters
可以拿到state
getters
rootState
rootGetters
的原因。然後根據使用者設定開啟strict
模式,如果存在oldVm,解除對state的引用,等dom更新後把舊的vue例項銷燬
function enableStrictMode (store) {
store._vm.$watch(
function () {
return this._data.$$state
},
() => {
if (process.env.NODE_ENV !== 'production') {
// 不允許在mutation之外修改state
assert(
store._committing,
`Do not mutate vuex store state outside mutation handlers.`
)
}
},
{ deep: true, sync: true }
)
}
複製程式碼
使用$watch
來觀察state
的變化,如果此時的store._committing
不會true,便是在mutation
之外修改state,報錯。
再次回到建構函式,接下來是各類外掛的註冊
2.4 外掛註冊
// apply plugins
plugins.forEach(plugin => plugin(this))
if (Vue.config.devtools) {
devtoolPlugin(this)
}
複製程式碼
到這裡store
的初始化工作已經完成。大概長這個樣子
看到這裡,相信已經對store
的一些實現細節有所瞭解,另外store
上還存在一些api,但是用到的比較少,可以簡單看看都有些啥
2.5 其他api
- watch (getter, cb, options)
用於監聽一個getter
值的變化
watch (getter, cb, options) {
if (process.env.NODE_ENV !== 'production') {
assert(
typeof getter === 'function',
`store.watch only accepts a function.`
)
}
return this._watcherVM.$watch(
() => getter(this.state, this.getters),
cb,
options
)
}
複製程式碼
首先判斷getter
必須是函式型別,使用$watch
方法來監控getter
的變化,傳入state
和getters
作為引數,當值變化時會執行cb回撥。呼叫此方法返回的函式可停止偵聽。
- replaceState(state)
用於修改state,主要用於devtool外掛的時空穿梭功能,程式碼也相當簡單,直接修改_vm.$$state
replaceState (state) {
this._withCommit(() => {
this._vm._data.$$state = state
})
}
複製程式碼
- registerModule (path, rawModule, options = {})
用於動態註冊module
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
if (process.env.NODE_ENV !== 'production') {
assert(Array.isArray(path), `module path must be a string or an Array.`)
assert(
path.length > 0,
'cannot register the root module by using registerModule.'
)
}
this._modules.register(path, rawModule)
installModule(
this,
this.state,
path,
this._modules.get(path),
options.preserveState
)
// reset store to update getters...
resetStoreVM(this, this.state)
}
複製程式碼
首先統一path
的格式為Array,接著是斷言,path只接受String
和Array
型別,且不能註冊根module,然後呼叫store._modules.register
方法收集module,也就是上面的module-collection
裡面的方法。再呼叫installModule
進行模組的安裝,最後呼叫resetStoreVM
更新_vm
- unregisterModule (path)
根據path
登出module
unregisterModule (path) {
if (typeof path === 'string') path = [path]
if (process.env.NODE_ENV !== 'production') {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}
this._modules.unregister(path)
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
}
複製程式碼
和registerModule
一樣,首先統一path
的格式為Array,接著是斷言,path只接受String
和Array
型別,接著呼叫store._modules.unregister
方法登出module
,然後在store._withCommit
中將該module
的state
通過Vue.delete
移除,最後呼叫resetStore
方法,需要再看看resetStore
的實現
function resetStore (store, hot) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
store._modulesNamespaceMap = Object.create(null)
const state = store.state
// init all modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}
複製程式碼
這裡是將_actions
_mutations
_wrappedGetters
_modulesNamespaceMap
都清空,然後呼叫installModule
和resetStoreVM
重新進行全部模組安裝和_vm
的設定
- _withCommit (fn)
用於執行mutation
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
複製程式碼
在執行mutation
的時候,會將_committing
設定為true,執行完畢後重置,在開啟strict
模式時,會監聽state
的變化,當變化時_committing
不為true時會給出警告
3. 輔助函式
為了避免每次都需要通過this.$store
來呼叫api,vuex
提供了mapState
mapMutations
mapGetters
mapActions
createNamespacedHelpers
等api,接著看看各api的具體實現,存放在src/helpers.js
3.1 一些工具函式
下面這些工具函式是輔助函式內部會用到的,可以先看看功能和實現,主要做的工作是資料格式的統一、和通過namespace
獲取module
/**
* 統一資料格式
* 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 } ]
* @param {Array|Object} map
* @return {Object}
*/
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
/**
* 返回一個函式,接受namespace和map引數,判斷是否存在namespace,統一進行namespace處理
* @param {Function} fn
* @return {Function}
*/
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
/**
* 根據namespace獲取module
* @param {Object} store
* @param {String} helper
* @param {String} namespace
* @return {Object}
*/
function getModuleByNamespace (store, helper, namespace) {
const module = store._modulesNamespaceMap[namespace]
if (process.env.NODE_ENV !== 'production' && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
複製程式碼
3.2 mapState
為元件建立計算屬性以返回 store
中的狀態
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
// 返回一個物件,值都是函式
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
// 如果存在namespace,拿該namespace下的module
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
// 拿到當前module的state和getters
state = module.context.state
getters = module.context.getters
}
// Object型別的val是函式,傳遞過去的引數是state和getters
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
複製程式碼
mapState
是normalizeNamespace
的返回值,從上面的程式碼可以看到normalizeNamespace
是進行引數處理,如果存在namespace
便加上名稱空間,對傳入的states
進行normalizeMap
處理,也就是資料格式的統一,然後遍歷,對引數裡的所有state
都包裹一層函式,最後返回一個物件
大概是這麼回事吧
export default {
// ...
computed: {
...mapState(['stateA'])
}
// ...
}
複製程式碼
等價於
export default {
// ...
computed: {
stateA () {
return this.$store.stateA
}
}
// ...
}
複製程式碼
3.4 mapGetters
將store
中的 getter
對映到區域性計算屬性中
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
// this namespace has been mutate by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
複製程式碼
同樣的處理方式,遍歷getters
,只是這裡需要加上名稱空間,這是因為在註冊時_wrapGetters
中的getters
是有加上名稱空間的
3.4 mapMutations
建立元件方法提交 mutation
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
// 返回一個物件,值是函式
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
// 執行mutation,
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
})
複製程式碼
和上面都是一樣的處理方式,這裡在判斷是否存在namespace
後,commit
是不一樣的,上面可以知道每個module
都是儲存了上下文的,這裡如果存在namespace
就需要使用那個另外處理的commit
等資訊,另外需要注意的是,這裡不需要加上namespace
,這是因為在module.context.commit
中會進行處理,忘記的可以往上翻,看makeLocalContext
對commit
的處理
3.5 mapAction
建立元件方法分發 action
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
複製程式碼
和mapMutations
基本一樣的處理方式
4. 外掛
Vuex
中可以傳入plguins
選項來安裝各種外掛,這些外掛都是函式,接受store
作為引數,Vuex
中內建了devtool
和logger
兩個外掛,
// 外掛註冊,所有外掛都是一個函式,接受store作為引數
plugins.forEach(plugin => plugin(this))
// 如果開啟devtools,註冊devtool
if (Vue.config.devtools) {
devtoolPlugin(this)
}
複製程式碼
// devtools.js
const devtoolHook =
typeof window !== 'undefined' &&
window.__VUE_DEVTOOLS_GLOBAL_HOOK__
export default function devtoolPlugin (store) {
if (!devtoolHook) return
store._devtoolHook = devtoolHook
// 觸發vuex:init
devtoolHook.emit('vuex:init', store)
// 時空穿梭功能
devtoolHook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})
// 訂閱mutation,當觸發mutation時觸發vuex:mutation方法,傳入mutation和state
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
})
}
複製程式碼
總結
到這裡基本vuex的流程原始碼已經分析完畢,分享下自己看原始碼的思路或者過程,在看之前先把官網的文件再仔細過一遍,然後帶著問題來看原始碼,這樣效率會比較高,利用chrome在關鍵點開啟debugger,一步一步執行,看原始碼的執行過程,資料狀態的變換。而且可以暫時遮蔽一些沒有副作用的程式碼,比如assert,這些函式一般都不會影響流程的理解,這樣也可以儘量減少原始碼行數。剩下的就是耐心了,前前後後斷斷續續看了很多次,總算也把這份分享寫完了,由於水平關係,一些地方可能理解錯誤或者不到位,歡迎指出。