Vuex 原理讲解

72 阅读3分钟

本质是内部生成一个新的 Vue 实例,用于 state 的响应式及 getter 的依赖性

初始化

  • install:通过 Vue.mixin 去合并配置到组件上。之后组件在 beforeCreate 阶段从 this.$options.parent 获取 this.$store 对象(和 vueRouter 一样的方式)

store 实例化

  • ModuleCollection() 函数收集:从数据结构上看,模块的设计就是个树型结构
    • this.register 来处理模块的父子关系。最后都维护在this._modules.root
    • 结构:root={state, _children: {mA1: {state, _children: {mA11: {state, _children}}}}}
class ModuleCollection {
  /**
   * 按 path 遍历从 root 下获取对应的父节点。并返回该父节点下的 _children 属性
   * path 就是树形的一个分枝 ['m', 'm1', 'm11']
   * 注:path 为空时,直接返回 this.root 对象
   */
  get(path) {
    return path.reduce((module, key) => {
      return module.getChild(key);
    }, this.root);
  }
  
  // 递归执行
  register(path, rawModule) {
    const newModule = new Module(rawModule);
    if (path.length === 0) {
      // 根模块
      this.root = newModule;
    } else {
      /**
       * 树形递归遍历操作
       * 根据 path 找到对应的父节点,把子模块放入 parent._children 内
       * this.get() 和 parent.addChild() 是一起配合的完成的
       * path.slice(0, -1) 去除最后一个(本身)返回 新数组
       * 因为先执行 path.concat(key) 然后再走到这一步
       */
      // 第一层 parent -> this.root
      const parent = this.get(path.slice(0, -1));
      // path[path.length - 1] 获取最后一个元素,即是当前子模块
      parent.addChild(path[path.length - 1], newModule);
    }

    // 模块嵌套:设置了 options.modules 选项
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // [] -> ['ma', 'ma1', 'ma11']
        this.register(path.concat(key), rawChildModule);
      });
    }
  }
}

/**
 * 描述单个模块的类
 * _rawModule 模块的配置内容
 * _children 维护所有的 子模块
 * state 模块定义的 state
 */
class Module {
  constructor(rawModule) {
    this._rawModule = rawModule;
    this._children = {};
    this.state = rawModule.state;
  }

  addChild(key, module) {
    this._children[key] = module;
  }

  getChild(key) {
    return this._children[key];
  }
}
  • installModule() 安装:将模块中的state/getters/mutations/actions作初始化,都挂到 this 上响应的属性下。并将子模块的 state 都挂到 rootState 且设为响应式
    • Vue.set(parentState, moduleName, module.state)构建树形结构的 state (重要)
    • const local = makeLocalContext(store, path)来维护本地上下文对象 (重要)
/**
 * 创建本地的 context
 * 代理 state/getters 使访问时,访问的是 local 的数据
 */
function makeLocalContext(store, path) {
  const local = {
    dispatch: store.dispatch,
    commit: store.commit,
  };

  // 往 local 对象新增 getters/state 属性,并只能获取值
  Object.defineProperties(local, {
    getters: {
      get: () => store.getters,
    },
    state: {
      get: () => {
        /**
         * 从根节点的 state 然后遍历找到对应的子 state
         * reduce 使用逻辑和之前的不太一样
         */
        let state = store.state;
        return path.length
          ? path.reduce((state, key) => state[key], state)
          : state;
      },
    },
  });

  return local;
}
  • mutations/actions 维护在this._mutations/_actions={ keyName: [ fn1, fn2, nameSpace/fn3 ] }内。然后配合commit/dispatch(type)获取对应的 key 后执行 (重要)
    • 同名的都在一个数组内,之后执行时也是遍历触发
  • getters 维护在this.wrappedGetters{}内,之后在 store._vm 时会挂到computed属性上 (重要)
  • 注意除了 state 是树形结构的,其他的都是平级的对象通过 keyName 区分
