vuex(v3.6.2)源码梳理

如何理解vuex源码?

// vuex 源码分析
// date:2021-04-01
1. Vue.use(Vuex)
2.             -> 调用install插件机制安装Vuex 内部调用applyMixin
3.             -> Vue2.x以上采用Vue.mixin混入到beforeCreate Vue1.x 重写_init方法并添加到options.init
4. new Vuex.Store(options)
5.                       -> 初始化Store等内部变量
6.                       -> 通过options进行模块收集ModuleCollection
7.                       -> 给commit&dispatch包裹一层函数外衣 内部call-store调用
8. installModule安装模块(内部递归生成每层module实例且关联到父子级上)
9.                                      -> 注册mutations & actions & getter & child
10.                                     -> 做每层module的state进行提升到所对应的父级state中
11. resetStoreVM重置空vue且操作$$state
12.                                  -> 实例化空Vue并且作$$state上挂载当前处理后的state(store严格模式下校验是否外部改变了state异常)
13.                                  -> 作computed挂载
14. 添加插件 (是个数组格式)
15.        -> 官方提供了内置Logger插件
16.        -> 一般配合subscribe(订阅mutation),subscribeAction(订阅) 取消订阅 需要执行该方法执行后返回的函数
17. 是否启用devtool
/**1. 通过插件方式安装vuex */
// 源码 src/store.js 539-550 行
var Vue; // bind on install
function install(_Vue) {
  // 若注册过 则不重复注册
  if (Vue && _Vue === Vue) {
    {
      console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.');
    }
    return;
  }
  Vue = _Vue;
  applyMixin(Vue);
}
// 使用mixin添加到beforeCreate钩子
function applyMixin(Vue) {
  var version = Number(Vue.version.split('.')[0]);
  // 这里判断Vue的版本 2.x以上则直接使用mixin混入 : 同样是beforeCreate钩子上
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit });
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    var _init = Vue.prototype._init;
    Vue.prototype._init = function (options) {
      if (options === void 0) options = {};

      options.init = options.init ? [vuexInit].concat(options.init) : vuexInit;
      _init.call(this, options);
    };
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  // 初始化vuex 这里调用的时候 已经是在beforeCreate钩子里面 此时的this指向vue实例
  // 拿到当前实例化Vue的时候的配置项 里面有store
  // 而store来自于 Vuex.Store({...})
  function vuexInit() {
    var options = this.$options;
    // store injection
    if (options.store) {
      // 这里给vue实例上添加$store方法 等于options中执行store方法 前面我们是es6省略了
      // 相同属性key-value 所以options里面有个store方法 而这个store方法指向Vuex.Store
      this.$store = typeof options.store === 'function' ? options.store() : options.store;
    } else if (options.parent && options.parent.$store) {
      // 这里是 组件树都会被赋值上父级的$store方法 所以不管嵌套多深 都会挂载$store指向父级
      // 初始化时绑定的$store方法
      this.$store = options.parent.$store;
    }
  }
}
/**2. Store */
// 源码 src/store.js
var Store = function Store(options) {
  var this$1 = this;
  if (options === void 0) 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

  // 这里也是检查是否存在Vue window下是否有Vue 既是不是window环境
  if (!Vue && typeof window !== 'undefined' && window.Vue) {
    install(window.Vue);
  }

  {
    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.');
  }
  // TODO: 1. 插件
  // 这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数
  var plugins = options.plugins;
  if (plugins === void 0) plugins = [];

  // TODO:2. 是否严格模式
  // 在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。
  // 这能保证所有的状态变更都能被调试工具跟踪到。
  var strict = options.strict;
  if (strict === void 0) strict = false;

  // store internal state
  this._committing = false; // 是否在进行提交状态标识
  this._actions = Object.create(null);
  this._actionSubscribers = [];
  this._mutations = Object.create(null);
  this._wrappedGetters = Object.create(null);
  // TODO: 这里是对含modules模式处理 后面来看 下面的3. 分析
  this._modules = new ModuleCollection(options);
  this._modulesNamespaceMap = Object.create(null);
  this._subscribers = [];
  this._watcherVM = new Vue(); // Vue 组件用于 watch 监视变化
  this._makeLocalGettersCache = Object.create(null);

  // bind commit and dispatch to self
  var store = this;
  var ref = this;
  // 这里从实例原型上拿到dispatch方法(Store.prototype.dispatch)
  var dispatch = ref.dispatch;
  var commit = ref.commit;
  // 但是这里给实例又添加了dispatch方法 相当于是包裹了一层
  // 这里既不影响原型上的方法 而且包裹了一层 最后还是调的原型方法
  // 做这一步包裹 主要当代码中这样使用时 也满足
  // 如: this.$store.dispatch.call(this,type,payload)
  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;

  var 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(function (plugin) {
    return plugin(this$1);
  });

  var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;
  if (useDevtools) {
    devtoolPlugin(this);
  }
};
// 2.1 执行提交
Store.prototype._withCommit = function _withCommit(fn) {
  var committing = this._committing;
  this._committing = true;
  fn();
  this._committing = committing;
};
// 2.2 执行提交mutations操作
Store.prototype.commit = function commit(_type, _payload, _options) {
  var this$1 = this;

  // check object-style commit
  // TODO:这里检查提交的第一个参数是不是对象格式(但是对象格式的话必须包含'type'的key值)
  // 比如:this.$store.commit('mutations-type',payload)
  // this.$store.commit({type:'mutations-type'},payload)
  var ref = unifyObjectStyle(_type, _payload, _options);
  var type = ref.type;
  var payload = ref.payload;
  var options = ref.options;
  // 组装标准mutation提交格式
  var mutation = { type: type, payload: payload };
  // 从之前组装的_mutations 拿当前type 若没得就报错 退出
  var entry = this._mutations[type];
  if (!entry) {
    {
      console.error('[vuex] unknown mutation type: ' + type);
    }
    return;
  }
  this._withCommit(function () {
    entry.forEach(function commitIterator(handler) {
      handler(payload);
    });
  });
  // TODO: 当每次执行完mutations时 会触发监听subscriber执行 并包含参数mutation和state
  this._subscribers
    .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
    .forEach(function (sub) {
      return sub(mutation, this$1.state);
    });

  if (options && options.silent) {
    console.warn('[vuex] mutation type: ' + type + '. Silent option has been removed. ' + 'Use the filter functionality in the vue-devtools');
  }
};
// 2.3 监听mutations
// 注意:这里返回了一个函数 根据文档中说执行这个函数就可以停止监听
Store.prototype.subscribe = function subscribe(fn, options) {
  return genericSubscribe(fn, this._subscribers, options);
};
// 2.4 监听actions
Store.prototype.subscribeAction = function subscribeAction(fn, options) {
  var subs = typeof fn === 'function' ? { before: fn } : fn;
  return genericSubscribe(subs, this._actionSubscribers, options);
};
// 2.5 处理监听函数
function genericSubscribe(fn, subs, options) {
  // 若当前subs(this._subscribers) 找不到改handler函数 则根据options来
  // 压入头部还是尾部 默认尾部
  if (subs.indexOf(fn) < 0) {
    options && options.prepend ? subs.unshift(fn) : subs.push(fn);
  }
  // 这里运用了函数闭包特性 保存了此时fn-handler函数 方便当我们执行取消时
  // 从数组中删除
  return function () {
    var i = subs.indexOf(fn);
    if (i > -1) {
      subs.splice(i, 1);
    }
  };
}
// 2.6 提交actions
Store.prototype.dispatch = function dispatch(_type, _payload) {
  var this$1 = this;

  // check object-style dispatch
  // TODO: 同理检查type类型
  var ref = unifyObjectStyle(_type, _payload);
  var type = ref.type;
  var payload = ref.payload;

  var action = { type: type, payload: payload };
  var entry = this._actions[type];
  if (!entry) {
    {
      console.error('[vuex] unknown action type: ' + type);
    }
    return;
  }

  // 1. TODO: 这里先看有没得actions操作监听
  // 若配置了 默认为提交处理前 before (可配置after 提交处理后)
  try {
    this._actionSubscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .filter(function (sub) {
        return sub.before;
      })
      .forEach(function (sub) {
        return sub.before(action, this$1.state);
      });
  } catch (e) {
    {
      console.warn('[vuex] error in before action subscribers: ');
      console.error(e);
    }
  }
  // 2. TODO: 若存在对应的提交actions 且个数大于1则以Promise.all包裹所有的action提交操作
  // 若只有1个 则直接执行
  var result =
    entry.length > 1
      ? Promise.all(
          entry.map(function (handler) {
            return handler(payload);
          })
        )
      : entry[0](payload);
  // 注意:这里返回了Promise 那么我们可以在dispatch后可以执行.then操作        
  return new Promise(function (resolve, reject) {
    // 在前面entry[0](payload)时 我们使用res = Promise.resovle(res) 因此他会走成功回调
    // 这里就很关键 为什么说action操作时异步的 此时执行result.then()
    // 这里就涉及Promise知识了 .then操作是个异步 那么就会执行我们代码dispatch后面的内容
    // 因此说action是异步操作
    result.then(
      function (res) {
        try {
          // 执行监听after钩子
          this$1._actionSubscribers
            .filter(function (sub) {
              return sub.after;
            })
            .forEach(function (sub) {
              return sub.after(action, this$1.state);
            });
        } catch (e) {
          {
            console.warn('[vuex] error in after action subscribers: ');
            console.error(e);
          }
        }
        resolve(res);
      },
      function (error) {
        try {
          this$1._actionSubscribers
            .filter(function (sub) {
              return sub.error;
            })
            .forEach(function (sub) {
              return sub.error(action, this$1.state, error);
            });
        } catch (e) {
          {
            console.warn('[vuex] error in error action subscribers: ');
            console.error(e);
          }
        }
        reject(error);
      }
    );
  });
};
/**3. ModuleCollection 模块收集 */
var ModuleCollection = function ModuleCollection(rawRootModule) {
  // register root module (Vuex.Store options)
  // TODO:这里是初始化时 第一次注册 根store
  this.register([], rawRootModule, false);
};
/**
 *
 * @param {Array} path 模块路径集合 初始化为空数组
 * @param {*} rawModule 当前模块store对象
 * @param {*} runtime
 */
