Vuex 2.0 源碼分析

Vuex 2.0 源碼分析

在一般情況之下, 我們普遍使用 global event bus 來解決全局狀態共享, 組件通訊的問題, 當遇到大型應用的時候, 這種方式將使代碼變得難以維護, Vuex應運而生, 接下來我將從源碼的角度分析Vuex的整個實現過程.

目錄結構

image
整個Vuex的目錄結構還是非常清晰地, index.js 是整個項目的入口, helpers.js 提供Vuex的輔助方法>, mixin.js 是$store注入到vue實例的方法, util.js 是一些工具函數, store.js是store類的實現 等等, 接下來就從項目入口一步步分析整個源碼.

項目入口

首先我們可以從index.js看起:

 export default {
    Store,
    install,
    version: '__VERSION__',
    mapState,
    mapMutations,
    mapGetters,
    mapActions,
    createNamespacedHelpers
 }

可以看到, index.js就是導出了一個Vuex對象, 這裏可以看到Vuex暴露的api, Store就是一個Vuex提供的狀態存儲類, 通常就是使用 new Vuex.Store(...)的方式, 來創建一個Vuex的實例. 接下來看, install 方法, 在store.js中;

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)
   }

install 方法有個重複install的檢測報錯, 並將傳入的_Vue賦值給自己定義的Vue變量, 而這個Vue變量已經變導出, 整個項目就可以使用Vue, 而不用安裝Vue;

 export let Vue

接着調用applyMixin方法, 該方法在mixin.js當中;

export default function (Vue) {
    const version = Number(Vue.version.split('.')[0])
    Vue.mixin({ beforeCreate: vuexInit })
}

所以, applyMixin方法的邏輯就是全局混入一個beforeCreate鉤子函數-vuexInit;

function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
        this.$store = typeof options.store === 'function'
            ? options.store()
            : options.store
    } else if (options.parent && options.parent.$store) {
        this.$store = options.parent.$store
    }
}

整個代碼很簡單, 就是將用戶傳入的store注入到每個vue實例的$store屬性中去, 從而在每個實例我們都可以通過調用this.$store.xx訪問到Vuex的數據和狀態;

Store類

在我們使用Vuex的時候, 通常會實例化一個Vuex.Store類, 傳入一個對象, 對象包括state、getters、mutations、actions、modules, 而我們實例化的時候, Vuex到底做了什麼呢? 帶着這個疑問, 我們一起來看store.js中的代碼, 首先是構造函數;


constructor (options = {}) {

    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    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
    } = options
    
    // store internal state
    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()
    
    // 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
    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)
    }
}

構造函數一開始是判斷當window.Vue存在的時候, 調用install方法, 確保script加載的Vuex可以正確被安裝, 接着是三個斷言函數, 確保Vue存在, 環境支持Promise, 當前環境的this是Store;

const {
    plugins = [],
    strict = false
} = options

利用es6的賦值結構拿到options中的plugins(默認是[]), strict(默認是false), plugins 表示應用的插件、strict 表示是否開啓嚴格模式, 接着往下看;

// store internal state
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()

這裏主要是初始化一些Vuex內部的屬性, _開頭, 一般代表着私有屬性,
this._committing標誌着一個提交狀態;
this._actions存儲用戶的所有的actions;
this.mutations存儲用戶所有的mutations;
this.wrappedGetters存儲用戶所有的getters;
this._subscribers用來存儲所有對 mutation 變化的訂閱者;
this._modules表示所有modules的集合;
this._modulesNamespaceMap表示子模塊名稱記錄.
繼續往下看:

// 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
const state = this._modules.root.state

這段代碼就是通過賦值結構拿到store對象的dispatch, commit 方法, 並重新定義store的dispatch, commit 方法, 使他們的this指向store的實例, 具體的dispatch和comiit實現稍後分析.

Vuex核心

installModule方法

installModule方法主要是根據用戶傳入的options, 進行各個模塊的安裝和註冊, 具體實現如下:

function installModule (store, rootState, path, module, hot) {
    const isRoot = !path.length
    const namespace = store._modules.getNamespace(path)
    
    // register in namespace map
    if (module.namespaced) {
        if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
        store._modulesNamespaceMap[namespace] = module
    }
    
    // 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, module.state)
        })
    }
    
    const local = module.context = makeLocalContext(store, namespace, path)
    
    module.forEachMutation((mutation, key) => {
        const namespacedType = namespace + key
        registerMutation(store, namespacedType, mutation, local)
    })
    
    module.forEachAction((action, key) => {
        const type = action.root ? key : namespace + key
        const handler = action.handler || action
        registerAction(store, type, handler, local)
    })
    
    module.forEachGetter((getter, key) => {
        const namespacedType = namespace + key
        registerGetter(store, namespacedType, getter, local)
    })
    
    module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child, hot)
    })
}

