首先這篇文章也是本人第一次發這種技術文章,錯別字,分析錯誤,不知道的東西在所不免,但願你們指正,目前本人仍是一位即將大四的學生,寫這個系列的目的也是爲了記錄在源碼中學習,經過寫博客讓我更加熟悉了源碼及其內部完完整整的實現,經過這篇文章也讓我對vuex的源碼變得很是熟悉,在寫這完篇文章以前,由於時間緣由,斷斷續續寫了兩個星期,雖然已經看完了,可是要所有分析完並寫出來,太耗費精力和時間。而後這個系列我打算按照這個順序來寫,我會堅持寫下來。排版可能有點差,我會慢慢學習,若是裏面有錯誤,大佬輕噴。。javascript
當咱們使用Vue.use
會調用vuex的install
方法,它的實現以下html
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構造函數,而後判斷若是_Vue === Vue
,則說明已經安裝過了就直接返回,不作處理。而後調用了applyMixin(Vue)
方法,咱們來看下applyMixin
方法實現vue
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 混入beforeCreate,vuexInit方法
Vue.mixin({ beforeCreate: vuexInit })
} else {
const _init = Vue.prototype._init
// 重寫_init方法,把vuexInit方法,掛載到options中
Vue.prototype._init = function (options = {}) {
// 這裏作了兼容處理,若是有其餘庫也使用了init方法,就把vuexInit添加到Init數組中
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/** * Vuex init hook, injected into each instances init hooks list. */
// 這個方法的做用就是可讓每一個組件都能經過this.$store放問到store對象
function vuexInit () {
// 獲取mergeoptios選線
const options = this.$options
// 若是存在store屬性
if (options.store) {
// 若是store是一個方法,就調用store,不然直接使用
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 獲取父親的$store屬性
this.$store = options.parent.$store
}
}
}
複製代碼
其實整個函數看起來彷佛有點複雜java
Vue.mixin({ beforeCreate: vuexInit })
複製代碼
其實只是調用了這段代碼,由於這是vue2.0版本及以上纔有的方法,咱們這裏只討論vue2.0的狀況,關於mixin
的用法,這裏不作介紹,它爲全部的組件添加beforeCreate
生命週期鉤子react
下面咱們看一下vuexInit
方法的實現ios
// 這個方法的做用就是可讓每一個組件都能經過this.$store放問到store對象
function vuexInit () {
// 獲取mergeoptions的選項
const options = this.$options
// 這段if邏輯其實實在根組件中,添加了一個store屬性,並賦給this.$store
if (options.store) {
// 若是store是一個方法,就調用store,不然直接使用
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 獲取父親的$store屬性
this.$store = options.parent.$store
}
}
複製代碼
首先,獲取了this.$options
,這段代碼,若是你們有看過vue源碼的應該知道,這是mergeOptions
後的options
, 先是判斷是否存在store
屬性,若是不存在,就在父組件中查找,若是有就使用父組件中的$store
,經過這種方式,可以在組件之間造成一種鏈式查找,其實本質上是引用了,根組件中的store
,舉個例子web
new Vue({
router,
store, // $store實際最終指向的都是這裏的store
render: h => h(App)
}).$mount('#app')
複製代碼
安裝install
完成以後,咱們來看看new Vuex.Store(options)
發生了什麼,因爲源碼太多,就只截取構造函數中的代碼,一塊兒來看,vuex進行了哪些初始化操做面試
constructor (options = {}) {
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.`)
}
const {
plugins = [],
strict = false //使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤。
} = options
this._committing = false // 正在提交
this._actions = Object.create(null) // actions對象
this._actionSubscribers = [] // actions訂閱數組
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options) // 收集modules,
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
// 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
// 根module的state屬性
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
複製代碼
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.`)
}
複製代碼
這段代碼咱們不作討論,相信你們也知道什麼意思vuex
const {
//一個數組,包含應用在 store 上的插件方法。這些插件直接接收 store 做爲惟一參數,能夠監聽 mutation(用於外部地數據持久化、記錄或調試)或者提交 mutation (用於內部數據,例如 websocket 或 某些觀察者)
plugins = [],
strict = false //使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤。
} = options
複製代碼
上面這段代碼,獲取了咱們傳入的配置plugins
和strict
,上面代碼中標註有每一個屬性的做用,關於詳細的使用能夠到官網查看,之後會有講解express
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()
複製代碼
這些代碼作了一些屬性的初始化,咱們暫且不看具體是幹什麼用的,關鍵是下面這段代碼
this._modules = new ModuleCollection(options)
複製代碼
看到這段代碼,咱們確定能立馬想到,咱們傳入的modules
配置,咱們來看看modules
作了哪些初始化
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
複製代碼
這個類的構造函數只有簡簡單單的一行代碼,它的參數rawRootModule
,是咱們給Vuex.Store(options)
傳入的完整的options
,接下來看看register
方法作了什麼
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
// 建立Module對象,初始runtime爲false
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// this.root = new Module(rawModule, runtime)
this.root = newModule
} else {
// 若是path = ['user', 'login'], path.slice(0, -1) = ['user'] 會去掉最後一個
// parent是根模塊
const parent = this.get(path.slice(0, -1))
// 把模塊添加到根Module對象的_children對象中,形式以下
// _children = {
// user: new Module(user, runtime)
// }
parent.addChild(path[path.length - 1], newModule)
}
// 若是options中存在modules屬性
if (rawModule.modules) {
// 遍歷modules都西昂
forEachValue(rawModule.modules, (rawChildModule, key) => {
// 獲取每一個module對應的options
/*{ modules: { user: { state, mutations }, login }, state: { }, mutations: { } }*/
// 看到上面的形式,若是modules裏有options,繼續遞歸遍歷,
// path = ['user', 'login']
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
複製代碼
const newModule = new Module(rawModule, runtime)
複製代碼
代碼一上來就建立了一個Module
對象,並把options
做爲參數傳入,咱們繼續看看Module
這個類中作了哪些操做
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
// 獲取state
const rawState = rawModule.state
// 若是state是個方法就調用
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// ...其餘方法
}
複製代碼
上面的構造函數進行了一些初始化,this.runtime
記錄了是不是運行時,this._children
初始化爲空對象,它主要是用來,保存當前模塊的子模塊,this._rawModule
記錄了,當前模塊的配置,而後又對state
進行了些處理。而後咱們大概知道了new Module
作了什麼
其餘並非很重要,咱們先不提,再回到new ModuleCollection(options)
,構造函數中
const newModule = new Module(rawModule, runtime)
複製代碼
這裏拿到了Module
對象
if (path.length === 0) {
// this.root = new Module(rawModule, runtime)
this.root = newModule
} else {
// 若是path = ['user', 'login'], path.slice(0, -1) = ['user'] 會去掉最後一個
// parent是根模塊
const parent = this.get(path.slice(0, -1))
// 把模塊添加到根Module對象的_children對象中,形式以下
// _children = {
// user: new Module(user, runtime)
// }
parent.addChild(path[path.length - 1], newModule)
}
複製代碼
這是一段邏輯判斷,而這個path
是在ModuleCollection
構造函數中,傳入的,初始時爲空
this.register([], rawRootModule, false)
/** * * @param {*} path 初始爲空數組 * @param {*} rawModule options * @param {*} runtime 初始爲false */
register (path, rawModule, runtime = true) {...}
複製代碼
if (path.length === 0) {
// this.root = new Module(rawModule, runtime)
this.root = newModule
} else {...}
複製代碼
他把ModuleCollection
對象的root屬性設置爲一個Module
對象,也就是表明根module,而else中的邏輯咱們暫時不看,由於後面會有遞歸,下個週期時會進入else分支
// 若是options中存在modules屬性
if (rawModule.modules) {
// 遍歷modules
forEachValue(rawModule.modules, (rawChildModule, key) => {
// 獲取每一個module對應的options
/*{ modules: { user: { state, mutations }, login }, state: { }, mutations: { } }*/
// 看到上面的形式,若是modules裏有options,繼續遞歸遍歷,
// path = ['user', 'login']
this.register(path.concat(key), rawChildModule, runtime)
})
}
複製代碼
這段代碼,拿到了當前模塊的配置,注意:根模塊的配置其實就是options
, 而後判斷是否存在modules
,若是存在,就遍歷每一個模塊,這個forEachValue
方法,其實實現很是簡單,感興趣的能夠去看一下,最終回調函數遍歷到每一個module
,並獲取到module
對象和它的模塊對象的key
,也就是模塊名。
以後再次調用了下register
方法,遞歸執行
this.register(path.concat(key), rawChildModule, runtime)
複製代碼
注意:path.concat(key)
, path原本是空數組,在每次遞歸時都會拼接模塊的名字,這段代碼很是關鍵,後面的namespace
會有用到
而後咱們再次回到register
方法的開始
// 建立Module對象,初始runtime爲false
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// this.root = new Module(rawModule, runtime)
this.root = newModule
} else {
// 若是path = ['user', 'login'], path.slice(0, -1) = ['user'] 會去掉最後一個
// parent是根模塊
const parent = this.get(path.slice(0, -1))
// 把模塊添加到根Module對象的_children對象中,形式以下
// _children = {
// user: new Module(user, runtime)
// }
parent.addChild(path[path.length - 1], newModule)
}
複製代碼
依然是建立了Module
對象,此時的Module
已是子Module
了, if-else
判斷也會執行到else
中
if (path.length === 0) {
//...
} else {
// 若是path = ['user', 'login'], path.slice(0, -1) = ['user'] 會去掉最後一個
// parent是根模塊
const parent = this.get(path.slice(0, -1))
// 把模塊添加到根Module對象的_children對象中,形式以下
// _children = {
// user: new Module(user, runtime)
// }
parent.addChild(path[path.length - 1], newModule)
}
複製代碼
假如咱們有兩個module
,它會獲取到除了最後一個的全部module
的key列表,並調用get
方法
get (path) {
return path.reduce((module, key) => {
// 獲取子模塊
return module.getChild(key)
}, this.root)
}
複製代碼
這段是get
方法的實現,它實際上是返回path對應模塊的子模塊
parent.addChild(path[path.length - 1], newModule)
複製代碼
從最後,把模塊添加到,當前模塊的_children
對象中
addChild (key, module) {
this._children[key] = module
}
複製代碼
最後,經過ModuleCollection
對象的root
,就能夠拿到Module
對象樹
相似這樣
new Vuex.Store({
modules:{
user: {
modules:{
login
}
},
cart: {
}
}
})
// 模擬一下
ModuleCollection = {
root = 根Module: {
_children: {
子module(user): {
_children: {
子module(login)
}
},
子module(cart)
}
}
}
複製代碼
小總結:new ModuleCollection(options)在root這個屬性上掛載了一個由module對象組成的樹
咱們回到new Vuex.Store(options)
時的構造函數
this._modules = new ModuleCollection(options)
複製代碼
this._modules
拿到了模塊的集合
// 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)
}
複製代碼
這段代碼,重寫了dispatch
和commit
方法,其實至關於調用了bind
方法,我我的認爲也能夠改寫成這樣
this.dispatch = this.dispatch.bind(store, type, payload)
this.commit = this.commit.bind(store, type, payload)
複製代碼
繼續後面的步驟
this.strict = strict
複製代碼
strict
使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤
// 根module的state屬性
const state = this._modules.root.state
複製代碼
保存根模塊的state
屬性
installModule(this, state, [], this._modules.root)
複製代碼
這段代碼雖然簡短,可是很是重要,咱們來具體分析installModule
方法
/** * * @param {*} store store對象 * @param {*} rootState 根module的state對象 * @param {*} path 初始爲空數組 * @param {*} module 根module對象 * @param {*} hot */
function installModule (store, rootState, path, module, hot) {
}
複製代碼
它的參數如上
// 若是是空數組,說明是根module
const isRoot = !path.length
複製代碼
判斷是不是根模塊
// 返回由module名字 拼接成的字符串
const namespace = store._modules.getNamespace(path)
複製代碼
這段代碼頗有意思,咱們來看下getNamespace
方法,它在ModuleCollection
類中
getNamespace (path) {
// 根module
let module = this.root
return path.reduce((namespace, key) => {
// 獲取子module
module = module.getChild(key)
// 若是模塊的namespace存在, 舉個列子: 一層模塊 user/, 二層模塊: user/login/
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
複製代碼
直接作一個簡單的例子,若是咱們在每一個模塊中使用了namespaced
,設置爲true
,當咱們調用commit
,dispatch
等方法時,咱們須要這樣作
this.$store.dispatch('count/increment')
this.$store.commit('count/INCREMENT')
複製代碼
getNamespace
要作的其實就是獲取到count/increment
前面的count/
,並返回
// 若是namespaced存在
if (module.namespaced) {
// 初始時store._modulesNamespaceMap[namespace]是不存在的
if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
// namespace對應module
store._modulesNamespaceMap[namespace] = module
}
複製代碼
這段代碼作的事情,就是把namespace
和module
做爲key,value保存在store
對象的_modulesNamespaceMap
屬性上,關於這個屬性在什麼地方用,能夠參考helper.js
的getModuleByNamespace
方法,這個方法是實現mapActions
,mapMutations
的關鍵,之後也會講到
而後是這段代碼
// 若是不是根root module ,初始時hot也不存在, 初始時hot爲ture,因此不會執行下面的
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
複製代碼
isRoot
想必不用多說,就是判斷是不是根模塊,而hot
這個變量又是哪裏來的呢,他是installModule
方法傳入的一個參數,初始時他是空的,但這又有什麼用處呢;emmm,因爲我本身不多用到,我就很少作詳細介紹了(由於菜,因此沒用過),具體用法官方文檔有詳細介紹
咱們繼續,前面說到,hot
是不存在的,而當前又是根節點,因此也不會執行這個if邏輯,可是咱們仍是要講一下,否則一會還要回來說,首先看一下getNestedState
方法實現
const parentState = getNestedState(rootState, path.slice(0, -1))
// 具體實現
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
複製代碼
首先它的第一個參數是state
,也就是當前模塊的state
,注意不必定是rootState
,不要被調用參數誤解,其實是遞歸引用的傳遞,這個函數就是判斷當前path
是否爲空,若是爲空,表示它是根模塊的state
,不爲空表示爲子模塊的state
,要注意的是path.slice(0, -1)
,它獲取了除了自己模塊名以前的模塊名數組,getNestedState
函數直接來講就是用來獲取父模塊的state
,從字面意思也能夠理解,至於reduce的一些操做就不詳細講解了。
const moduleName = path[path.length - 1]
複製代碼
而後就是獲取了當前模塊名,接下來關鍵來了
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
複製代碼
從字面意思,好像是跟隨commit調用?沒錯就是這樣。。
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
// 從新設置以前的提交狀態
this._committing = committing
}
複製代碼
它就簡單的調用了傳入的回調函數,設置了先後的狀態,而後來看下回調函數的內部
parentState:父模塊的state
moduleName:當前模塊名
module.state:當前模塊的state
Vue.set(parentState, moduleName, module.state)
複製代碼
關於Vue.set
方法的介紹:向響應式對象中添加一個屬性,並確保這個新屬性一樣是響應式的,且觸發視圖更新。它必須用於向響應式對象上添加新屬性,由於 Vue 沒法探測普通的新增屬性
也就是說,它能夠在把每一個state
屬性變爲響應式,在commit
以前,爲何在以前呢,由於這是初始化階段,咱們沒有主動調用commit
咱們繼續後面的代碼
// 重寫了dispatch, commit ,getter,state等方法,所有掛載到了當前模塊的context屬性上
const local = module.context = makeLocalContext(store, namespace, path)
複製代碼
下面我將詳細講解makeLocalContext
方法
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
// 若是不存在namespace,就重寫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) {
// 使用namespace拼接action的類型
type = namespace + type
// 若是不使用 namespace/action的形式調用action就會報錯
if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
},
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
if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
複製代碼
這面代碼返回了一個local對象,而且這些對象對dispatch
,commit
等方法還有state
,getter
進行了包裝
const noNamespace = namespace === ''
複製代碼
這段代碼用來判斷是否存在命名空間namespace
,而後咱們再來看下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) {
// 使用namespace拼接action的類型
type = namespace + type
// 若是不使用 namespace/action的形式調用action就會報錯
if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
},
複製代碼
首先判斷是否有命名空間,若是沒有就是正常的dispatch
,若是存在,則先統一對象風格unifyObjectStyle
先來看下unifyObjectStyle
實現,具體講解就寫在註釋裏了
// 統一對象風格
function unifyObjectStyle (type, payload, options) {
//
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
if (process.env.NODE_ENV !== 'production') {
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}
return { type, payload, options }
}
複製代碼
在看這段代碼以前,先說一下,通常來講咱們都是這樣使用dispatch
store.dispatch('incrementAsync', {
amount: 10
})
複製代碼
但其實也能夠這樣,而且官方文檔也有例子
store.dispatch({
type: 'incrementAsync',
amount: 10
})
複製代碼
知道這些咱們就繼續往下分析
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
複製代碼
這裏是對參數進行了簡單的處理,統一處理成了咱們日常使用的模式,最後返回了相應的type, payload, options
接下來,回到makeLocalContext
方法
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
// 使用namespace拼接action的類型
type = namespace + type
// 若是不使用 namespace/action的形式調用action就會報錯
if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
},
複製代碼
統一這些參數之後,又是一個if判斷,第三個參數用的也不多,可是官方文檔是有說明的,options
裏能夠有 root: true
,它容許在命名空間模塊裏提交根的 mutation或action
,而後返回了調用store.dispatch
方法的返回值,而後咱們來看看包裝後的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
if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
複製代碼
這段代碼和dispatch
的實現很是類似,就不講解了,所作的事情就是對參數進行統一
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
複製代碼
而後這段代碼是把state
和getter
代理到了local對象上,
判斷當前模塊是否有命名空間,若是不是,就不作任何處理,不然調用makeLocalGetters
方法,並傳入store
對象和namespace
完整模塊字符串,至於這個namespace
是什麼,能夠往前翻一翻,有具體的講解。好比user/login
,表示user模塊下的login模塊的namespace。而後咱們來看看makeLocalGetters
作了什麼
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// 截取getter中的namespace,若是不相等,就不作處理
if (type.slice(0, splitPos) !== namespace) return
// 獲取getter 的namespace後面的字符串
const localType = type.slice(splitPos)
Object.defineProperty(gettersProxy, localType, {
// 把getters中的屬性方法,代理到新的對象中
get: () => store.getters[type],
enumerable: true
})
})
return gettersProxy
}
複製代碼
這個函數被調用說明必定是有namespace
的,而後遍歷getter
,此時的getter
的屬性名是包含有namespace
的,至於爲何會有,這個在之後的registerGetters
中會有講解。而後獲取到namespace
後面真實的getter
屬性名,並被代理到一個新的對象中,而且被獲取時,仍然是使用了完整的namespace
,舉個例子
假設模塊: user/todo
store.getters.doSomething()
等價於
store.getters['user/todo/doSomething']()
複製代碼
看完這些相信你們都明白了
調用了getNestedState
方法,這個方法想必不用多說,前面也有講過,用來獲取模塊的父模塊state
,並返回
咱們再回到一開始,調用makeLocalContext
的位置, 返回的local對象,最終放在了模塊的context
屬性上
const local = module.context = makeLocalContext(store, namespace, path)
複製代碼
接下來咱們繼續分析,後面的內容
// 遍歷mutations
module.forEachMutation((mutation, key) => {
// 把namespace和mutation名進行拼接
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
複製代碼
這段代碼,簡單來講就是遍歷了,當前模塊的全部mutations
,並對每一個mutation
調用了registerMutation
方法,傳入了store
對象,完整的namespace + commit名
,mutation函數
,以及local
對象,接下來看看registerMutation
方法實現,至於forEachMutation
方法,你們能夠本身看一下,實現也很簡單
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
// 調用mutation, 並傳入state和參數
handler.call(store, local.state, payload)
})
}
複製代碼
這個函數,其實是把當前模塊的mutation
放在了一個_mutations
對象中,那這個屬性在哪定義的呢
this._mutations = Object.create(null)
複製代碼
實際上在Store
類的構造函數的時候已經初始化爲了一個空對象,registerMutation
所作的事情,就是把mutations
和namespaceType
,造成一個映射關係,而且mutations
是一個數組,好比這樣
{
'user/todo/INCREMENT': [
function() {...}
]
}
複製代碼
這裏之因此用數組的形式存儲函數,我以爲是爲了防止重複定義mutation
,由於調用以後只有最後一個會生效
entry.push(function wrappedMutationHandler (payload) {
// 調用mutation, 並傳入state和參數
handler.call(store, local.state, payload)
})
複製代碼
而後就是把mutation
的調用放在一個函數中,傳入了state,payload,在真正調用commit
的時候纔會循環調用,真實的mutation
下面咱們繼續看後面的代碼
module.forEachAction((action, key) => {
// namespace + type
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
複製代碼
這裏和前面的處理差很少,只是有個判斷,若是action存在root說明是根模塊,因此直接用key
就行了,options
裏能夠有 root: true
,它容許在命名空間模塊裏提交根的 mutation,不然就使用namespace
和key
拼接成的action名,而後咱們來看registerAction
是實現
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
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)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
// 這是給devTool用的,能夠不用關心
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
複製代碼
咱們暫且不看wrappedActionHandler
函數裏面的內容,它的處理依舊和mutation
的處理同樣,也是把action放在_actions
對象中,而後再看wrappedActionHandler
裏的內容,它調用了action
,而且讓他this指向了store
,傳入了,local
對象中的dispatch
,commit
等方法還有state
,getter
,這不就是咱們以前看到的,通過處理後的API方法嗎。
而後它拿到action
調用以後的返回值,最終返回了一個Promise.resolve(res)
,也就是一個Promise
經過上面這些代碼,咱們能在實際中這麼用
注意:commit, dispatch,getters,state都是當前模塊裏的方法和對象
{
actions: {
async increment({ commit, dispatch, getters,state, rootGetters, rootState }) {
return await getData()
}
}
}
複製代碼
說完了registerAction
,咱們來講一說registerGetter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
複製代碼
很少廢話,直接看registerGetter
的實現
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
複製代碼
一上來就是一個判斷,簡單點來講就是,不容許有重複定義的getters
,咱們以前是看到actions
和mutation
是能夠重複定義的。而後再來看其餘的,它和以前的處理有所不一樣,但也相差不大,由於不容許有重複,因此就不須要push一個函數了,直接調用了getter
方法,傳入了state
,getters
,根state
,根getters
,咱們能夠這樣用
{
['INCREMENT']: function(state, getters, rootState, rootGetters){
//...
}
}
複製代碼
講完這些installModule
基本上要結束了,咱們看最後一段代碼
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
複製代碼
沒錯,是個遞歸,它拿到了子模塊進行了遞歸,你們能夠翻到前面梳理一下流程
installModule
方法咱們也講完了,咱們要回到Store類的構造函數中,看看還有些什麼初始化操做
resetStoreVM(this, state)
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
複製代碼
接下來分析resetStoreVM
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure enviroment.
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
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())
}
}
複製代碼
首先看一下store._vm
是什麼,若是有注意到這個函數中間的一段代碼的話能夠看到,_vm
是又建立了一個Vue實例,這個咱們後面講。而後在store
上定義了一個對象getters
,而後遍歷以前,registerGetters
註冊的getter
,而後是這段代碼
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure enviroment.
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// partial函數實現
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
複製代碼
首先是遍歷全部getters
,調用partial
函數,返回了一個新函數,並把它放入computed
對象中,後面的代碼實際上是作了這件事
$store.getter
等價於
$store._vm.getter
複製代碼
把getter
代理到了一個新的Vue實例的computed
對象上,這在後面的代碼有所體現
const silent = Vue.config.silent
// 啓動Vue的日誌和警告
Vue.config.silent = true
store._vm = new Vue({
data: {
// 把state放在Vue的data中
$$state: state
},
computed // 把全部getter放在了computed中
})
複製代碼
這段代碼相信不會陌生,vuex之因此可以響應式,緣由就在這裏,咱們經過調用mutation
,修改了state
,會觸發頁面更新,實際上是Vue的幫助
咱們繼續看後面的代碼
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// 強制getters從新計算
store._withCommit(() => {
oldVm._data.$$state = null
})
}
// 防止重複建立Vue實例(我的理解)
Vue.nextTick(() => oldVm.$destroy())
}
複製代碼
首先是判斷strict
是否爲true, 表示是嚴格模式,若是直接更改state,會報錯,咱們看一下它的實現
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
複製代碼
很關鍵的是中間的箭頭函數,咱們能夠直接看一下Vue源碼的實現,它是如何實現修改state報錯
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true // 很關鍵的屬性
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
複製代碼
這段代碼有個地方很關鍵,options.user = true
,它被傳入了Watcher
對象中,還有咱們傳入了箭頭函數cb
咱們看看Watcher哪裏有使用到user
屬性
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// ...
}
複製代碼
我先說一下,這個run
方法在什麼時機調用的,它是在set
屬性訪問器內部調用notify
以後,watcher
會調用自身的update
方法,而後run
就會被調用,可能說的不太清楚,若是各位有時間能夠看一下,這裏只針對strict
原理來說
下面咱們只看這段代碼
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
複製代碼
咱們知道以前傳入的user
屬性爲true, 若是調用回調是必定會拋出錯誤的
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
複製代碼
這就是strict
模式下,直接修改state
會報錯的緣由
講完這些,其實後面的代碼就簡單略過了,也不是很重要(懶?)
而後咱們來看Store
構造函數中最後一點內容
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
複製代碼
首先調用了全部的plugin
,並傳入了store
對象,關於plugin
的用法官方文檔都有介紹。而後關於useDevtools
內容我就不講解了,它和devTool相關
終於講完了初始化,咱們開始講Vuex
的一些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) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => {
// 遍歷type對應的mutation數組
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 遍歷全部訂閱,並傳入mutation對象和狀態
this._subscribers.forEach(sub => sub(mutation, this.state))
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
複製代碼
首先是調用unifyObjectStyle
方法,統一對象風格,若是有看前面的內容的話,應該知道,這是用來處理如下兩種狀況的參數
commit(type: string, payload?: any, options?: Object)
commit(mutation: Object, options?: Object)
複製代碼
而後是下面這段
const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
複製代碼
若是commit
的mutation
不存在的話,就會報出警告,並返回不作處理
this._withCommit(() => {
// 遍歷type對應的mutation數組
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
複製代碼
_withCommit
方法前面也有講過,簡單點說其實就是調用傳入的回調函數,這裏循環調用了mutation
,至於爲何是數組,前面有講到,是在registerMutation
方法
咱們繼續來看
// 遍歷全部訂閱,並傳入mutation對象和狀態
this._subscribers.forEach(sub => sub(mutation, this.state))
// silent屬性已經被刪除,不讓使用
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
複製代碼
this._subscribers
屬性也是在Store
對象的構造函數初始化時建立的一個數組,看到這個數組的名字,不用多說確定是發佈訂閱模式,而後循環調用訂閱的回調函數,它是在mutation
被調用後執行, 可是在哪裏訂閱的呢,實際上是在subscribe
方法,它也是Vuex的一個API,下面咱們來具體講講
訂閱 store 的 mutation。handler
會在每一個 mutation
完成後調用,接收 mutation 和通過 mutation 後的狀態做爲參數
subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
}
複製代碼
function genericSubscribe (fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
複製代碼
這就是一個簡單的發佈訂閱模式的應用,把回調存儲在了訂閱數組中,其中genericSubscribe
方法利用了閉包,返回了一個函數,調用它以後就能夠取消訂閱,其實還有其餘的訂閱方法,subscribeAction
subscribeAction (fn) {
const subs = typeof fn === 'function' ? { before: fn } : fn
return genericSubscribe(subs, this._actionSubscribers)
}
複製代碼
判斷是不是一個函數,若是是默認爲before
函數,也就是在dispatch
調用action
以前調用,若是是{after: fn}
就會在action
以後調用
// 執行了beforeActions全部回調
// 執行全部actions,並拿到全部promise返回的結果
// 執行了afterActions全部回調
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
return res
})
}
複製代碼
前面關於對象統一,以及是否存在action
的判斷就不講了
try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
複製代碼
而後過濾篩選獲取到了訂閱的一些before
函數,也就是在調用action
以前調用,並傳入了action
, action = { type, payload }
以及state
響應式地偵聽 fn
的返回值,當值改變時調用回調函數。fn
接收 store 的 state 做爲第一個參數,其 getter 做爲第二個參數。最後接收一個可選的對象參數表示 Vue 的 vm.$watch
方法的參數。
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)
}
// Store構造函數初始化時
this._watcherVM = new Vue()
複製代碼
這裏給偵聽函數裏的,getter
傳入了state
和getters
, 當state
發生變化時,偵聽函數的返回值也發生了變化,值改變後就會觸發cb
回調函數, 關於vm.$watch
的用法,能夠參考Vue的官方文檔vm.$watch
替換 store 的根狀態,僅用狀態合併或時光旅行調試。
this._withCommit(() => {
this._vm._data.$$state = state
})
複製代碼
直接替換掉了$$state
本來狀態
能夠註冊模塊,例子:
// 註冊模塊 `myModule`
store.registerModule('myModule', {
// ...
})
// 註冊嵌套模塊 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
複製代碼
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
和一些斷言,而後調用了register
方法installModule
方法,resetStoreVM
方法,這幾個方法前面都有講到,至關於又建立了一個Store
對象,流程也差很少
卸載一個動態模塊。
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)
}
複製代碼
前面是對path
模塊名進行了處理以及斷言是不是數組,而後調用unregister
this._modules.unregister(path)
unregister (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
if (!parent.getChild(key).runtime) return
parent.removeChild(key)
}
複製代碼
這裏獲取到了傳入模塊名,也就是path
的父模塊,而後獲取子模塊判斷是否存在runtime
屬性,這個屬性是幹嗎的,我也不是很清楚,但願又大佬解惑(菜 !- -,沒辦法啊)
parent.removeChild(key)
removeChild (key) {
delete this._children[key]
}
複製代碼
最後刪除了子模塊,也就是咱們要刪除的模塊
熱替換新的 action 和 mutation
官方的例子
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'
Vue.use(Vuex)
const state = { ... }
const store = new Vuex.Store({
state,
mutations,
modules: {
a: moduleA
}
})
if (module.hot) {
// 使 action 和 mutation 成爲可熱重載模塊
module.hot.accept(['./mutations', './modules/a'], () => {
// 獲取更新後的模塊
// 由於 babel 6 的模塊編譯格式問題,這裏須要加上 `.default`
const newMutations = require('./mutations').default
const newModuleA = require('./modules/a').default
// 加載新模塊
store.hotUpdate({
mutations: newMutations,
modules: {
a: newModuleA
}
})
})
}
複製代碼
熱模塊更新源碼以下
hotUpdate (newOptions) {
this._modules.update(newOptions)
resetStore(this, true)
}
複製代碼
this._modules.update(newOptions)
方法是在module-collection.js
文件中定義
update (rawRootModule) {
update([], this.root, rawRootModule)
}
複製代碼
function update (path, targetModule, newModule) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, newModule)
}
// update target module
targetModule.update(newModule)
// update nested modules
if (newModule.modules) {
for (const key in newModule.modules) {
// 若是傳入的配置中沒有該模塊就報錯
if (!targetModule.getChild(key)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
}
return
}
update(
path.concat(key),
targetModule.getChild(key),
newModule.modules[key]
)
}
}
}
複製代碼
以上代碼總的來講就是遞歸遍歷模塊,並更新模塊,其中涉及到三個update
方法,你們不要弄混。
update([], this.root, rawRootModule)
複製代碼
主要傳入了,一個空數組,本來的根模塊對象,要用來替換的模塊配置
function update (path, targetModule, newModule) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, newModule)
}
// update target module
targetModule.update(newModule)
// update nested modules
if (newModule.modules) {
for (const key in newModule.modules) {
// 若是傳入的配置中沒有該模塊就報錯
if (!targetModule.getChild(key)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
}
return
}
update(
path.concat(key),
targetModule.getChild(key),
newModule.modules[key]
)
}
}
複製代碼
遞歸遍歷,本來的模塊樹,使用新模塊替換掉本來模塊
以上代碼中還有一個模塊中的update
方法,即targetModule.update(newModule)
// update target module
targetModule.update(newModule)
// module.js
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
複製代碼
這個方法其實很簡單,替換掉了本來的模塊。
mapXXX
方法都在helper.js
文件中
// helper.js
export const mapState = normalizeNamespace((namespace, states) => {
//..
})
export const mapMutations = normalizeNamespace((namespace, mutations) => {
// ..
})
// ...
複製代碼
能夠看到他們都調用了normalizeNamespace
方法,咱們知道mapXxx
是一個方法,因此它必定會返回一個方法
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
不是字符串,也就是說它可能不存在,namespace
就設置爲一個空字符串,好比這樣
{
computed: {
...mapState(['username'])
}
}
複製代碼
若是傳入了namespace
字符串,而且最後沒有斜槓,就自動幫它加上,最後纔是調用真實的mapXXX
,好比這樣
{
computed: {
...mapState('user/', ['username'])
}
}
複製代碼
接下來咱們看一下mapState
實現
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) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
複製代碼
首先又是調用了一個normalizeMap
方法,傳入了咱們須要獲取的states
,normalizeMap
實現以下
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
複製代碼
這段代碼看起來可能有點複雜,舉個例子
normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
normalizeMap(['user', 'count']) => [ { key: 'user', val: 'user' }, { key: 'count', val: 'count' }]
normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
複製代碼
而後咱們回到以前的代碼
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) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
複製代碼
細心的童鞋可能注意到了,整個mapState
返回的是一個對象,其形式以下,其餘mapMutations
,mapActions
均可以這樣
mapState('user', ['username', 'password'])
{
username: function(){},
password: function(){}
}
mapMutation('count', ['increment'])
複製代碼
如今知道爲啥mapState
要寫在computed
裏了吧!緣由就在這裏。爲了方便我就直接用註釋分析了
res[key] = function mappedState () {
// store對象中的state,這個state是根state
let state = this.$store.state
// 根getters
let getters = this.$store.getters
// 若是傳入了namespace
if (namespace) {
// 調用getModuleByNamespace方法,源碼實如今下方,它返回namespace對應的模塊
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
// 有看過前面源碼應該記得,不少方法和對象都掛載到了context屬性上
state = module.context.state
getters = module.context.getters
}
// 調用val或獲取state
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
複製代碼
function getModuleByNamespace (store, helper, namespace) {
// _modulesNamespaceMap屬性是否是很眼熟?
// 它是在Store類的installModule方法中使用到,記錄了namespace對應的module
const module = store._modulesNamespaceMap[namespace]
if (process.env.NODE_ENV !== 'production' && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
複製代碼
上面這些代碼有幾個注意點
getModuleByNamespace
方法中的store._modulesNamespaceMap[namespace]
是在installModules
中進行的初始化
mapState
是能夠傳入回調函數的
{
computed: mapState({
// 箭頭函數可以使代碼更簡練
count: state => state.count,
// 傳字符串參數 'count' 等同於 `state => state.count`
countAlias: 'count',
// 爲了可以使用 `this` 獲取局部狀態,必須使用常規函數
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
複製代碼
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
}
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
})
複製代碼
其餘相同的代碼就不講了,關鍵看下面的
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
}
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
複製代碼
這段代碼其實和mapState
裏的相差不大,都是獲取到commit
,若是有namespace
就獲取模塊裏的commit
,最後調用commit
,它也能夠傳入一個回調函數,不過,舉個例子
methods: {
...mapMutations(['increment']),
//等價於
...mapMutations({
add: function(commit, ...args){
commit('increment', ...args)
}
}),
// 等價於
...mapMutations({
add: 'increment' // 將 `this.add()` 映射爲 `this.$store.commit('increment')`
})
}
// 組件中調用
this.add(1)
複製代碼
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
// 若是namespace存在可是沒有找到對應的模塊 就直接返回,不作處理
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
// 若是沒有找到對應的getter會報錯並返回
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
})
複製代碼
mapGetters和其它實現有所區別
全部模塊的getters
都被代理在store
對象中,因此直接使用getter
的key
和namespace
拼接獲取到對應的getter
;具體在哪代理能夠參見
// store.js 的makeLocalContext方法裏的實現
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
複製代碼
getter
不支持傳入函數
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
})
複製代碼
mapActions
的實現和mutation
的實現如出一轍?確實是這樣。。。下面只說下用法
methods: {
...mapActions(['increment']),
//等價於
...mapActions({
add: function(dispatch, ...args){
dispatch('increment', ...args)
}
}),
// 等價於
...mapActions({
add: 'increment' // 將 `this.add()` 映射爲 `this.$store.dispatch('increment')`
})
}
// 組件中調用
this.add(1)
複製代碼
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})
複製代碼
官方例子
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
}
複製代碼
對於這個createNamespacedHelpers
如何實現,我想你們應該看的懂吧
終於分析完了Vuex
的源碼,完成這篇文章也是沒事抽出空閒時間寫出來的,可能會有錯別字,分析錯誤或者有些我不知道的,歡迎你們指正,閱讀源碼也使我學到了不少東西,讓我從陌生,逐漸開始駕輕就熟,一直到如今,我對於源碼再也不是單純的爲了面試,而是一種興趣,謝謝你們觀看
逐行級源碼分析系列(二) Redux和React-Redux源碼(正在寫做)
未完待續。。。