ModuleCollection.prototype.register = function register(path, rawModule, runtime) {
  var this$1 = this;
  if (runtime === void 0) runtime = true;
  // 检查它是不是合法的模块
  {
    assertRawModule(path, rawModule);
  }
  // 根据传入的store配置生成模块
  var newModule = new Module(rawModule, runtime);
  // TODO: 若path为空数组 则证明是一个根Store
  if (path.length === 0) {
    this.root = newModule;
  } else {
    // 若path不为空 则提取path的最开始到倒数第一个 变向说就是排除了最后一个
    var parent = this.get(path.slice(0, -1));
    // 说穿了就是找到各自的父级 将子级添加到父级的_children当中
    parent.addChild(path[path.length - 1], newModule);
  }

  // register nested modules
  if (rawModule.modules) {
    forEachValue(rawModule.modules, function (rawChildModule, key) {
      this$1.register(path.concat(key), rawChildModule, runtime);
    });
  }
};
// 断言推断是否是个module
var assertTypes = {
  getters: functionAssert, // 证明getter中配置项必须是一个函数
  mutations: functionAssert, // 证明mutations中配置项必须是一个函数
  actions: objectAssert, // 证明actions中配置项是一个函数或对象 这个很奇怪 平时基本不用
  // 类似:
  // actions:{
  //   {
  //     handler:function(){
  //       意思这样写 也得行?
  //     }
  //   }
  // }
};
var functionAssert = {
  assert: function (value) {
    return typeof value === 'function';
  },
  expected: 'function',
};
var objectAssert = {
  assert: function (value) {
    return typeof value === 'function' || (typeof value === 'object' && typeof value.handler === 'function');
  },
  expected: 'function or object with "handler" function',
};
function assertRawModule(path, rawModule) {
  Object.keys(assertTypes).forEach(function (key) {
    // 这里若在forEach中使用return 不会跳出循环 且跳过后面类似for -> continue
    if (!rawModule[key]) {
      return;
    }
    var assertOptions = assertTypes[key];
    forEachValue(rawModule[key], function (value, type) {
      assert(assertOptions.assert(value), makeAssertionMessage(path, key, type, value, assertOptions.expected));
    });
  });
}
ModuleCollection.prototype.get = function get(path) {
  // 这里使用一个累加器 将this.root作为第一次参数传入 分别调用获取子级
  // 这里就是变向的一直查找子级的子级 当然前提是path有很多值
  return path.reduce(function (module, key) {
    return module.getChild(key);
  }, this.root);
};
ModuleCollection.prototype.getNamespace = function getNamespace(path) {
  var module = this.root;
  return path.reduce(function (namespace, key) {
    module = module.getChild(key);
    return namespace + (module.namespaced ? key + '/' : '');
  }, '');
};
/**4. Module 模块 */
// Base data struct for store's module, package with some attribute and method
// 这块就是根据每个Store生成模块
var Module = function Module(rawModule, runtime) {
  this.runtime = runtime; // runtime属性
  // Store some children item
  this._children = Object.create(null); // 当前模块的子集
  // Store the origin module object which passed by programmer
  this._rawModule = rawModule; // 当前模块的原始完成Store数据
  var rawState = rawModule.state; // 取出当前模块的state项

  // Store the origin module's state
  // 若state是一个函数 则执行函数(意思state支持函数返回state 平时都是对象) 其余就直接挂载state
  this.state = (typeof rawState === 'function' ? rawState() : rawState) || {};

  // 即每个Module包含 4个属性
  // {
  //   runtime,
  //   _children,
  //   _rawModule,
  //   state
  // }
};
Module.prototype.getChild = function getChild(key) {
  return this._children[key]; // 就是返回子级模块
};
Module.prototype.addChild = function addChild(key, module) {
  this._children[key] = module; // 给当前模块赋值上子级模块
};
/**5. 安装module */
/**
 *
 * @param {*} store
 * @param {*} rootState
 * @param {*} path
 * @param {*} module
 * @param {*} hot
 */