// 将模块中的 state/getters/mutations/actions 作初始化,都挂到 this 上
// 并将子模块的 state 都挂到 rootState 设为响应式
function installModule(store, rootState, path, module) {
  const isRoot = !path.length;

  // 根的 state 在 resetStoreVM 动态化
  if (!isRoot) {
    const parentState = rootState;
    const moduleName = path[path.length - 1];
    /**
     * 按照 path 路径将 childModule.state 添加响应式
     * 结构 root.state.moduleName.state
     * 最终都是挂在了 this._vm._data.$$state 下的
     */
    Vue.set(parentState, moduleName, module.state);
  }

  // !重要的步骤。构造本地上下文对象
  const local = makeLocalContext(store, path);

  /**
   * 下面把 getter/mutaion/action 都添加到 rootStore 下
   */
  if (module._rawModule.actions) {
    forEachValue(module._rawModule.actions, (handler, type) => {
      registerAction(store, type, handler, local);
    });
  }

  if (module._rawModule.mutations) {
    forEachValue(module._rawModule.mutations, (mutation, key) => {
      registerMutation(store, key, mutation, local);
    });
  }

  if (module._rawModule.getters) {
    forEachValue(module._rawModule.getters, (getter, key) => {
      registerGetter(store, key, getter, local);
    });
  }

  // 有子模块,递归调用。最终是个树形结构
  forEachValue(module._children, (child, key) => {
    installModule(store, rootState, path.concat(key), child);
  });
}

/**
 * 设定的 函数 需要读取到 state 参数,所以先把 state 放到内部
 * 同一个 type 的 _mutations 可以对应多个方法
 * !即使配置了 namespace 其实也是存储在 _mutations 中,只是 key 是带着 namespace 前缀的
 */
function registerMutation(store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = []);
  entry.push((payload) => {
    handler.call(store, local.state, payload);
  });
}
  • 初始化 store._vm:vuex 的响应式是通过 new Vue() 实现的
    • 配置的 state 设置在 data 里实现响应式
    • getters 配置在 computed 内,并代理 this.$store.getter
// 核心 实现响应式
function resetStoreVM(store, state) {
  // 使得可以使用 this.$store.getter.xxx 方式
  store.getters = {};
  const wrappedGetters = store._wrappedGetters;
  // !使用 computed 实现可缓存性
  const computed = {};
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = () => fn();
    Object.defineProperty(store.getters, key, {
      enumerable: true,
      get: () => {
        // 获取的是对应的 computed: {} -> computed[key]
        return store._vm[key];
      },
    });
  });

  store._vm = new Vue({
    data: {
      $$state: state,
    },
    computed,
  });
}

API

数据获取

  • state 通过store.state.a.b.xxx按照模块嵌套的树形结构获取
  • getter 通过store.getter.xxx获取。在 resetStoreVM() 设置了代理
  • mutaions/actions 通过store.commit/dispatch(keyName)执行
class Store {
/**
   * 这里执行的是个 高阶函数
   * this._mutations[type] 是个函数并已经传递了 state
   * this._mutations[type](payload) 传递了 payload
   */
  commit(type, payload) {
    // console.log('# commit', this, type, payload);
    // return this._mutations[type](payload);
    const entry = this._mutations[type];
    if (!entry) return;
    entry.forEach((handler) => handler(payload));
  }

  /**
   * action 执行的是异步函数,所有使用 Promise.all()
   */
  dispatch(type, payload) {
    // console.log('# dispatch', type);
    // return this._actions[type](payload);
    const entry = this._actions[type];
    if (!entry) return;
    return entry.length > 1
      ? Promise.all(entry.map((handler) => handler(payload)))
      : entry[0](payload);
  }
}

map 糖果语法

本质是从this.$store.state/mutations/actions上获取对应的内容,生成一个临时对象在合并到当前的组件实例下

  • mapState 返回一个 object 它的 val 为配置的处理函数,并把this.$store.state作为参数传入
  • mapGetters 返回内部包裹了this.$store.getter[key]的函数
  • mapMutations 返回包裹commit.apply(this.$store, [val].concat(args))的函数
/**
 * 通过 封装函数 调用组件实例自身上的 this.$store
 * 然后把这个 函数 合并到 computed/methods 中
 */
export const mapState = (states) => {
  const res = {};

  normalizeMap(states).forEach(({key, val}) => {
    res[key] = function mappedState() {
      let state = this.$store.state;
      return typeof val === 'function' ? val.call(this, state) : state[val];
    };
  });
  return res;
};

export const mapMutations = (mutations) => {
  const res = {};
  normalizeMap(mutations).forEach(({key, val}) => {
    res[key] = function mappedMutation(...args) {
      const commit = this.$store.commit;

      // this.$store.commit('xxx', payload)
      // [val].concat(args) -> ['xxx', payload]
      commit.apply(this.$store, [val].concat(args));
    };
  });

  return res;
};