vuex源码解析

这篇博客深入解析了Vuex的源码,重点关注了Store类的实现、ModuleCollect类以及辅助函数mapState、mapMutations、mapActions和mapGetters。文中指出在Vuex中,模块内的state和getters具有更高优先级,当与全局命名冲突时会覆盖。同时强调getters必须避免重名,而mutations和actions则允许重名。最后提到了Vuex插件的实现原理,采用发布订阅模式来响应mutations。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

vuex源码解析

首先我们先观察一下官方store实例的结构,然后再考虑按照它的结构实现一下

在这里插入图片描述

主要实现一个Store类和ModuleCollect类、以及helper辅助函数

Store类

// 1、插件:挂载$store
// 2、实现Store
// state的name与module名重复,state同名状态会被覆盖掉
import ModuleCollection from './ModuleCollection'
let Vue;
class Store {
    constructor(options) {
        // 所有的actions,包括子模块的actions
        // {string:Array<function>}
        this._mutations = {}
        // 所有的actions,包括子模块的actions
        // {string:Array<function>}
        this._actions = {}
        // 收集getters的初始容器,还没有实现响应式
        // {string:function}
        this._wrappedGetters = {}
        // 命名空间与module的映射
        // {string:module类}
        this._modulesNamespaceMap = {}
        // actions的插件订阅者
        this._actionSubscribers = {}
        // mutations的插件订阅者
        this._subscribes = []
        // getters
        this.getters = {} // 需要借助vue的计算属性
        // 把数据格式化成一个想要的树结构
        this._modules = new ModuleCollection(options)
        const state = this._modules.root.state
        // 递归将结果进行分类
        // this 整个store
        // this.state 当前的根状态
        // [] 为了递归来创建
        // this.module.root 从根模块进行安装
        installModule(this, state, [], this._modules.root)
        // 将state变为响应式,并且把getters变为计算属性
        resetStoreVm(this, state)
        // 处理插件
        options.plugins.forEach(plugin => plugin(this))
    }

    // 插件订阅mutations
    subscribe(fn) {
        this._subscribes.push(fn)
    }

    // 插件订阅actions
    subscribeActions(fn) {
        this._actionSubscribers.push(fn)
    }

    // 提交更改 会在当前的store上找到对应的函数执行
    // 采用箭头函数就不需要绑定this了
    commit = (mutationName, payload) => {
        this._mutations[mutationName].forEach(fn => fn(payload))
    }
    // dispatch对应的action在当前的store上找到对应的函数执行
    // 采用箭头函数就不需要绑定this了
    dispatch = (actionName, payload) => {
        this._actions[actionName].forEach(fn => fn(payload))
    }

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

    set state(val) {
        // 禁止用户直接改变state
        throw new Error('不允许改变,请使用replaceState')
    }

    replaceState(state) {
        this._vm._data.$$state = state
    }
}

const install = (_Vue) => {
    Vue = _Vue
    Vue.mixin({
        beforeCreate() {
            if (this.$options.store) {
                Vue.prototype.$store = this.$options.store
            }
        },
    })
}

// 迭代对象,会将对象的key与value拿到
function forEach(obj, cb) {
    Object.keys(obj).forEach(key => {
        cb(key, obj[key])
    })
}
// 判断是否是对象
function isObject(obj) {
    return obj !== null && typeof obj == 'object'
}
// 统一参数
function unifyObjectStyle(type, payload, options) {
    if (isObject(type) && type.type) {
        options = payload
        payload = type
        type = type.type
    }
    return {
        type,
        payload,
        options
    }
}
// 获取嵌套的子模块的state
function getNestedState(store, path) {
    return path.reduce((root, current) => {
        return root[current]
    }, store.state)
}
// 创造模块自己的getters
function makeLocalGetters(store, namespace) {
    let splitPos = namespace.length
    const gettersProxy = {}
    Object.keys(store.getters).forEach(type => {
        if (type.slice(0, splitPos) !== namespace) return
        const localType = type.slice(splitPos)
        Object.defineProperty(gettersProxy, localType, {
            get: () => store.getters[type],
            enumerable: true
        })
    })
    return gettersProxy
}

// 初始化store的vm,利用vue实现数据的响应式
// 同时利用vue的计算属性,实现getters的计算属性
function resetStoreVm(store, state) {
    let computed = {}
    forEach(store._wrappedGetters, (k, fn) => {
        computed[k] = fn
        // 懒加载,只有获取的时候才会调用get的到getters的值
        Object.defineProperty(store.getters, k, {
            get: () => store._vm[k],
            enumerable: true
        })
    })
    store._vm = new Vue({
        data() {
            return {
                // 利用class的get state,从而实现能通过this.state直接访问
                // 同时,利用set state,禁止用户改变state的值
                // 使用store自带的replaceState来改变值
                $$state: state
            }
        },
        computed
    })
}