function installModule(store, rootState, path, module, hot) {
  var isRoot = !path.length;
  var namespace = store._modules.getNamespace(path); // 获取当前命名空间 根节点为''

  // register in namespace map
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && true) {
      console.error('[vuex] duplicate namespace ' + namespace + ' for the namespaced module ' + path.join('/'));
    }
    store._modulesNamespaceMap[namespace] = module;
  }

  // set state
  // 非根节点时及非热更新时
  if (!isRoot && !hot) {
    // 获取当前module的父级state
    var parentState = getNestedState(rootState, path.slice(0, -1));
    var moduleName = path[path.length - 1];
    // 做这步操作就是为了构造state树 但是是将当前父级下的所有子级state提升到
    // 父级的state上面 而不是所有都提升到根父级下 这点要注意
    store._withCommit(function () {
      {
        // TODO:这里就是判断当前module的名字是否包含在父级的state里面
        if (moduleName in parentState) {
          console.warn('[vuex] state field "' + moduleName + '" was overridden by a module with the same name at "' + path.join('.') + '"');
        }
      }
      // 这里调用vue的set方法将父级state上增加子级state
      // 但是有个区别 这个时候parentState并没有被Vue处理过 不存在__ob__
      // 所以这里只是做了个简单讲子级添加到父级
      // 搞不懂 为啥要用Vue的方法 这里不该就自己写个方法添加吗???
      Vue.set(parentState, moduleName, module.state);
    });
  }

  var local = (module.context = makeLocalContext(store, namespace, path));

  module.forEachMutation(function (mutation, key) {
    var namespacedType = namespace + key;
    registerMutation(store, namespacedType, mutation, local);
  });

  module.forEachAction(function (action, key) {
    var type = action.root ? key : namespace + key;
    var handler = action.handler || action;
    registerAction(store, type, handler, local);
  });

  module.forEachGetter(function (getter, key) {
    var namespacedType = namespace + key;
    registerGetter(store, namespacedType, getter, local);
  });

  module.forEachChild(function (child, key) {
    // 这里就是递归执行 去注册所有的子级module
    installModule(store, rootState, path.concat(key), child, hot);
  });
}
/**
 * make localized dispatch, commit, getters and state
 * if there is no namespace, just use root ones
 */
