Vuex源码解析

327 阅读4分钟

源码地址:gitee.com/QQ181530446…

下面代码展示不全,如有需要可自行下载源码或加入QQ群(631837530)。

基本使用

Vuex提供了5个核心概念:

  • module:用于开启模块化,将单一仓库拆分为多个子模块。
  • state:用于存储状态。
  • getters:用于在 state 中派生状态。
  • mutations:用于改变 state,是改变对应仓库状态的唯一方法。
  • actions:用于处理副作用,这里可以提交 mutation 去改变仓库状态。

示例:

主模块代码如下:

import { createStore } from 'vuex';

// 导入不同的模块
import userModule from './modules/userModule';
import counterModule from './modules/counterModule';

const store = createStore({
    modules: { // 开启模块化
        userInfo: userModule,
        count: counterModule
    }
});

export default store;

counterodule模块代码如下:

export default {
    // 若开启模块化尽量开启命名空间防止多个模块的方法重名导致相互覆盖的问题
    namespaced: true, 
    
    state: { // 书写状态,必须事先定义状态,在低版本的vuex无法后续新增状态。
        count: 0
    },

    mutations: {
        // state 是本仓库的 state 对象,payload 是外界调用 increase 方法时传递过来的。
        increase(state, payload) {
            state.count++;
        }
    },

    getters: {
        getDoubleCount: (state) => { // 获取双倍的 count。
            return state.count * 2;
        }
    },

    actions: {
        asyncIncrease({commit}, payload) {
            // 这里可以发送 ajax 请求,或其他的异步操作。
            
            commit('increase', payload); // 提交 mutation 改变仓库状态。
        }
    }
}

源码解析

我们看完上述的例子之后发现 vuex 中是通过 createStore 方法创建仓库的,那让我们去看一看 createStore 在做什么事情揭开 vuex 的神秘面纱。

1.createStore 函数内部运行 Store 构造函数去创建仓库。

export function createStore(options){
    return new Store(options);
}

2.Store 构造函数运行时会有几个重要的步骤。

  • 初始化:设置一些私有属性存储每个模块的配置项,例:每个模块的 mutations 会被放入 this._mutations 中。
  • intall方法:该方法是暴露给 vue 使用的,向 vue 全局对象上注册 $store 属性。
  • installModule方法:该方法是将每个模块的配置项放入初始化时创建的私有属性中。
  • resetStoreState方法:经过 installModule 方法后模块所有的配置项都添加到 Store 构造函数的私有属性上,resetStoreState方法就是将所有模块的 getterscomputed 方法包裹转为计算属性并且使用 reactive 函数将所有模块的 state 转为响应式数据。
