源自:blog.csdn.net/sinat_17775…vue
Vuex 常見的 API 如 dispatch、commit 、subscribe 咱們前面已經介紹過了,這裏就再也不贅述了,下面介紹的一些 Store 的 API,雖然不經常使用,可是瞭解一下也不錯。vuex
watch(getter, cb, options)編程
watch 做用是響應式的監測一個 getter 方法的返回值,當值改變時調用回調。getter 接收 store 的 state 做爲惟一參數。來看一下它的實現:redux
watch (getter, cb, options) {
assert(typeof getter === 'function', `store.watch only accepts a function.`)
return this._watcherVM.$watch(() => getter(this.state), cb, options)
}
複製代碼
函數首先斷言 watch 的 getter 必須是一個方法,接着利用了內部一個 Vue 的實例對象 ````this._watcherVM ``` 的 $watch 方法,觀測 getter 方法返回值的變化,若是有變化則調用 cb 函數,回調函數的參數爲新值和舊值。watch 方法返回的是一個方法,調用它則取消觀測。數組
registerModule(path, module)瀏覽器
registerModule 的做用是註冊一個動態模塊,有的時候當咱們異步加載一些業務的時候,能夠經過這個 API 接口去動態註冊模塊,來看一下它的實現:緩存
registerModule (path, module) {
if (typeof path === 'string') path = [path]
assert(Array.isArray(path), `module path must be a string or an Array.`)
this._runtimeModules[path.join('.')] = module
installModule(this, this.state, path, module)
// reset store to update getters...
resetStoreVM(this, this.state)
}
複製代碼
函數首先對 path 判斷,若是 path 是一個 string 則把 path 轉換成一個 Array。接着把 module 對象緩存到this._runtimeModules 這個對象裏,path 用點鏈接做爲該對象的 key。接着和初始化 Store 的邏輯同樣,調用 installModule 和 resetStoreVm 方法安裝一遍動態注入的 module。bash
unregisterModule(path)app
和 registerModule 方法相對的就是 unregisterModule 方法,它的做用是註銷一個動態模塊,來看一下它的實現:框架
unregisterModule (path) {
if (typeof path === 'string') path = [path]
assert(Array.isArray(path), `module path must be a string or an Array.`)
delete this._runtimeModules[path.join('.')]
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
}
複製代碼
函數首先仍是對 path 的類型作了判斷,這部分邏輯和註冊是同樣的。接着從 this._runtimeModules 裏刪掉以 path 點鏈接的 key 對應的模塊。接着經過 this._withCommit 方法把當前模塊的 state 對象從父 state 上刪除。最後調用 resetStore(this) 方法,來看一下這個方法的定義:
function resetStore (store) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
const state = store.state
// init root module
installModule(store, state, [], store._options, true)
// init all runtime modules
Object.keys(store._runtimeModules).forEach(key => {
installModule(store, state, key.split('.'), store._runtimeModules[key], true)
})
// reset vm
resetStoreVM(store, state)
}
複製代碼
這個方法做用就是重置 store 對象,重置 store 的 _actions、_mutations、_wrappedGetters 等等屬性。而後再次調用 installModules 去從新安裝一遍 Module 對應的這些屬性,注意這裏咱們的最後一個參數 hot 爲true,表示它是一次熱更新。這樣在 installModule 這個方法體類,以下這段邏輯就不會執行
function installModule (store, rootState, path, module, hot) {
...
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, state || {})
})
}
...
}
複製代碼
因爲 hot 始終爲 true,這裏咱們就不會從新對狀態樹作設置,咱們的 state 保持不變。由於咱們已經明確的刪除了對應 path 下的 state 了,要作的事情只不過就是從新註冊一遍 muations、actions 以及 getters。
回調 resetStore 方法,接下來遍歷 this._runtimeModules 模塊,從新安裝全部剩餘的 runtime Moudles。最後仍是調用 resetStoreVM 方法去重置 Store 的 _vm 對象。
hotUpdate(newOptions)
hotUpdate 的做用是熱加載新的 action 和 mutation。 來看一下它的實現:
hotUpdate (newOptions) {
updateModule(this._options, newOptions)
resetStore(this)
}
複製代碼
函數首先調用 updateModule 方法去更新狀態,其中當前 Store 的 opition 配置和要更新的 newOptions 會做爲參數。來看一下這個函數的實現:
function updateModule (targetModule, newModule) {
if (newModule.actions) {
targetModule.actions = newModule.actions
}
if (newModule.mutations) {
targetModule.mutations = newModule.mutations
}
if (newModule.getters) {
targetModule.getters = newModule.getters
}
if (newModule.modules) {
for (const key in newModule.modules) {
if (!(targetModule.modules && targetModule.modules[key])) {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
return
}
updateModule(targetModule.modules[key], newModule.modules[key])
}
}
}
複製代碼
首先咱們對 newOptions 對象的 actions、mutations 以及 getters 作了判斷,若是有這些屬性的話則替換 targetModule(當前 Store 的 options)對應的屬性。最後判斷若是 newOptions 包含 modules 這個 key,則遍歷這個 modules 對象,若是 modules 對應的 key 不在以前的 modules 中,則報一條警告,由於這是添加一個新的 module ,須要手動從新加載。若是 key 在以前的 modules,則遞歸調用 updateModule,熱更新子模塊。
調用完 updateModule 後,回到 hotUpdate 函數,接着調用 resetStore 方法從新設置 store,剛剛咱們已經介紹過了。
replaceState
replaceState的做用是替換整個 rootState,通常在用於調試,來看一下它的實現:
replaceState (state) {
this._withCommit(() => {
this._vm.state = state
})
}
複製代碼
函數很是簡單,就是調用 this._withCommit 方法修改 Store 的 rootState,之因此提供這個 API 是因爲在咱們是不能在 muations 的回調函數外部去改變 state。
到此爲止,API 部分介紹完了,其實整個 Vuex 源碼下的 src/index.js 文件裏的代碼基本都過了一遍。
Vuex 除了提供咱們 Store 對象外,還對外提供了一系列的輔助函數,方便咱們在代碼中使用 Vuex,提供了操做 store 的各類屬性的一系列語法糖,下面咱們來一塊兒看一下:
mapState
mapState 工具函數會將 store 中的 state 映射到局部計算屬性中。爲了更好理解它的實現,先來看一下它的使用示例:
// vuex 提供了獨立的構建工具函數 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭頭函數可讓代碼很是簡潔
count: state => state.count,
// 傳入字符串 'count' 等同於 `state => state.count`
countAlias: 'count',
// 想訪問局部狀態,就必須藉助於一個普通函數,函數中使用 `this` 獲取局部狀態
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
複製代碼
當計算屬性名稱和狀態子樹名稱對應相同時,咱們能夠向 mapState 工具函數傳入一個字符串數組。
computed: mapState([
// 映射 this.count 到 this.$store.state.count
'count'
])
複製代碼
經過例子咱們能夠直觀的看到,mapState 函數能夠接受一個對象,也能夠接收一個數組,那它底層到底幹了什麼事呢,咱們一塊兒來看一下源碼這個函數的定義:
export function mapState (states) {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
return typeof val === 'function'
? val.call(this, this.$store.state, this.$store.getters)
: this.$store.state[val]
}
})
return res
}
複製代碼
函數首先對傳入的參數調用 normalizeMap 方法,咱們來看一下這個函數的定義:
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
複製代碼
這個方法判斷參數 map 是否爲數組,若是是數組,則調用數組的 map 方法,把數組的每一個元素轉換成一個{key, val: key}的對象;不然傳入的 map 就是一個對象(從 mapState 的使用場景來看,傳入的參數不是數組就是對象),咱們調用 Object.keys 方法遍歷這個 map 對象的 key,把數組的每一個 key 都轉換成一個{key, val: key}的對象。最後咱們把這個對象數組做爲 normalizeMap 的返回值。
回到 mapState 函數,在調用了 normalizeMap 函數後,把傳入的 states 轉換成由 {key, val} 對象構成的數組,接着調用 forEach 方法遍歷這個數組,構造一個新的對象,這個新對象每一個元素都返回一個新的函數 mappedState,函數對 val 的類型判斷,若是 val 是一個函數,則直接調用這個 val 函數,把當前 store 上的 state 和 getters 做爲參數,返回值做爲 mappedState 的返回值;不然直接把 this.$store.state[val] 做爲 mappedState 的返回值。
那麼爲什麼 mapState 函數的返回值是這樣一個對象呢,由於 mapState 的做用是把全局的 state 和 getters 映射到當前組件的 computed 計算屬性中,咱們知道在 Vue 中 每一個計算屬性都是一個函數。
爲了更加直觀地說明,回到剛纔的例子:
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭頭函數可讓代碼很是簡潔
count: state => state.count,
// 傳入字符串 'count' 等同於 `state => state.count`
countAlias: 'count',
// 想訪問局部狀態,就必須藉助於一個普通函數,函數中使用 `this` 獲取局部狀態
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
複製代碼
通過 mapState 函數調用後的結果,以下所示:
import { mapState } from 'vuex'
export default {
// ...
computed: {
count() {
return this.$store.state.count
},
countAlias() {
return this.$store.state['count']
},
countPlusLocalState() {
return this.$store.state.count + this.localCount
}
}
}
複製代碼
咱們再看一下 mapState 參數爲數組的例子:
computed: mapState([
// 映射 this.count 到 this.$store.state.count
'count'
])
複製代碼
通過 mapState 函數調用後的結果,以下所示:
computed: {
count() {
return this.$store.state['count']
}
}
複製代碼
mapGetters
mapGetters 工具函數會將 store 中的 getter 映射到局部計算屬性中。它的功能和 mapState 很是相似,咱們來直接看它的實現:
export function mapGetters (getters) {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
res[key] = function mappedGetter () {
if (!(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
}
return this.$store.getters[val]
}
})
return res
}
複製代碼
mapGetters 的實現也和 mapState 很相似,不一樣的是它的 val 不能是函數,只能是一個字符串,並且會檢查val in this.$store.getters 的值,若是爲 false 會輸出一條錯誤日誌。爲了更直觀地理解,咱們來看一個簡單的例子:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用對象擴展操做符把 getter 混入到 computed 中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
複製代碼
通過 mapGetters 函數調用後的結果,以下所示:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
doneTodosCount() {
return this.$store.getters['doneTodosCount']
},
anotherGetter() {
return this.$store.getters['anotherGetter']
}
}
}
複製代碼
再看一個參數 mapGetters 參數是對象的例子:
computed: mapGetters({
// 映射 this.doneCount 到 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})
複製代碼
通過 mapGetters 函數調用後的結果,以下所示:
computed: {
doneCount() {
return this.$store.getters['doneTodosCount']
}
}
複製代碼
mapActions
mapActions 工具函數會將 store 中的 dispatch 方法映射到組件的 methods 中。和 mapState、mapGetters 也相似,只不過它映射的地方不是計算屬性,而是組件的 methods 對象上。咱們來直接看它的實現:
export function mapActions (actions) {
const res = {}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
return this.$store.dispatch.apply(this.$store, [val].concat(args))
}
})
return res
}
複製代碼
能夠看到,函數的實現套路和 mapState、mapGetters 差很少,甚至更簡單一些, 實際上就是作了一層函數包裝。爲了更直觀地理解,咱們來看一個簡單的例子:
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment' // 映射 this.increment() 到 this.$store.dispatch('increment')
]),
...mapActions({
add: 'increment' // 映射 this.add() to this.$store.dispatch('increment')
})
}
}
複製代碼
通過 mapActions 函數調用後的結果,以下所示:
import { mapActions } from 'vuex'
export default {
// ...
methods: {
increment(...args) {
return this.$store.dispatch.apply(this.$store, ['increment'].concat(args))
}
add(...args) {
return this.$store.dispatch.apply(this.$store, ['increment'].concat(args))
}
}
}
複製代碼
mapMutations
mapMutations 工具函數會將 store 中的 commit 方法映射到組件的 methods 中。和 mapActions 的功能幾乎同樣,咱們來直接看它的實現:
export function mapMutations (mutations) {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
res[key] = function mappedMutation (...args) {
return this.$store.commit.apply(this.$store, [val].concat(args))
}
})
return res
}
複製代碼
函數的實現幾乎也和 mapActions 同樣,惟一差異就是映射的是 store 的 commit 方法。爲了更直觀地理解,咱們來看一個簡單的例子:
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment' // 映射 this.increment() 到 this.$store.commit('increment')
]),
...mapMutations({
add: 'increment' // 映射 this.add() 到 this.$store.commit('increment')
})
}
}
複製代碼
通過 mapMutations 函數調用後的結果,以下所示:
import { mapActions } from 'vuex'
export default {
// ...
methods: {
increment(...args) {
return this.$store.commit.apply(this.$store, ['increment'].concat(args))
}
add(...args) {
return this.$store.commit.apply(this.$store, ['increment'].concat(args))
}
}
}
複製代碼
Vuex 的 store 接收 plugins 選項,一個 Vuex 的插件就是一個簡單的方法,接收 store 做爲惟一參數。插件做用一般是用來監聽每次 mutation 的變化,來作一些事情。
在 store 的構造函數的最後,咱們經過以下代碼調用插件:
import devtoolPlugin from './plugins/devtool'
// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
複製代碼
咱們一般實例化 store 的時候,還會調用 logger 插件,代碼以下:
import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
...
plugins: debug ? [createLogger()] : []
})
複製代碼
在上述 2 個例子中,咱們分別調用了 devtoolPlugin 和 createLogger() 2 個插件,它們是 Vuex 內置插件,咱們接下來分別看一下他們的實現。
devtoolPlugin
devtoolPlugin 主要功能是利用 Vue 的開發者工具和 Vuex 作配合,經過開發者工具的面板展現 Vuex 的狀態。它的源碼在 src/plugins/devtool.js 中,來看一下這個插件到底作了哪些事情。
const devtoolHook =
typeof window !== 'undefined' &&
window.__VUE_DEVTOOLS_GLOBAL_HOOK__
export default function devtoolPlugin (store) {
if (!devtoolHook) return
store._devtoolHook = devtoolHook
devtoolHook.emit('vuex:init', store)
devtoolHook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
})
}
複製代碼
咱們直接從對外暴露的 devtoolPlugin 函數看起,函數首先判斷了devtoolHook 的值,若是咱們瀏覽器裝了 Vue 開發者工具,那麼在 window 上就會有一個 VUE_DEVTOOLS_GLOBAL_HOOK 的引用, 那麼這個 devtoolHook 就指向這個引用。
接下來經過 devtoolHook.emit('vuex:init', store) 派發一個 Vuex 初始化的事件,這樣開發者工具就能拿到當前這個 store 實例。
接下來經過 devtoolHook.on('vuex:travel-to-state', targetState => { store.replaceState(targetState) })監聽 Vuex 的 traval-to-state 的事件,把當前的狀態樹替換成目標狀態樹,這個功能也是利用 Vue 開發者工具替換 Vuex 的狀態。
最後經過 store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) }) 方法訂閱 store 的 state 的變化,當 store 的 mutation 提交了 state 的變化, 會觸發回調函數——經過 devtoolHook 派發一個 Vuex mutation 的事件,mutation 和 rootState 做爲參數,這樣開發者工具就能夠觀測到 Vuex state 的實時變化,在面板上展現最新的狀態樹。
loggerPlugin
一般在開發環境中,咱們但願實時把 mutation 的動做以及 store 的 state 的變化實時輸出,那麼咱們能夠用 loggerPlugin 幫咱們作這個事情。它的源碼在 src/plugins/logger.js 中,來看一下這個插件到底作了哪些事情。
// Credits: borrowed code from fcomb/redux-logger
import { deepCopy } from '../util'
export default function createLogger ({
collapsed = true,
transformer = state => state,
mutationTransformer = mut => mut
} = {}) {
return store => {
let prevState = deepCopy(store.state)
store.subscribe((mutation, state) => {
if (typeof console === 'undefined') {
return
}
const nextState = deepCopy(state)
const time = new Date()
const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
const formattedMutation = mutationTransformer(mutation)
const message = `mutation ${mutation.type}${formattedTime}`
const startMessage = collapsed
? console.groupCollapsed
: console.group
// render
try {
startMessage.call(console, message)
} catch (e) {
console.log(message)
}
console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))
try {
console.groupEnd()
} catch (e) {
console.log('—— log end ——')
}
prevState = nextState
})
}
}
function repeat (str, times) {
return (new Array(times + 1)).join(str)
}
function pad (num, maxLength) {
return repeat('0', maxLength - num.toString().length) + num
}
複製代碼
插件對外暴露的是 createLogger 方法,它實際上接受 3 個參數,它們都有默認值,一般咱們用默認值就能夠。createLogger 的返回的是一個函數,當我執行 logger 插件的時候,實際上執行的是這個函數,下面來看一下這個函數作了哪些事情。
函數首先執行了 let prevState = deepCopy(store.state) 深拷貝當前 store 的 rootState。這裏爲何要深拷貝,由於若是是單純的引用,那麼 store.state 的任何變化都會影響這個引用,這樣就沒法記錄上一個狀態了。咱們來了解一下 deepCopy 的實現,在 src/util.js 裏定義:
function find (list, f) {
return list.filter(f)[0]
}
export function deepCopy (obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}
// if obj is hit, it is in circular structure
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive deepCopy
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
複製代碼
deepCopy 並不陌生,不少開源庫如 loadash、jQuery 都有相似的實現,原理也不難理解,主要是構造一個新的對象,遍歷原對象或者數組,遞歸調用 deepCopy。不過這裏的實現有一個有意思的地方,在每次執行 deepCopy 的時候,會用 cache 數組緩存當前嵌套的對象,以及執行 deepCopy 返回的 copy。若是在 deepCopy 的過程當中經過 find(cache, c => c.original === obj) 發現有循環引用的時候,直接返回 cache 中對應的 copy,這樣就避免了無限循環的狀況。
回到 loggerPlugin 函數,經過 deepCopy 拷貝了當前 state 的副本並用 prevState 變量保存,接下來調用 store.subscribe 方法訂閱 store 的 state 的變。 在回調函數中,也是先經過 deepCopy 方法拿到當前的 state 的副本,並用 nextState 變量保存。接下來獲取當前格式化時間已經格式化的 mutation 變化的字符串,而後利用 console.group 以及 console.log 分組輸出 prevState、mutation以及 nextState,這裏能夠經過咱們 createLogger 的參數 collapsed、transformer 以及 mutationTransformer 來控制咱們最終 log 的顯示效果。在函數的最後,咱們把 nextState 賦值給 prevState,便於下一次 mutation。
Vuex 2.0 的源碼分析到這就告一段落了,最後我再分享一下看源碼的當心得:對於一個庫或者框架源碼的研究前,首先了解他們的使用場景、官網文檔等;而後必定要用他,至少也要寫幾個小 demo,達到熟練掌握的程度;最後再從入口、API、使用方法等等多個維度去了解他內部的實現細節。若是這個庫過於龐大,那就先按模塊和功能拆分,一點點地消化。
最後還有一個問題,有些同窗會問,源碼那麼枯燥,咱們分析學習它的有什麼好處呢?首先,學習源碼有助於咱們更深刻掌握和應用這個庫或者框架;其次,咱們還能夠學習到源碼中不少編程技巧,能夠遷移到咱們平時的開發工做中;最後,對於一些高級開發工程師而言,咱們能夠學習到它的設計思想,對未來有一天咱們也去設計一個庫或者框架是很是有幫助的,這也是提高自身能力水平的很是好的途徑。