// 创造子模块的context上下文,主要包括commit、dispatch、state、getters
// 当有命名空间时,commit、dispatch会被默认加上当前的命名空间
// state、getters为子模块内部的state、getters
const makeLocalContext = (store, namespace, path) => {
    const noNamespace = namespace === ''
    const local = {
        // 如果没有命名空间那么子模块的commit直接使用store上的即可
        commit: noNamespace ? store.commit : (_type, _payload, _options) => {
            const args = unifyObjectStyle(_type, _payload, _options)
            const { payload, options } = args
            let { type } = args
            // 如果root为true,表示用户想调用全局的mutations,所以只有当root为false才会添加命名空间
            if (!options || options.root) {
                type = namespace + type
            }
            return store.commit(type, payload, options)
        },
        // 如果没有命名空间那么子模块的dispatch直接使用store上的即可
        dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
            const args = unifyObjectStyle(_type, _payload, _options)
            const { payload, options } = args
            let { type } = args
            // 如果root为true,表示用户想调用全局的mutations,所以只有当root为false才会添加命名空间
            if (!options || options.root) {
                type = namespace + type
            }
            return store.dispatch(type, payload)
        },
    }
    Object.defineProperties(local, {
        getters: {
            get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace),
            enumerable: true
        },
        state: {
            get: noNamespace ? () => store.state : () => getNestedState(store, path),
            enumerable: true
        },
    })
    return local
}

// 安装模块,并且递归安装所有的子模块
function installModule(store, rootState, path, rootModule) {
    // 处理子模块的state
    if (path.length > 0) {
        // 子模块
        let parent = path.slice(0, -1).reduce((root, current) => {
            return root[current]
        }, rootState)
        // vue 不能再对象上增加上不存在的属性 否则不会导致视图的更新
        console.log(parent, rootState, 'parent');
        Vue.set(parent, path[path.length - 1], rootModule.state || {})
    }

    // 处理命名空间
    let module = store._modules.root
    let namespace = path.reduce((space, current) => {
        module = module._children[current]
        return space + (module.namespaced ? current + '/' : '')
    }, '')

    // 缓存命名空间对应模块方便后面直接通过命名空间寻找模块
    // 后面helper函数会用到这个缓存
    store._modulesNamespaceMap[namespace] = rootModule

    // 处理context,并绑定到模块上面
    let local = makeLocalContext(store, namespace, path)
    rootModule.context = local

    // 以下代码都是在处理 模块中的getters actions mutations
    // 让子模块中的 getters actions mutations 挂载到外层
    let getters = rootModule._rawModule.getters
    if (getters) {
        forEach(getters, (type, fn) => {
            store._wrappedGetters[namespace + type] = function () {
                return fn(local.state, local.getters, store.state, store.getters)
            }
        })
    }
    let mutations = rootModule._rawModule.mutations
    if (mutations) {
        forEach(mutations, (mutationsName, fn) => {
            let mutations = store._mutations[namespace + mutationsName] || []
            mutations.push((payload) => {
                fn(local.state, payload)
                // 
                store._subscribes.forEach(fn => fn({ type: mutationsName, payload }, rootState))
            })
            // 发布 让所有的订阅mutations的插件callback依次执行
            store._mutations[namespace + mutationsName] = mutations
        })
    }
    let actions = rootModule._rawModule.actions
    if (actions) {
        forEach(actions, (actionsName, fn) => {
            let actions = store._actions[namespace + actionsName] || []
            actions.push((payload) => {
                fn({
                    ...local,
                    rootState: store.state,
                    rootGetters: store.getters
                }, payload)
                // 发布 让所有的订阅mutations的插件callback依次执行
                store._mutations[namespace + actionsName] = mutations
            })
            store._actions[namespace + actionsName] = actions
        })
    }
    // 递归安装子模块
    forEach(rootModule._children, (moduleName, module) => {
        installModule(store, rootState, path.concat(moduleName), module)
    })
}

export default { install, Store }

ModuleCollect类