installModules方法需要傳入5個參數, store, rootState, path, module, hot; store指的是當前Store實例, rootState是根實例的state, path當前子模塊的路徑數組, module指的是當前的安裝模塊, hot 當動態改變 modules 或者熱更新的時候爲 true。

先看這段代碼:

if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
    console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
}

這段代碼主要是爲了防止子模塊命名重複, 故定義了一個map記錄每個子模塊;

接下來看下面的代碼:

// 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, module.state)
    })
}

這裏判斷當不爲根且非熱更新的情況,然後設置級聯狀態,這裏乍一看不好理解,我們先放一放,稍後來回顧。

再往下看代碼:

const local = module.context = makeLocalContext(store, namespace, path)

首先, 定義一個local變量來接收makeLocalContext函數返回的結果, makeLocalContext有三個參數, store指的是根實例, namespace 指的是命名空間字符, path是路徑數組;

function makeLocalContext (store, namespace, path) {
    const noNamespace = namespace === ''
    const local = {
        dispatch: noNamespace ? store.dispatch : (_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._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
}

makeLocalContext 函數主要的功能就是根據是否有namespce定義不同的dispatch和commit, 並監聽local的getters和sate的get屬性, 那namespace是從何而來呢, 在installModule的開始:

const isRoot = !path.length
const namespace = store._modules.getNamespace(path)

namespace 是根據path數組通過_modules中的getNamespace獲得, 而store._modules是ModuleCollection的實例, 所以可以到ModuleCollection中找到getNamespace方法:

getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
        module = module.getChild(key)
        return namespace + (module.namespaced ? key + '/' : '')
    }, '')
}

該函數通過對path路徑數組reduce遍歷, 獲得模塊的命名空間(eg: 'city/');,接下來是各個模塊的註冊流程, 首先看mutaiton的註冊;

module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
})

forEachMutation函數一個循環遍歷, 拿到用戶傳入的mutation函數和key值, 接着調用registerMutation函數;

// $store.state.commit('add', 1)
function registerMutation (store, type, handler, local) {
    const entry = store._mutations[type] || (store._mutations[type] = [])
    entry.push(function wrappedMutationHandler (payload) {
        handler.call(store, local.state, payload)
    })
}

這段代碼的作用就是, 將所有的mutation函數封裝成wrappedMutationHandler存入store._mutations這個對象當中, 我們結合前面提過的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(() => {
        entry.forEach(function commitIterator (handler) {
            handler(payload)
        })
    })
    
    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 函數就是對參數的規範, 而後, 通過`
this._mutations[type] 拿到type所對應的所有wrappedMutationHandler函數, 遍歷執行, 傳入payload, this._withCommit`函數在源碼中出現過很多次, 代碼如下:

_withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
}

代碼作用就是每次提交的時候, 將this._committing置爲true, 執行完提交操作之後, 在重新置爲初始狀態, 確保只有mutation才能更改state的值, _subscribers相關代碼暫時不看, 我們接下來看一看action的註冊流程:

module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
})

這段代碼和mutation的註冊流程是類似的, 不同在於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)
        }
        
        if (store._devtoolHook) {
            return res.catch(err => {
                store._devtoolHook.emit('vuex:error', err)
                throw err
            })
        } else {
            return res
        }
    })
}

可以看到, 基於用戶的action函數, 源碼封多了一層wrappedActionHandler函數, 在action函數中, 可以獲得一個context對象, 就是在這裏做的處理, 然後, 它把action函數的執行結果封裝成了Promise並返回, 結合dispatch函數可以更好的理解;

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
    }
    
    const result = entry.length > 1
        ? Promise.all(entry.map(handler => handler(payload)))
        : entry[0](payload)
        
        return result.then(res => {
            return res
        })
}

dispatch 拿到actions後, 根據數組長度, 執行Promise.all或者直接執行, 然後通過then函數拿到promise resolve的結果.

接下來是getters的註冊

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
        )
    }
}

將用戶傳入的rawGetter封裝成wrappedGetter, 放入store._wrappedGetters的對象中, 函數的執行稍後再說, 我們繼續子模塊的安裝;

module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
})

這段代碼首先是對state.modules遍歷, 遞歸調用installModule, 這時候的path是不爲空數組的, 所以會走到這個邏輯;

// 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, module.state)
    })
}

