vuex 源码分析_Vuex 2.0 源码分析(下)
大家好,我叫黃軼,來自滴滴公共前端團隊,最近在幕課網上線了一門 Vue.js 的實戰課程——《Vue.js高仿餓了么外賣App 2016最火前端框架》,同時,我們團隊最近寫了一本書 ——《Vue.js 權威指南》,內容豐富,由淺入深。不過有一些同學反饋說缺少 Vuex 的介紹的章節。既然 Vue.js 2.0 已經正式發布了,我們也要緊跟步伐,和大家聊一聊 Vuex 2.0。本文并不打算講官網已有的內容,而會通過源碼分析的方式,讓同學們從另外一個角度認識和理解 Vuex 2.0。
輔助函數
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、使用方法等等多個維度去了解他內部的實現細節。如果這個庫過于龐大,那就先按模塊和功能拆分,一點點地消化。
最后還有一個問題,有些同學會問,源碼那么枯燥,我們分析學習它的有什么好處呢?首先,學習源碼有助于我們更深入掌握和應用這個庫或者框架;其次,我們還可以學習到源碼中很多編程技巧,可以遷移到我們平時的開發工作中;最后,對于一些高級開發工程師而言,我們可以學習到它的設計思想,對將來有一天我們也去設計一個庫或者框架是非常有幫助的,這也是提升自身能力水平的非常好的途徑。
總結
以上是生活随笔為你收集整理的vuex 源码分析_Vuex 2.0 源码分析(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 藏蓝色的含义!(成熟而优雅,深沉而高贵-
- 下一篇: 家用空调价格是多少