简易vuex4核心原理及实现源码分析

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

简易vuex4核心原理及实现源码分析

驺虞   2023-02-03 我要评论

前言

Vuex 是一个专为 Vue.js 应用程序开发的 状态管理模式 。它借鉴了Flux、redux的基本思想,将共享的数据抽离到全局,同时利用Vue.js的 响应式 机制来进行高效的状态管理与更新。想要掌握了解基础知识可以查阅Vuex官网,本篇主要是对 vuex4.x版本的源码 进行研究分析。

Vuex 核心原理

使用方式

创建 store

import { createStore } from "@/vuex";
const store = createStore({
  state: {
      count: 0,
  },
  getters: {
      double: (state) => {
      return state.count * 2;
      },
  },
  mutations: {
      add(state, payload) {
      state.count += payload;
      },
  },
  actions: {
      asyncAdd({ commit }, payload) {
      return new Promise((resolve, reject) => {
          setTimeout(() => {
          commit("add", payload);
          resolve();
          }, 1000);
      });
      },
  },
});
export default store;

引入 store

import store from "./store";
// 传入key值,标识 store
createApp(App).use(store, "my").mount("#app");

使用 store

<template>
    <div>
        count:{{ count }}
        <hr />
        getter:{{ double }}
        <hr />
        <button @click="$store.state.count++">直接修改state</button>
        <button @click="add">同步修改</button>
        <button @click="asyncAdd">异步修改</button>
    </div>
</template>
<script>
import { computed } from "vue";
import { useStore } from "@/vuex";
export default {
    name: "App",
    setup() {
        // 传入 key 使用特定的 store
        const store = useStore("my");
        function add() {
            store.commit("add", 1);
        }
        function asyncAdd() {
            store.dispatch("asyncAdd", 1).then(() => {
                console.log("ok");
            });
        }
        return {
            count: computed(() => store.state.count),
            double: computed(() => store.getters.double),
            add,
            asyncAdd,
        };
    },
};
</script>

vuex 运行流程

Vuex 的运作流程如下图所示:

核心原理

  • vuex4 是一个插件,所以创建的 store 实例需要实现一个 install 方法
  • vuex4 需要导出 createStore,用于创建 store ,接收一个 options 对象,
  • vuex4 需要导出 useStore ,用于在组件中使用 store
  • store 是一个全局状态库,并且是响应式的,可以在各个组件中使用 store 中的状态
  • 可以创建多个 store 实例,通过 key 标识来区分不同的 store

实现一个简易版的 vuex

首先不考虑 modules、插件、严格模式、动态模块等功能,实现一个简易版的vuex; 该版本包含的功能有:

  • store 的派发和注册
  • state 的响应式
  • gettersmutationsactionscommitdispatch
  • 通过 key 标识多个 store

实现 store 的派发和注册、响应式、injectKey

  • 通过 provide/inject 实现 store 的派发和注册
  • 通过 reactive 实现 state 的响应式
  • 通过在 provide/inject 时传入 injectKey ,来标识不同的 store
import { inject, reactive } from "vue";
const storeKey = "store";
class Store {
    constructor(options) {
        const store = this;
        // state 响应式
        // 做状态持久化时需要整体替换state,为了保持state的响应式,用data进行包裹
        store._state = reactive({ data: options.state });
    }
    // 代理 store._state.data 到 store.state 上
    get state() {
        return this._state.data;
    }
    install(app, injectKey) {
        // 全局暴露一个变量,暴露的是store实例
        app.provide(injectKey || storeKey, this); // this 指向 store 实例
        // 设置全局变量 $store
        app.config.globalProperties.$store = this;
    }
}
export function createStore(options) {
  return new Store(options);
}
export function useStore(injectKey = storeKey) {
  return inject(injectKey);
}

实现 getters、mutations、actions、commit、dispatch

  • getters 的实现:将 options.getters 代理到 store.getters,并传入参数 store.state;在vue3.2以上版本,可以使用 computed 实现 getters 的缓存。
  • mutations 的实现:将 options.mutations 代理到 store._mutations 上,将 mutation 内部的 this 指向 store,并传入参数 store.statepayloadactions 的实现类似。
  • commitdispatch 的实现:它们是一个函数,通过传入的 typepayload 匹配并执行对应的 mutationaction