export default class ModuleCollection {
    constructor(options) {
        // 注册模块,将模块注册成树结构
        this.register([], options)
    }
    register(path, rootModule) {
        const isRoot = path.length === 0
        // 将模块格式化
        let module = {
            _rawModule: rootModule,
            _children: {},
            state: rootModule.state,
            namespaced: rootModule.namespaced
        }
        if (isRoot) {
            // 如果是根模块,将这个进行实例挂载到根节点上
            this.root = module
        } else {
            let parent = path.slice(0, -1).reduce((root, current) => {
                return root._children[current]
            }, this.root)
            parent._children[path[path.length - 1]] = module
        }
        // 看当前模块是否有modules,开始重新注册
        if (rootModule.modules) {
            Object.keys(rootModule.modules).forEach(moduleName => {
                const childModule = rootModule.modules[moduleName]
                this.register(path.concat(moduleName), childModule)
            })
        }
    }
}

helper辅助函数(mapState、mapMutations、mapActions、mapGetters)

function isObject(obj) {
    return obj !== null && typeof obj === 'object'
}

// 验证是否是数组或者对象
function isValidMap(map) {
    return Array.isArray(map) || isObject(map)
}

// 统一map参数对象为{key,val}形式
function normalizeMap(map) {
    if (!isValidMap(map)) {
        throw new Error('不是有效的参数')
    }
    return Array.isArray(map)
        ? map.map(key => ({ key, val: key }))
        : Object.keys(map).map(key => ({ key, val: map[key] }))
}

// 统一命名空间
function normalizeNamespace(fn) {
    return (namespace, map) => {
        if (typeof namespace !== 'string') {
            map = namespace
            namespace = ''
        } else if (namespace.charAt(namespace.length - 1) !== '/') {
            namespace += '/'
        }
        return fn(namespace, map)
    }
}

// 通过命名空间
function getModuleByNamespace(store, helper, namespace) {
    const module = store._modulesNamespaceMap[namespace]
    if (!module) {
        throw new Error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
    }
    return module
}

// 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) {
                // 如果有命名空间,则把state与getters替换对应子模块的state和getters
                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]
        }
    })
    return res
})

// mapGetters函数
export const mapGetters = normalizeNamespace((namespace,map) => {
    const res = {}
    normalizeMap(map).forEach(({key,val}) => {
        // 拼接上命名空间前缀
        val = namespace + val
        res[key] = function mappedGetters() {
            return this.$store.getters[val]
        }
    })
    return res
})

// mapMutations函数
export const mapMutations = normalizeNamespace((namespace,map) => {
    const res = {}
    normalizeMap(map).forEach(({key,val}) => {
        res[key] = function mappedMutations(...args) {
            let commit = this.$store.commit
            if(namespace) {
                // 如果有命名空间,则把commit换成子模块的commit
                // commit内部,添加了命名空间前缀,所以此处不需要再添加命名空间
                let module = getModuleByNamespace(this.$store,'mapMutations',namespace)
                if(!module) return
                commit = module.context.commit
            }
            // 需要判断用户传过来的是函数,还是字符串类型
            // 需要注意this的指向问题
            typeof val === 'function' 
            ? val.apply(this,[commit].concat(args))
            : commit.apply(this.$store,[val].concat(args))
        }
    })
    return res
})

// mapActions函数
export const mapActions = normalizeNamespace((namespace,map) => {
    const res = {}
    normalizeMap(map).forEach(({key,val}) => {
        res[key] = function mappedMutations(...args) {
            let dispatch = this.$store.dispatch
            if(namespace) {
                // 如果有命名空间,则把commit换成子模块的commit
                // dispatch内部,添加了命名空间前缀,所以此处不需要再添加命名空间
                let module = getModuleByNamespace(this.$store,'mapMutations',namespace)
                if(!module) return
                dispatch = module.context.dispatch
            }
            // 需要判断用户传过来的是函数,还是字符串类型
            // 需要注意this的指向问题
            typeof val === 'function'
            ? val.apply(this,[dispatch].concat(args))
            : dispatch.apply(this.$store,[val].concat(args))
            
        }
    })
    return res
})

// 创建命名空间辅助函数
export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})

注意事项

  • vuex里面mutationsactions可以重名,当重名时,按照注册先后顺序执行
  • 当state对象名与模块名重复时,模块有更高的优先级,会覆盖掉state里面的同名属性
  • getters不能重名,重名会报错。
  • store里面的mutations与actions结构为{string:[]},所以能过重名
  • store里面的getters结构为{string:getter}所以不能重名
  • 添加命名空间后,getters、mutations、actions均会被添加模块名前缀,并且会拼接上父级的前缀
  • 插件(实际就是一个高阶函数)的实现采用的是发布者订阅模式,当mutations触发时,发布通知,让所有的订阅依次执行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值