function makeLocalContext(store, namespace, path) {
  var noNamespace = namespace === '';

  var local = {
    dispatch: noNamespace
      ? store.dispatch
      : function (_type, _payload, _options) {
          var args = unifyObjectStyle(_type, _payload, _options);
          var payload = args.payload;
          var options = args.options;
          var type = args.type;

          if (!options || !options.root) {
            type = namespace + type;
            if (!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
      : function (_type, _payload, _options) {
          var args = unifyObjectStyle(_type, _payload, _options);
          var payload = args.payload;
          var options = args.options;
          var type = args.type;

          if (!options || !options.root) {
            type = namespace + type;
            if (!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
        ? function () {
            return store.getters;
          }
        : function () {
            return makeLocalGetters(store, namespace);
          },
    },
    state: {
      get: function () {
        return getNestedState(store.state, path);
      },
    },
  });

  return local;
}
// 5.1 遍历Mutation
Module.prototype.forEachMutation = function forEachMutation(fn) {
  if (this._rawModule.mutations) {
    forEachValue(this._rawModule.mutations, fn);
  }
};
// 5.1.1 注册Mutation
function registerMutation(store, type, handler, local) {
  var entry = store._mutations[type] || (store._mutations[type] = []);
  // 这里是引用类型 则变相的给当前_mutations的执行函数包裹了一个wrappedMutationHandler
  entry.push(function wrappedMutationHandler(payload) {
    handler.call(store, local.state, payload);
  });
}
// 5.2 遍历Action
Module.prototype.forEachAction = function forEachAction(fn) {
  if (this._rawModule.actions) {
    forEachValue(this._rawModule.actions, fn);
  }
};
// 5.2.1 注册Action
function registerAction(store, type, handler, local) {
  var entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler(payload) {
    
  // 执行到用户编写的action操作时 
  // action接收两个参数xxx_action({commit,dispatch,getters,rootGetters,rootState,state},payload)
    var res = handler.call(
      store,
      {
        dispatch: local.dispatch,
        commit: local.commit,
        getters: local.getters,
        state: local.state,
        rootGetters: store.getters,
        rootState: store.state,
      },
      payload
    );
    // TODO:这点很关键 当我们执行了用户编写的action时 若没有返回值 则直接Promise.resolve该值
    if (!isPromise(res)) {
      // 那么现在res是一个Promise且状态为fulfilled
      res = Promise.resolve(res);
    }
    // 若存在_devtoolHook 则给res<Promise>增加catch方法 通知devTool
    if (store._devtoolHook) {
      return res.catch(function (err) {
        store._devtoolHook.emit('vuex:error', err);
        throw err;
      });
    } else {
      // 没有 那么久直接返回res 他是一个Promsie
      return res;
    }
  });
}
// 5.3 遍历Getter
Module.prototype.forEachGetter = function forEachGetter(fn) {
  if (this._rawModule.getters) {
    forEachValue(this._rawModule.getters, fn);
  }
};
// 5.3.1 注册Getter
function registerGetter(store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    {
      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
    );
  };
}
// 遍历子级
Module.prototype.forEachChild = function forEachChild(fn) {
  forEachValue(this._children, fn);
};
/**6. 重置store的vm */
/**
 *
 * @param {*} store store实例
 * @param {*} state 经过处理提升过后的state
 * @param {*} hot 是否热更新
 */
function resetStoreVM(store, state, hot) {
  var oldVm = store._vm;

  // bind store public getters
  store.getters = {};
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null);
  var wrappedGetters = store._wrappedGetters;
  var computed = {};
  forEachValue(wrappedGetters, function (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 environment.
    computed[key] = partial(fn, store);
    Object.defineProperty(store.getters, key, {
      get: function () {
        return store._vm[key];
      },
      enumerable: true, // for local getters
    });
  });

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins

  // TODO: 这里很巧妙实例化了一个空的vue 上面来装载“$$state”挂载state和computed!!!
  var silent = Vue.config.silent;
  Vue.config.silent = true;
  store._vm = new Vue({
    data: {
      $$state: state,
    },
    computed: computed,
  });
  Vue.config.silent = silent;

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store);
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(function () {
        oldVm._data.$$state = null;
      });
    }
    Vue.nextTick(function () {
      return oldVm.$destroy();
    });
  }
}
// 6.1 当为Store严格模式的时候 给$$state添加监听
// 因为在前面我们实例化了一个vue
// 并且利用data初始化了$$state
// 当我们手动去修改$$state时 比如this.$store.state.xxx = 1
// 由于引用类型 会触发修改$$state 此时也就触发了$$state的setter函数
// 而setter会noticfy通知watcher者 watcher也就执行了cb 也就是下面的断言报错
function enableStrictMode(store) {
  store._vm.$watch(
    function () {
      return this._data.$$state;
    },
    function () {
      {
        assert(store._committing, 'do not mutate vuex store state outside mutation handlers.');
      }
    },
    { deep: true, sync: true }
  );
}
/**7. devtool 模块*/
function devtoolPlugin(store) {
  if (!devtoolHook) {
    return;
  }

  store._devtoolHook = devtoolHook;
  // 初始化 devtool
  devtoolHook.emit('vuex:init', store);
  // 注册空间旅行
  devtoolHook.on('vuex:travel-to-state', function (targetState) {
    store.replaceState(targetState);
  });
  // 监听mutations
  store.subscribe(
    function (mutation, state) {
      devtoolHook.emit('vuex:mutation', mutation, state);
    },
    { prepend: true }
  );
  // 监听actions
  store.subscribeAction(
    function (action, state) {
      devtoolHook.emit('vuex:action', action, state);
    },
    { prepend: true }
  );
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值