Vuex-20-源码分析

12次阅读

共计 16378 个字符,预计需要花费 41 分钟才能阅读完成。

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

结语

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

正文完
 0