// 遍历 obj,对每一项执行 fn(obj[key], key)
export function forEachValue(obj, fn) {
  Object.keys(obj).forEach((key) => fn(obj[key], key));
}
class Store {
  constructor(options) {
    const store = this;
    store._state = reactive({ data: options.state });
    /**
     * 实现getters
     */
    const _getters = options.getters; // {getter1: fn1, getter2: fn2}
    store.getters = {};
    forEachValue(_getters, function (fn, key) {
      Object.defineProperty(store.getters, key, {
        get: computed(() => fn(store.state)), // 用 computed 对 getters 进行缓存
      });
    });
    /**
     * 实现 mutation 和 actions
     */
    store._mutations = Object.create(null);
    store._actions = Object.create(null);
    const _mutations = options.mutations;
    const _actions = options.actions;
    forEachValue(_mutations, (mutation, key) => {
      store._mutations[key] = (payload) => {
        mutation.call(store, store.state, payload);
      };
    });
    forEachValue(_actions, (action, key) => {
      store._actions[key] = (payload) => {
        action.call(store, store, payload);
      };
    });
  }
  /**
   * 实现 commit 和 dispatch
   * commit、dispatch必须写成箭头函数,来保证commit、dispatch里面的this指向store实例
   */
  commit = (type, payload) => {
    this._mutations[type](payload);
  };
  dispatch = (type, payload) => {
    this._actions[type](payload);
  };
  get state() {
    return this._state.data;
  }
  install(app, injectKey) {
    app.provide(injectKey || storeKey, this);
    app.config.globalProperties.$store = this;
  }
}

源码解析

当项目变得复杂,我们就不得不使用 modules 让项目结构更清晰,更具可维护性;同时引入严格模式、插件系统、动态modules等功能。

ModuleCollection

modules 包含 rootModule 以及 options.modules 中的各个子模块,我们 期望将用户传入的所有 module 转化成以下树状结构,并存放到 store._modules 变量中 :

root = {
    _raw: rootModule,
    state: rootModule.state,
    _children: {
        aCount: {
            _raw: aModule,
            state: aModule.state,
            _children: {
                cCount: {
                    _raw:cModule,
                    state: cModule.state,
                    _children:{}
                }
            },
        },
        bCount: {
            _raw: bModule,
            state: bModule.state,
            _children: {},
        },
    },
};

实现方式:

// vuex/store.js
import { storeKey } from "./injectKey";
import ModuleCollection from "./module/module-collection";
export default class Store {
  constructor(options) {
    const store = this;
    // 1. modules 数据格式化
    store._modules = new ModuleCollection(options);
  }
  install(app, injectKey) {
    app.provide(injectKey || storeKey, this);
    app.config.globalProperties.$store = this;
  }
}
// module/module-collection.js
import Module from "./module";
import { forEachValue } from "../utils";
export default class ModuleCollection {
  constructor(rootModule) {
    this.root = null;
    this.register(rootModule, []);
  }
  register(rawModule, path) {
    const newModule = new Module(rawModule);
    // 1. 如果是根模块
    if (path.length === 0) {
      this.root = newModule;
    } else {
      // 2. 如果不是根模块,则设置父模块的 _children 属性
      const parent = path.slice(0, -1).reduce((module, current) => {
        return module.getChild(current);
      }, this.root);
      // key 为 path 的最后一位
      parent.addChild(path[path.length - 1], newModule);
    }
    // 递归处理 modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(rawChildModule, path.concat(key));
      });
    }
  }
}
// module/module.js
import { forEachValue } from "../utils";
export default class Module {
  constructor(rawModule) {
    this._raw = rawModule;
    this.state = rawModule.state;
    this._children = {};
  }
  addChild(key, module) {
    this._children[key] = module;
  }
  getChild(key) {
    return this._children[key];
  }
  forEachChild(fn) {
    forEachValue(this._children, fn);
  }
}

installModule

另外,当我们取子 module 中的 state 时,采用的方式是:store.state.moduleA.count,是直接从store.state 上链式获取的。我们 期望在 store._state 上包含所有 modules 中的数据,其结构如下 :

