引言
无论是 React 也好,Vue 也罢,它们都有自己的全局状态管理机制 Redux 和 Vuex。同样地,这也是它们框架的一大特色。并且,对于 Vue 而言,Vuex 和之前我讲的 Vue-Router 一样,是作为插件使用。
在认识 Vuex 的 Store 源码之前,我们先来回顾一下 Store 的特点:
- 存储的数据是全局可访问的
- 数据同样是响应式的,即它也是基于订阅者-发布模式
- 其中的数据好比被存到仓库中,和其他变量没有交叉和影响
- 数据的修改,需要通过提交 action,然后通过 mutation 来操作,可以很好地跟踪数据地变化过程
然后,我们带着这些优点,来认识一下 Vuex 的初始化过程它做了什么。
注册
对于 Vuex,同样地是通过 Vue.use() 方法注册。而 use 方法会对应地调用 Vuex 中的 install 方法。
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);
}
可以看出和 Vue-Router 的 install 方法大同小异,同样地也会在 install 方法中判断是否已经注册过 Vuex。如果没有注册过,将 use 方法中传的 Vue 实例(此时代码中对应的为 _ Vue)赋值给 Vue,然后调用 applyMixin(Vue)。
function applyMixin (Vue) {
var version = Number(Vue.version.split('.')[0]);
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit });
} else { // 1x 走的逻辑
...
}
而 applyMixin 则是根据不同版本,命中不同逻辑(本次我们只看 2x 版本)。首先,会先在 Vue 实例的 beforeCreate 生命周期函数注入 vuexInit 函数。
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;
}
}
}
vuexInit 函数做了两件事,如果此时 store。反正,则将它父级的 store。所以,这也是为什么我们在开发中可以使用 $store 的原因。
实例化
Vuex 的实例化,就是 new 一个 Vuex.Store() 实例。
var Store = function Store (options) {
var this$1 = this;
if ( options === void 0 ) options = {};
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);
}
};
store 构造函数的代码可能有点多,我们来分点讲解具体它做了什么:
1.对如果不是基于 Vue-Cli,即仅仅是通过 link 了一个 Vuex 的文件,然后在构造函数会通过在 window 上绑定一个 Vue 属性来作为 Install 的参数进行注册。
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
2.对开发环境下一些环境判断,例如必须使用 use 方法注册 Vuex、必须在支持或者有 polyfill 的 Promise 下开发、必须使用 new 关键字初始化 Vuex.Store。
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.");
}
3.在当前上下文 this 中绑定一些私有属性来存储 state、action、mutation、getter、module、moduleNamespaceMap(模块的映射)、subscribe(订阅者)、watcherVM(store对应的 watcher),里面有几个重要的属性,moduleNamespaceMap 这个的作用是记录此时的 moduleName(实际上是 /+name),然后每一个键值对应一个 module,从而完成 module 的映射,例如此时有一个 module a 和 b,而它们在源码中的映射为:
然后,这里出现了我们熟悉的 watcher 和 subscribe,是因为要用来动态更新那些订阅了 Store 的订阅者。
4.前面只是定义了我们需要使用的属性,然后,需要进行一些初始化操作,installModule 就是按照 module,并生成访问规则和模块映射关系,它的作用是便于开发时的代码编写,例如我们不同模块间是可以重复 action、state、getter 名称的。
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root);
5.初始化 module 后,我们需要对 state 进行响应式处理,以及将 getters 转成 computed。
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state);
6.最后,会对 options 进行一些插件调用以及和 devtoolPlugin 的一些配合,然后我们就可以在 devtool 中观察到我们 vuex 的一些变化。
// apply plugins
plugins.forEach(function (plugin) { return plugin(this$1); });
var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
devtoolPlugin(this);
}
};
总结
大致过了一下 Vuex 的初始化过程,主要是在传进的根实例上绑定和 Store 相关的一些私有属性,然后注册生成 module 树,然后对生成不同模块的属性的映射 Map,最后对 state 和 getters 进行一些转化,把它们传入到一个 vue 实例中(分别对应 vue 实例中的 data 和 compute),并通过 Obeject.defineProperty 对数据的访问做一层代理访问到 vue 实例中的 data 和 compute,从而实现数据的响应式。其实,我上面描述的一些具体的细节,有兴趣的同学还是写个简单的 Demo,然后 debugger 一下,这样记忆点会更深刻一些。