class Store {
    constructor(options) {
        this._actions = Object.create(null);
        this._mutations = Object.create(null);
        this._wrappedGetters = Object.create(null);
        this._modules = new ModuleCollection(options);
        this._modulesNamespaceMap = Object.create(null);
        this._makeLocalGettersCache = Object.create(null);

        this._scope = null;
        // 绑定commit和dispatch到self
        const store = this;
        const ref = this;
        const dispatch = ref.dispatch;
        const commit = ref.commit;

        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)
        };
        const state = this._modules.root.state;

        this.installModule(this, state, [], this._modules.root);
        this.resetStoreState(this, state);
    }

    install(app, options) {
        app.provide(storeKey, this);
        app.config.globalProperties.$store = this;
    }

    get state() {
        return this._state.data;
    }

    // 向Store实例上添加所有模块的信息,例如:mutations里面的东西被放入Store实例的_mutations对象下
    installModule(store, rootState, path, module, hot) {
        const this$$1 = this;
        const isRoot = !path.length;
        const namespace = store._modules.getNamespace(path);// 拿到命名空间,这里会将命名空间组合起来并且会加上/

        if (module.namespaced) {
            if (store._modulesNamespaceMap[namespace] && true) {
                console.error(("[vuex] 重复的命名空间 " + namespace + " 用于命名空间模块 " + (path.join('/'))));
            }
            store._modulesNamespaceMap[namespace] = module;
        }

        if (!isRoot && !hot) {
            // 拿到父级模块的状态,后面会直接往里面添加属性,在一开始运行时是一个空对象
            // path.slice(0, -1) 拿到path数组倒数第一个元素
            const parentState = getNestedState(rootState, path.slice(0, -1));

            const moduleName = path[path.length - 1];
            if (moduleName in parentState) {
                console.warn(`[vuex] 状态失败 ${moduleName} 被同名变量覆盖 ${path.join('.')}`);
            }
            parentState[moduleName] = module.state;
        }

        // makeLocalContext 方法里面提供给了 dispatch、commi t方法和 getters、state属性。
        // makeLocalContext 方法提供的 dispatch、commit 就是 Store 上对应实例方法。
        // makeLocalContext 方法提供的 getters、state 是在 Store 的私有属性上找到对应该仓库的 getters、state。
        const local = module.context = makeLocalContext(store, namespace, path);

        const mutations = module._rawModule.mutations;
        const actions = module._rawModule.actions;
        const getters = module._rawModule.getters;
        const children = module._children;

        if (mutations) {
            const keys = Object.keys(mutations);

            keys.forEach((key) => {
                const namespacedType = namespace + key;
                
                const action = mutations[key];

                const entry = store._mutations[namespacedType] || (store._mutations[namespacedType] = []);
                entry.push(function wrappedMutationHandler(payload) {
                    action.call(store, local.state, payload);
                });
            })
        }

        if (actions) {
            const keys = Object.keys(actions);

            keys.forEach((key) => {
                const namespacedType = namespace + key;
                const mutation = actions[key];

                const entry = store._actions[namespacedType] || (store._actions[namespacedType] = []);
                entry.push(function wrappedMutationHandler(payload) {
                    const options = {
                        dispatch: local.dispatch,
                        commit: local.commit,
                        getters: local.getters,
                        state: local.state,
                        rootGetters: store.getters,
                        rootState: store.state
                    };
                    const result = mutation.call(store, options, payload);
                    return Promise.resolve(result);
                });
            })
        }

        if (getters) {
            const keys = Object.keys(getters);

            keys.forEach((key) => {
                const namespacedType = namespace + key;
                const getter = getters[key];

                if (store._wrappedGetters[namespacedType]) {
                    console.error(("[vuex] 重复的 getter 名称: " + namespacedType));
                    return;
                }

                store._wrappedGetters[namespacedType] = function wrappedGetter(state) {
                    return getter(local.state, local.getters, store.state, store.getters);
                };
            })
        }

        if (children) {
            const keys = Object.keys(children);
            keys.forEach((key) => {
                const childrenModule = children[key]
                this$$1.installModule(store, rootState, path.concat(key), childrenModule, hot);
            })
        }
    }

    // 将Store实例的_wrappedGetters存放的getter函数用computed包裹,并且将 Store 实例的_state通过reactive转为响应式
    resetStoreState(store, state, hot) {
        const oldState = store._state;
        const oldScope = store._scope;
        store.getters = {};

        // 下面的东西都是为了getter函数被computed包裹后不会立即执行computed函数,而是等用到时执行
        store._makeLocalGettersCache = Object.create(null);
        const wrappedGetters = store._wrappedGetters;
        const computedObj = {};
        const computedCache = {};

        const scope = effectScope(true); // 使用 effectScope 是为了更好的管理 computed 方法。

        scope.run(() => {
            const keys = Object.keys(wrappedGetters);
            keys.forEach((key) => {
                // 使用 computed 方法转为响应式对象。
                computedObj[key] = () => wrappedGetters[key](this.state);
                computedCache[key] = computed(() => computedObj[key]());

                Object.defineProperty(store.getters, key, {
                    get: () => computedCache[key].value,
                    enumerable: true
                })
            })
        })

        store._state = reactive({data: state}); // 转为响应式对象。

        store._scope = scope;

        if (oldScope) {
            oldScope.stop();
        }
    }

    // commit执行只需要在Store实例找到对应的mutation函数运行即可,数据变化导致组件渲染是vue的事情和vuex无关。
    commit(_type,_payload,_options){
        // unifyObjectStyle 方法目的是进行归一化,有的时候_type传递进来的是对象 ({type : xxx,payload: xxx}) _payload 没有值后续需要将 _payload 传入函数中运行所以需要拆解。
        const {type, payload, options} = unifyObjectStyle(_type, _payload, _options);
        const entry = this._mutations[type];

        if (!entry) {
            console.error(`[vuex] 未知的 mutation 类型: ${type}`);
            return
        }

        this._withCommit(function () { // 这个方法就是立即执行回调函数不会做额外的事情。
            entry.forEach(function (mutation) {
                mutation(payload);
            });
        });
    }

    // 实际上 commit 和 dispatch 在源码实现中差不多都是找到对应的函数去执行,只是 dispatch 函数使用 Promsie 进行了包裹。
    dispatch(_type,_payload){
        const {type, payload} = unifyObjectStyle(_type, _payload);
        const entry = this._actions[type];

        if (!entry) {
            console.error(`[vuex] 未知的 action 类型: ${type}`);
            return
        }

        // 找到对应的 actions 函数执行,对应执行后数据如何变化如何依赖收集是 vue 要做的事情。
        const result = entry.length > 1
            ? Promise.all(entry.map((handler) => handler(payload)))
            : Promise.resolve(entry[0](payload));

        return new Promise((resolve, reject) => result.then(resolve, reject));
    }

    _withCommit(fn) { // 这个函数可以理解为传入回调函数后立即执行。
        const committing = this._committing;
        this._committing = true;
        fn();
        this._committing = committing;
    }
}

在初始化时会执行 new ModuleCollection(xxx) 操作,这个构造函数作用是转换格式的。

示例:

createStore({
    modules: {
        userInfo: userModule,
        count: counterModule
    }
});

将 createStore 传入的对象转换为
{
    root: {
        namespaced: false, // 是否开启命名空间
        state,
        _children: [], // 存储子模块
        __rawModule: xxx, // createStore函数传入的对象
        context: {dispatch,commit,getters,state}
    }
}

总结

  • 对于模块化是将所有的模块合并成一个大模块,因此 vuex 实际上就是维护一个普通对象,调用 vue 的方法实现响应式。
  • 由于合并的时候模块之间的函数可能存在重名造成相互覆盖的问题这也就是为什么需要开启命名空间的原因。
  • 对于数据变化导致组件更新完全是 vue 去做的事情和 vuex 没有一点关系。