{
    count: 0,
    moduleA: {
        count: 0
        moduleC: {
            count: 0
        }
    },
    moduleB: {
        count: 0
    }
}

所以我们首先需要将 store._modules.root.state 插入各个模块的 state 之后,改造成上述结构:

// vuex/store.js
function installModule(store, rootState, path, module) {
  let isRoot = !path.length;
  if (!isRoot) {
    let parentState = path
      .slice(0, -1)
      .reduce((state, key) => state[key], rootState);
    parentState[path[path.length - 1]] = module.state;
  }
  // 【遍历】 module._children,【递归】执行 installModule
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child);
  });
}
export default class Store {
  constructor(options) {
    const store = this;
    store._modules = new ModuleCollection(options);
    // 2. 改造 store._modules.root.state
    const state = store._modules.root.state; // 根状态
    installModule(store, state, [], store._modules.root);
  }
}

resetStoreState

创建 store._wrappedGettersstore._mutationsstore._actions 用来存储所有模块的 gettersmutationsactions,期望的格式如下:

store: {
    // actions 和 mutations 都是数组格式
    _actions: {
        'moduleB/asyncAdd': [ ƒ ]
    },
    _mutations: {
        'moduleA/add': [ ƒ ]
        'moduleA/moduleC/add': [ ƒ ]
        'add': [ ƒ ]
        'moduleB/add': [ ƒ ]
    }
    _wrappedGetters: {
        'moduleB/plus': () =&gt; (...)
        'double': () =&gt; (...)
    }
}

具体实现:

// vuex/store.js
// 根据路径,获取store上面的最新状态(因为store.state是响应式的,通过store.state.xx.xx获取的也是响应式的)
function getNestedState(state, path) {
  return path.reduce((state, key) => state[key], state);
}
function isPromise(val) {
  return val && typeof val.then === "function";
}
function installModule(store, rootState, path, module) {
  // 略...
  // getters  module._raw.getters
  module.forEachGetter((getter, key) => {
    store._wrappedGetters[key] = () => {
      return getter(getNestedState(store.state, path)); //  getter(module.state) 不可行,因为如果直接使用模块自己的状态,此状态不是响应式的
    };
  });
  // mutation:{add: [mutation1,mutation2], double: [mutation3]} 不同modules中的同名mutation放到同一个数组中
  module.forEachMutation((mutation, key) => {
    const entry = store._mutations[key] || (store._mutations[key] = []);
    entry.push((payload) => {
      // 也通过 getNestedState(store.state, path) 获取module的最新状态
      mutation.call(store, getNestedState(store.state, path), payload);
    });
  });
  // action:【action执行完返回一个Promise】
  module.forEachAction((action, key) => {
    const entry = store._actions[key] || (store._actions[key] = []);
    entry.push((payload) => {
      let res = action.call(store, store, payload);
      if (!isPromise(res)) {
        return Promise.resolve(res);
      }
      return res;
    });
  });
  // 【遍历】 module._children,【递归】执行各个module 的 installModule
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child);
  });
}
export default class Store {
  constructor(options) {
    const store = this;
    // 在store上定义变量,用来存储getters、mutations、actions
    store._wrappedGetters = Object.create(null);
    store._mutations = Object.create(null);
    store._actions = Object.create(null);
  }
}
// module/module.js
import { forEachValue } from "../utils";
export default class Module {
    // ...略
  forEachGetter(fn) {
    if (this._raw.getters) {
      forEachValue(this._raw.getters, fn);
    }
  }
  forEachMutation(fn) {
    if (this._raw.mutations) {
      forEachValue(this._raw.mutations, fn);
    }
  }
  forEachAction(fn) {
    if (this._raw.actions) {
      forEachValue(this._raw.actions, fn);
    }
  }
}

然后执行 resetStoreState ,实现数据响应式,并创建getters

