跟 vue-router 一样,vuex 也有几个基本步骤:
- 注册插件
- 实例化 Vuex
- 挂载到 Vue
Vuex 导出模块内容为:
declare const _default: {
Store: typeof Store;
install: typeof install;
mapState: typeof mapState,
mapMutations: typeof mapMutations,
mapGetters: typeof mapGetters,
mapActions: typeof mapActions,
createNamespacedHelpers: typeof createNamespacedHelpers,
createLogger: typeof createLogger
};
export default _default;
以以下实例阅读源码:
import Vue from 'vue'
import Vuex from '../../node_modules/vuex/dist/vuex.common.js'
// import Vuex from 'vuex'
Vue.use(Vuex) // 注册 Vuex 插件
// 实例化 Store
export default new Vuex.Store({
state: {
name: 'A'
},
mutations: {
changeName(state, name) {
state.name = name
}
},
actions: {
changeNameAsync({ commit }, payload) {
setTimeout(() => {
commit('changeName', payload)
}, 1000)
}
},
modules: {
mA: {
state: {
age: 18
},
mutations: {
changeAge(state, age) {
state.age = age
}
},
actions: {
changeAgeAsync({ commit }, payload) {
setTimeout(() => {
commit('changeAge', payload)
}, 1000);
}
}
}
}
})
# 注册插件
function install (_Vue) {
if (Vue && _Vue === Vue) {
if ((process.env.NODE_ENV !== 'production')) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
);
}
return
}
Vue = _Vue;
applyMixin(Vue);
}
function applyMixin (Vue) {
var version = Number(Vue.version.split('.')[0]);
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.
*/
function vuexInit () {
var options = this.$options;
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
}
}
插件注册的主要逻辑:
- 先确保 vuex 只注册一次,接着调用 applyMixin 函数。
- applyMixin 函数内部根据 vue 版本的不同,对 vues 初始化逻辑的注入方式不同。vue 版本高于等于 2,主要采用 mixin 机制;而低于 2 版本则采用将初始化逻辑添加到 Vue 原型链上。不管哪种方式,都是注入 vuexInit 函数。
- vuexInit 函数的执行时机:将 vuex 挂载到 vue 后,vue 实例化时。
# 实例化 Vuex.Store
var Store = function Store (options) {
var this$1 = this;
if ( options === void 0 ) options = {};
// 如果没有 Vue 但是在 window.Vue 上有,则注册安装 window.Vue
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
if ((process.env.NODE_ENV !== 'production')) {
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.");
}
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
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);
this._modules = new ModuleCollection(options);
this._modulesNamespaceMap = Object.create(null);
this._subscribers = [];
this._watcherVM = new Vue();
this._makeLocalGettersCache = Object.create(null);
// bind commit and dispatch to self
var store = this;
var ref = this;
var dispatch = ref.dispatch;
var 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)
};
// 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);
}
};
代码执行逻辑:
- 如果没有 Vue 但是在 window.Vue 上有,则注册安装 window.Vue;
- 初始化一系列基本信息,着重关注
this._modules = new ModuleCollection(options);
和this._watcherVM = new Vue();
的初始化。this._modules = new ModuleCollection(options);
:收集 Modules 。this._watcherVM = new Vue();
:初始化 Vue 实例作为 VM,通过该操作可实现数据的发布订阅。
- 绑定 commit 和 dispatch 到当前 this,主要通过 call 函数实现。
- 获取根 state,调用
installModule(this, state, [], this._modules.root);
初始化 Module。 - 调用
resetStoreVM(this, state)
重置 vm,建立 getter 和 state 的关系。 - 应用插件。
# 收集 module
调用this._modules = new ModuleCollection(options);
函数。
var ModuleCollection = function ModuleCollection (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false);
};
该函数的主要作用:收集 module,函数内部调用 this.register
进行收集。
register 函数的参数有:
- path:收集 module 名当作 path。
- rawModule:当前 module。这里是最外层根 module。
- runtime:是否为运行时。
register 源码如下:
ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
var this$1 = this;
if ( runtime === void 0 ) runtime = true;
if ((process.env.NODE_ENV !== 'production')) {
assertRawModule(path, rawModule);
}
var newModule = new Module(rawModule, runtime);
if (path.length === 0) {
this.root = newModule;
} else {
var parent = this.get(path.slice(0, -1));
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);
});
}
};
代码执行逻辑:
调用
new Modules
实例化当前的 module,主要初始化一些当前 module 基本信息。module 源码如下:var Module = function Module (rawModule, runtime) { this.runtime = runtime; this._children = Object.create(null); // 用于保存子 module this._rawModule = rawModule; // 保存原 module var rawState = rawModule.state; // 保存当前 module 的 state // Store the origin module's state this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}; };
通过 path 的长度判断是否为根 module。如果是根 module 则赋值给 this.root 保存,非根 module 则在其父级 module 的子 module 中加入当前 module。
最后,判断是否有嵌套 module,有则递归调用 register 进行 module 的收集。
回到最开始的例子,执行this._modules = new ModuleCollection(options);
后,得到的结果为:
{
"root": {
"runtime": false,
"_children": {
"mA": {
"runtime": false,
"_children": {},
"_rawModule": {
"state": {
"age": 18
},
"mutations": {},
"actions": {}
},
"state": {
"age": 18
}
}
},
"_rawModule": {
"state": {
"name": "A"
},
"mutations": {},
"actions": {},
"modules": {
"mA": {
"state": {
"age": 18
},
"mutations": {},
"actions": {}
}
}
},
"state": {
"name": "A"
}
}
}
# 初始化 module
Store 内部调用逻辑installModule(this, state, [], this._modules.root);
function installModule (store, rootState, path, module, hot) {
var isRoot = !path.length;
var namespace = store._modules.getNamespace(path); // 获取命名空间
// 收集到命名空间 map
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && (process.env.NODE_ENV !== 'production')) {
console.error(("[vuex] duplicate namespace " + namespace + " for the namespaced module " + (path.join('/'))));
}
store._modulesNamespaceMap[namespace] = module;
}
// 非根 module,获取父级 module,将当前 module 设置到父 module 的 _children 列表中
if (!isRoot && !hot) {
var parentState = getNestedState(rootState, path.slice(0, -1));
var moduleName = path[path.length - 1];
store._withCommit(function () {
if ((process.env.NODE_ENV !== 'production')) {
if (moduleName in parentState) {
console.warn(
("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"")
);
}
}
Vue.set(parentState, moduleName, module.state);
});
}
// 为当前模块设置当前上下文 context 属性,即 dispatch 的函数参数 ctx
// 内部通过是否开始命名空间来判断获取的对应模块的上下文还是根模块的上下文
// local 为一个包含 commit\dispatch\getter\state 的对象
var local = module.context = makeLocalContext(store, namespace, path);
// 遍历注册 mutation,将每个 mutation 添加到 store._mutation 列表中
module.forEachMutation(function (mutation, key) {
var namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});
// 遍历注册 action,将每个 action 添加到 store._action 列表中
module.forEachAction(function (action, key) {
var type = action.root ? key : namespace + key;
var handler = action.handler || action;
registerAction(store, type, handler, local);
});
// 遍历注册 getter
module.forEachGetter(function (getter, key) {
var namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});
// 有子 module,递归注册 module
module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});
}
主要执行逻辑:
- 获取 namespace。
- 判断是否有设置 namespace,是的话则注册 namespace map。
- 设置 state。通过 path 获取父级 module 的 state,然后调用
Vue.set(parentState, moduleName, module.state);
设置 state。 - 通过 makeLocalContext 为当前模块设置当前上下文 context 属性,即 dispatch 的函数参数 ctx。函数内部通过是否开始命名空间来判断获取的对应模块的上下文还是根模块的上下文, local 为一个包含 commit\dispatch\getter\state 的对象。
- 遍历注册 mutation,将每个 mutation 添加到 store._mutation 列表中。
- 遍历注册 action,将每个 action 添加到 store._action 列表中。
- 遍历注册 getter。
- 有子 module,递归注册 module。
# registerMutation 函数实现
function registerMutation (store, type, handler, local) {
var entry = store._mutations[type] || (store._mutations[type] = []);
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload);
});
}
上面函数将 mutation 函数添加到对应 mutation type 列表 _mutation[type] 中,因为可能存在不同 module 中有相同 mutation 命名的情况。
# registerAction 函数实现
function registerAction (store, type, handler, local) {
var entry = store._actions[type] || (store._actions[type] = []);
entry.push(function wrappedActionHandler (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);
if (!isPromise(res)) {
res = Promise.resolve(res);
}
if (store._devtoolHook) {
return res.catch(function (err) {
store._devtoolHook.emit('vuex:error', err);
throw err
})
} else {
return res
}
});
}
action 中的函数主要执行的是异步函数,处理跟 mutation 一样将 action 函数添加到对应 action type 列表 _action[type] 中,还会对函数做 Promise 处理,如果不是 Promise 函数,则会将函数 Promise 化。
# registerGetter 函数实现
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
if ((process.env.NODE_ENV !== 'production')) {
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
)
};
}
注册 getter 则是将 getter 函数包裹一层函数,然后添加到 store._wrappedGetters[type] 上。
# makeLocalContext 函数实现
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 ((process.env.NODE_ENV !== 'production') && !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 ((process.env.NODE_ENV !== 'production') && !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
}
上面源码中,commit 和 dispatch 的逻辑比较相似,都是判断是否开启命名空间,没有的话直接使用原型(根 store)上的对应方法,有的话,如果不是根 module,则将当前命名空间拼接上当前 type 作为新的 type,再调用原型上的对应方法。对于 getters 和 state,则需要延迟计算,因为需要等数据更新后才重新计算。
# resetStoreVM
function resetStoreVM (store, state, hot) {
var oldVm = store._vm;
// bind store public getters
store.getters = {};
// 重置当前 getter 缓存
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
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(); });
}
}
上面函数主要建立 getter 和 state 的关系,因为 getter 内部值的获取是依赖于 state 的,借用 computed 可以实现对 getter 值的缓存。
通过对 store 的 getter 进行遍历,然后将每个 getter 收集到 computed 对象中,同时将每个 getter 进行代理到 store._vm 上。最后通过新建一个 Vue 实例 _vm
,将收集到的 computed 对象挂到 vue 实例中,同时在实例的 data 中挂上变量 $$state。
当访问 store.state.xxx 的时候,实际上通过代理会访问到_vm._data.$$state.xxx
。state 代理代码如下:
var prototypeAccessors$1 = { state: { configurable: true } };
prototypeAccessors$1.state.get = function () {
return this._vm._data.$$state
};
Object.defineProperties( Store.prototype, prototypeAccessors$1 );
当访问 store.getters.getter1 时,实际上是访问 store._vm.getter1;当 getter1 依赖的 state 改变时,也就是_vm._data.$$state.xxx
改变时,通过 vue 实例作为纽带,对应的 computed getters getter1 就会同时发生改变。