通過getNestedState找到它的父state, 它的模塊key就是path的最後一項, store._withCommit上面已經解釋過了, 然後通過Vue.set 將子模塊響應式的添加到父state, 從而將子模塊都註冊完畢.

resetStoreVM 方法

resetStoreVM 函數第一部分

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
    })
})
    
    

首先, 拿到所有的wrappedGetter函數對象, 即包裝過的用戶傳入的getters, 定義一個變量computed, 接受所有的函數, 並通過Ojbect.defineProperty在store.getters屬性定義了get方法, 也就是說, 我們通過this.$store.getters.xx 會訪問到 store._vm[xx], 而store._vm又是什麼呢?

// 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     // 關閉vue警告, 提醒

store._vm = new Vue({
    data: {
        $$state: state
    },
    computed
})

Vue.config.silent = silent

顯然, store._vm是一個Vue的實例, 包含所有用戶getters的計算屬性和 用戶state的$$state屬性, 而我們訪問this.$store.state 其實就是訪問這裏的$$state屬性, 原因在於, Store類直接定義了一個state的取值函數, 其中返回的正是這個$$state屬性;

get state () {
    return this._vm._data.$$state
}

我們接着看;

// enable strict mode for new vm
if (store.strict) {
    enableStrictMode(store)
}

當在Vuex嚴格模式下, strict爲true, 所以會執行enableStrictMode函數;

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.$watch函數, 監聽$$state的變化, 當store._committing 爲false的話, 就會拋出不允許在mutation函數之外操作state;

接着我們再來看最後一部分;

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())
}

oldVm保存着上一個store._vm對象的引用, 每次執行這個函數, 都會創建一個新的store._vm, 所以需要在這段代碼中銷燬;

至此, Store類初始化大致都講完了, 接下來分析Vuex提供的輔助函數.

輔助函數

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方法, 該方法主要是用於格式化參數, 用戶使用mapState函數, 可以使傳入一個字符串數組, 也可以是傳入一個對象, 經過normalizeMap方法處理, 統一返回一個對象數組;;

// normalizeMap([1,2]) => [{key: 1, val: 1}, {key: 2, val: 2}] 
// normalizeMap({a: 1, b: 2}) => [{key: 'a', val: 1}, {key: 'b', val: 2}] 
function normalizeMap (map) {
    return Array.isArray(map)
        ? map.map(key => ({ key, val: key }))
        : Object.keys(map).map(key => ({ key, val: map[key] }))
}

接着, 對於處理過的對象數組遍歷, 定義了一個res對象接收, key爲鍵, mappedState方法爲值;

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]
}

整個函數代碼比較簡單, 唯一需要注意的點是, 當傳入了namespace時, 需要通過getModuleByNamespace函數找到該屬性對應的module, 還記得在installModule中, 有在store._modulesNamespaceMap中記錄namespace和模塊間的對應關係, 因此, getModuleByNamespace就是通過這個map找到了module, 從而拿到了當前module的state和getters;

最後mapstate函數返回一個res函數對象, 用戶可以直接利用...操作符導入到計算屬性中.

mapMutations

mapMutations函數和mapstate函數是類似的, 唯一的區別在於mappedMutation是commit 函數代理, 並且它需要被導入到methods;

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))
}

mapActions, mapGetters 的實現也都大同小異, 便不再具體分析.

plugins選項

我們可以通過類似這種方式使用plugins:

const myPlugin = store => {
    // 當 store 初始化後調用
    store.subscribe((mutation, state) => {
        // 每次 mutation 之後調用
        // mutation 的格式爲 { type, payload }
     }
 )}

const store = new Vuex.Store({
     // ...
     plugins: [myPlugin]
 })

在源碼當中, 可以看到這麼一段代碼:

// apply plugins
plugins.forEach(plugin => plugin(this))

即遍歷所有plugins, 傳入當前Store實例, 執行plugin函數, 因此, 示例的store參數就是Store實例, 然後示例調用store.subscribe方法, 這是Store類暴露的一個成員方法;

subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
}

其實這就是一個訂閱函數, 當有commit操作的時候, 就會通知所有訂閱者, 該函數返回一個函數fn, 調用這個fn即可以取消訂閱, 發佈通知代碼在commit函數中:

this._subscribers.forEach(sub => sub(mutation, this.state))

結語

當學無所學之時, 看優秀源碼或許是一種突破瓶頸的方法, 可以更加深入的瞭解這個庫, 知其然, 亦知其所以然, 同時作者的一些庫的設計思想, 也會對我們大有裨益.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章