// vuex/store.js
function resetStoreState(store, state) {
  // 由于state在状态持久化的时候可能会整体替换,为了维持响应式,给state包一层data属性
  store._state = reactive({ data: state });
  store.getters = {};
  forEachValue(store._wrappedGetters, (getter, key) => {
    Object.defineProperty(store.getters, key, {
      enumerable: true,
      get: () => getter(), // 在vue3.2版本后,可以用 computed 对 getter 值进行缓存
    });
  });
}
export default class Store {
  constructor(options) {
    const store = this;
    // 在store上定义变量,用来存储getters、mutations、actions
    store._wrappedGetters = Object.create(null);
    store._mutations = Object.create(null);
    store._actions = Object.create(null);
    store._modules = new ModuleCollection(options);
    const state = store._modules.root.state;
    installModule(store, state, [], store._modules.root);
    // state数据响应式、创建store.getters
    resetStoreState(store, state);
  }
  get state() {
    return this._state.data;
  }
}

实现 commitdispatch

export default class Store {
    // ...略
  commit = (type, payload) => {
    const entry = this._mutations[type] || [];
    entry.forEach((handler) => handler(payload));
  };
  dispatch = (type, payload) => {
    const entry = this._actions[type] || [];
    // action 返回的是一个 Promise
    return Promise.all(entry.map((handler) => handler(payload)));
  };
}

namespaced

在没有设置命名空间的情况下,模块内部的 actionmutationgetters 是注册在全局命名空间的,这样可能会导致多个模块对同一个 actionmutation 作出响应。启用命名空间会让模块内部的状态拥有私有局部空间,不受其他模块影响。 首先修改 Module 类,增加一个 namespaced 属性:

// vuex/module/module.js
export default class Module {
  constructor(rawModule) {
    this._raw = rawModule;
    this.state = rawModule.state;
    this._children = {};
    this.namespaced = rawModule.namespaced;
  }
}

然后创建 store._modules 实例的 getNamespaced 方法,用来获取 namespaced 路径,形如 moduleA/moduleC/

// vuex/module/module-collection.js
export default class ModuleCollection {
    // ...略
    // 获取 namespaced 的路径,形如 moduleA/moduleC/
    getNamespaced(path) {
        let module = this.root;
        return path.reduce((namespacedStr, key) => {
            module = module.getChild(key);
            return namespacedStr + (module.namespaced ? key + "/" : "");
        }, "");
    }
}

最后修改 store._mutationsstore._actionsstore.__wrappedGetters 中子模块相关的路径:

// vuex/store.js
function installModule(store, rootState, path, module) {
  // 略...
  const namespaced = store._modules.getNamespaced(path);
  // getters
  module.forEachGetter((getter, key) => {
    store._wrappedGetters[namespaced + key] = () => {
      return getter(getNestedState(store.state, path));
    };
  });
  // mutation
  module.forEachMutation((mutation, key) => {
    const entry = store._mutations[namespaced + key] || (store._mutations[namespaced + key] = []);
    entry.push((payload) => {
      mutation.call(store, getNestedState(store.state, path), payload);
    });
  });
  // action
  module.forEachAction((action, key) => {
    const entry = store._actions[namespaced + key] || (store._actions[namespaced + key] = []);
    entry.push((payload) => {
      let res = action.call(store, store, payload);
      if (!isPromise(res)) {
        return Promise.resolve(res);
      }
      return res;
    });
  });
  // ...略
}

严格模式

用户在 options 中通过 strict: true 开启严格模式;

  • 在严格模式中,mutation 只能执行同步操作
  • 修改 store 的状态只能在 mutation 中进行

实现严格模式的原理:

  • 设置一个初始状态 _commiting 为 false;当执行fn回调时,将 _commiting 设为 true,最后将 _commiting 设为 false;如果 fn 是同步的,那么在 fn 中获取到的 _commiting 就为 true,否则 在 fn 中获取到的 _commitingfalse
  • 如果没有通过 mutation 修改数据,那么 _commiting 依然为初始值 false

具体实现:

