乐趣区

Vuex-20-源码分析

Vuex 2.0 源码分析

在一般情况之下, 我们普遍使用 global event bus 来解决全局状态共享, 组件通讯的问题, 当遇到大型应用的时候, 这种方式将使代码变得难以维护, Vuex 应运而生, 接下来我将从源码的角度分析 Vuex 的整个实现过程.

目录结构


整个 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))

结语

当学无所学之时, 看优秀源码或许是一种突破瓶颈的方法, 可以更加深入的了解这个库, 知其然, 亦知其所以然, 同时作者的一些库的设计思想, 也会对我们大有裨益.

退出移动版