// vuex/store.js
import { watch } from "vue";
function resetStoreState(store, state) {
  // ...略
  if (store.strict) {
    enableStricMode(store);
  }
}
function enableStricMode(store) {
  // 监控数据变化
  // 1. 如果是mutation同步修改数据,则 store._commiting 为 true,不会报错
  // 2. 如果是mutation异步修改数据、或通过其它方式修改数据,则store._commiting 为 false,会报错
  watch(
    () => store._state.data,
    () => {
      // 当第一个参数是false是,会打印出警告
      console.assert(
        store._commiting,
        "do not mutate vuex store state outside mutation handlers"
      );
    },
    { deep: true, flush: "sync" } // watch 默认是异步的,这里改成同步(状态改变立刻执行回调)监听
  );
}
export default class Store {
    // 先把 this._commiting 改为 true,执行fn后,再将 this._commiting 改回去;如果fn是同步的,则在fn中this._commiting为true。
  _withCommit(fn) {
    const commiting = this._commiting;
    this._commiting = true;
    fn();
    this._commiting = commiting;
  }
  constructor(options) {
    // ...略
    this.strict = options.strict || false;
    this._commiting = false;
  }
  commit = (type, payload) => {
    const entry = this._mutations[type] || [];
    this._withCommit(() => {
      entry.forEach((handler) => handler(payload));
    });
  };
}

插件系统

手写一个状态持久化插件:

// vuex插件就是一个函数
// 实现一个数据持久化插件
function persistedStatePlugin(store) {
  // 从缓存中读取数据,并替换store中的state
  let local = localStorage.getItem("VUEX:STATE");
  if (local) {
    store.replaceState(JSON.parse(local));
  }
  // 每当状态变化(执行了mutation),就会执行subscribe的回调
  store.subscribe((mutation, state) => {
    // 缓存状态
    localStorage.setItem("VUEX:STATE", JSON.stringify(state));
  });
}
export default createStore({
    plugins: [persistedStatePlugin],
})

该插件有几个重点:

  • vuex插件本质上是一个函数,接收一个参数 store
  • store.replaceState() 方法会替换掉 state
  • 每当通过 mutation 修改了状态,都会执行 store.subscribe(fn) 里的回调函数(发布订阅模式)

具体实现:

// vuex/store.js
export default class Store {
    constructor(options) {
        // ...略
        // 执行插件(本质是一个函数)
        store._subscribers = [];
        options.plugins.forEach((plugin) => plugin(store));
    }
    subscribe(fn) {
        this._subscribers.push(fn);
    }
    replaceState(newState) {
        // 直接修改state会报错,所以使用 _withCommit 包裹一下
        this._withCommit(() => {
            this._state.data = newState;
        });
    }
    commit = (type, payload) => {
        const entry = this._mutations[type] || [];
        this._withCommit(() => {
            entry.forEach((handler) => handler(payload));
        });
        // 每次 commit 的时候执行所有的 subscribers
        this._subscribers.forEach((sub) => sub({ type, payload }, this.state));
    };
}

store.registerModule

vuex 可以使用store.registerModule 动态注册modules,使用方式如下:

import { createStore } from "@/vuex";
const store = createStore({
    // ...略
})
// 在moduleA内部创建一个moduleC
store.registerModule(["moduleA", "moduleC"], {
  namespaced: true,
  state: { count: 0 },
  mutations: {
    add(state, payload) {
      state.count += payload;
    },
  },
});
export default store;

具体实现:

  • 创建 store.registerModule 方法
export default class Store {
    registerModule(path, rawModule) {
        const store = this;
        if (typeof path === "string") {
            path = [path];
        }
        // 1. 在原有模块基础上新增加一个module
        const newModule = store._modules.register(rawModule, path);
        // 2. 再把模块安装上
        installModule(store, store.state, path, newModule);
        // 3. 重置容器
        resetStoreState(store, store.state);
    }
}

修改 ModuleCollectionregister 方法,返回新的 newModule

export default class ModuleCollection {
     // ...
     register(rawModule, path) {
         const newModule = new Module(rawModule);
         // ...略
         return newModule;
     }
     // ...
 }

installModule 中设置 parentStatestate 时,使用 store._withCommit() 进行包裹,否则会警告(严格模式下)

 function installModule(store, rootState, path, module) {
     if (!isRoot) {
     let parentState = path
         .slice(0, -1)
         .reduce((state, key) => state[key], rootState);
     store._withCommit(() => {
         parentState[path[path.length - 1]] = module.state;
     });
     module.forEachChild((child, key) => {
         installModule(store, rootState, path.concat(key), child);
     });
 }
 }

最后

本篇主要是对 vuex4.0 源码的学习总结,源代码仓库可以查看 mini-vuex4

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们