最近参加的几场面试,全部都问到了Vuex。虽然之前在工作中经常用到,但是被问到了还是只能答出个大概,不能够从原理上去剖析。趁着周末的时间,好好的捋一捋Vuex的源码。


为什么要使用Vuex?

首先我们来看看官网中的介绍

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

首先我们要知道的就是这个‘状态’,什么是状态,在Vuex中,状态就是数据,Vuex就是用来管理这些数据的。
Vuex通过将共享的数据抽离到全局,并以一个单例存放,通过Vue的响应式原理来进行数据的管理和更新,这也就是为什么Vuex只能跟Vue搭配使用的原因了。

用过Vuex源码的童靴应该对这张图很熟悉吧!
vuex实现的是一个单向数据流,在全局拥有一个State存放数据,所有修改State的操作必须通过Mutation进行,Mutation的同时提供了订阅者模式供外部插件调用获取State数据的更新。所有异步的接口需要走Action,经常用在调用后端异步的接口,并且Action也不能修改state的数据,还是需要通过mutation来修改,最后根据state的修改渲染到视图上。
上面的这段话就是我在面试时经常拿来说的,但是我感觉这样说是肯定不行的,所以今天我们就来看看Vuex的源码

注入

用过Vuex的小伙伴一定知道,Vuex使用前的配置十分简单,只需要提供一个store,然后将store注入到Vue实例中即可

1
2
3
4
5
6
7
8
9
// main.js
new Vue({
store,
router,
render: h => h(App)
}).$mount('#app');

// store/index.js
Vue.use(Vuex)

Vue使用插件的方法很简单,只需要调用Vue.use(plugin), 对于Vuex只需要Vue.use(plugin)。Vue.use()的原理就是通过调用插件的install方法来进行插件的安装,前提是插件是一个对象,如果传入的是一个函数,则直接执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function install (_Vue) {
// 避免重复安装Vuex
if (Vue && _Vue === Vue) {
{
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
);
}
return
}
// 保存Vue实例,同时可用于检测是否重复安装
Vue = _Vue;
applyMixin(Vue);
}

这段代码很简单,主要做了两件事,一是防止Vuex被重复安装,二是执行applyMixin方法。
至于applyMixin方法,主要是为了初始化Vuex,Vue针对Vue1.0和Vue2.0进行了不同的处理。对于Vue1.0,Vuex会将vuexInit放入到Vue的_init方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function vuexInit () {
var options = this.$options;
// store injection
// 判断是否存在store,存在就代表是Root节点
if (options.store) {
// 直接执行store(function时)或者使用store(非function)
this.$store = typeof options.store === 'function'
? options.store()
: options.store;
} else if (options.parent && options.parent.$store) {
// 子组件直接从父组件中拿store,这样就确保了所有的组件共用同一份store
// 这样我们就可以通过this.$store访问全局Store实例了。
this.$store = options.parent.$store;
}
}

对于Vue2.0就简单了,直接利用mixin混入到实例中

1
Vue.mixin({ beforeCreate: vuexInit });

store对象

在分析store源码之前,我们先来看看它的构造过程(图片来自网上)

环境判断

首先进行了Vue判断,如果没有通过Vue.use(Vuex)注册,则调用install方法注册,然后判断当前环境是否支持promise以及是否通过new来创建store对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
...
// 判断是否已经安装过vuex
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
{
assert(Vue, "must call Vue.use(Vuex) before creating a store instance.");
// 判断当前环境是否支持promise
assert(typeof Promise !== 'undefined', "vuex requires a Promise polyfill in this browser.");
// 判断是否通过new操作符来创建store实例
assert(this instanceof Store, "store must be called with the new operator.");
}
...

初始化

判断通过后,开始进行一系列的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 判断是否制定了plugin和strict
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
var strict = options.strict; if ( strict === void 0 ) strict = false;

// 用来判断严格模式下是否是用mutation修改state的
this._committing = false;
// 保存action
this._actions = Object.create(null);
// action的订阅者
this._actionSubscribers = [];
// 保存mutation
this._mutations = Object.create(null);
保存getter
this._wrappedGetters = Object.create(null);
// Vuex支持store分模块传入,在内部用Module构造函数将传入的options构造成一个Module对象,
// 如果没有命名模块,默认绑定在this._modules.root上
this._modules = new ModuleCollection(options);
// 模块的namespace
this._modulesNamespaceMap = Object.create(null);
// 用来存放订阅者
this._subscribers = [];
// 用来实现Vue响应式,监听数据的变化
this._watcherVM = new Vue();

// 将dispatch、commit绑定到store对象本身
// 目的是为了在组件中通过this.$store直接调用dispatch、commit,并且this指向store实例
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)
};

// 传入参数,是否启用严格模式
this.strict = strict;

var state = this._modules.root.state;

// 初始化根module,下文讲
installModule(this, state, [], this._modules.root);

// 通过重设store, 通过Vue的响应式原理实现store的注册,下文讲
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);
}

在上面这段代码,主要实现的就是初始化一些内部变量,然后执行了两个核心的方法–installModule(初始化module)和resetStoreVM(通过VM使store变成响应式)。

初始化Module

installModule主要是为module加上命名空间,注册action、mutation、getter,同时递归所有子模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function installModule (store, rootState, path, module, hot) {
// 判断是不是根module
var isRoot = !path.length;
// 获取modulenamespace
var namespace = store._modules.getNamespace(path);

// 如果有namespace则在_modulesNamespaceMap中注册
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module;
}

// 不是根module,就把子module的每一个state设置到其父级的state属性上
if (!isRoot && !hot) {
// 获取父module
var parentState = getNestedState(rootState, path.slice(0, -1));
// 当前模块的模块名
var moduleName = path[path.length - 1];
store._withCommit(function () {
// 将子module设置为响应式的
Vue.set(parentState, moduleName, module.state);
});
}
// 给context对象赋值
var local = module.context = makeLocalContext(store, namespace, path);
// 循环注册mutation
module.forEachMutation(function (mutation, key) {
var namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});
// 循环注册每一个module的action
module.forEachAction(function (action, key) {
var type = action.root ? key : namespace + key;
var handler = action.handler || action;
registerAction(store, type, handler, local);
});
// 循环注册每一个module的getter
module.forEachGetter(function (getter, key) {
var namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});
// 递归注册每一个子module
module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});
}

在installModule函数中,首先判断是不是根module以及是否设置了命名空间,如果设置了命名空间,则将module放入_modulesNamespaceMap中。
如果不是根module,则调用Vue.set()方法将当前module的state挂载到父module的state中。然后调用 makeLocalContext函数给module.context赋值,设置局部的dispatch、commit方法以及getters和state。
具体的makeLocalContext方法就不再展开了(里面同样使用了Object.defineProperty来设置响应式)。

设置响应式

resetStoreVM函数中主要包括初始化store._vm,将state属性赋值到Vue实例的data属性,形成数据的响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function resetStoreVM (store, state, hot) {
// 保存老的_vm
var oldVm = store._vm;

// 初始化getter
store.getters = {};
// 在之前初始化getter时设置的
var wrappedGetters = store._wrappedGetters;
var computed = {};
// 通过Object.defineProperty为每一个getter设置get属性,类似于Vue的computed属性。
forEachValue(wrappedGetters, function (fn, key) {
// use computed to leverage its lazy-caching mechanism
computed[key] = function () { return fn(store); };
Object.defineProperty(store.getters, key, {
get: function () { return store._vm[key]; },
enumerable: true // for local getters
});
});

// 用一个vue实例来存储store树,将getters作为计算属性传入
var silent = Vue.config.silent;
Vue.config.silent = true;
store._vm = new Vue({
data: {
$$state: state
},
computed: computed
});
Vue.config.silent = silent;

// 如果是严格模式,则启用严格模式,深度watch state属性
if (store.strict) {
enableStrictMode(store);
}

if (oldVm) {
if (hot) {
// 若存在oldVm,解除对state的引用,等dom更新后把旧的vue实例销毁
store._withCommit(function () {
oldVm._data.$$state = null;
});
}
Vue.nextTick(function () { return oldVm.$destroy(); });
}
}

resetStoreVM会首先遍历_wrappedGetters,使用Object.defineProperty方法为每一个getter添加get方法,这样我们在组件中调用this.$store.getter.user就等同于访问store._vm.user。然后Vuex通过实例化一个Vue对象来实现数据的响应式原理,运用Vue内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。这样我们访问store._vm.user就相当于访问Vue实例中data的属性了。
注意:开启严格模式时,会深度监听$$state的变化,如果不是通过this._withCommit()方法触发的state修改,也就是store._committing如果是false,就会报错。
执行完上述代码之后,我们就可以利用this.$store.getter.test访问Vue实例中的test属性了。
看到了这儿,Vuex内部的实现已经完成理解一大半,接下来就可以愉快的去和面试官吹牛逼了。

为了更好的把这个牛逼吹下去,我们再来看看store提供的一些API。

dispatch

让我们先来看看dispatch的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Store.prototype.dispatch = function dispatch (_type, _payload) {
var this$1 = this;

// check object-style dispatch
var ref = unifyObjectStyle(_type, _payload);
var type = ref.type;
var payload = ref.payload;

var action = { type: type, payload: payload };
// actions中取出type对应的ation
var entry = this._actions[type];
if (!entry) {
{
console.error(("[vuex] unknown action type: " + type));
}
return
}

try {
// 执行所有的订阅者函数
this._actionSubscribers
.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);
}
}
// 如果数组长度大于1就包装promise形成一个全新的promise
// 如果只有一个元素就直接取出来执行
var result = entry.length > 1
? Promise.all(entry.map(function (handler) { return handler(payload); }))
: entry[0](payload);
...
};

再来看看registerAction的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 遍历注册action
function registerAction (store, type, handler, local) {
// 去除type对应的action
var entry = store._actions[type] || (store._actions[type] = []);
// 在action上添加handle函数
entry.push(function wrappedActionHandler (payload, cb) {
var res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb);
// 判断是否是promise
if (!isPromise(res)) {
// 不是Promise对象的时候转化称Promise对象
res = Promise.resolve(res);
}
if (store._devtoolHook) {
return res.catch(function (err) {
// 存在devtool插件的时候触发vuex的error给devtool
store._devtoolHook.emit('vuex:error', err);
throw err
})
} else {
return res
}
});
}

因为registerAction的时候将push进_actions的action进行了一层封装(wrappedActionHandler),所以我们在进行dispatch的第一个参数中获取state、commit等方法。
之后,执行结果res会被进行判断是否是Promise,不是则会进行一层封装,将其转化成Promise对象。dispatch时则从_actions中取出,只有一个的时候直接返回,否则用Promise.all处理再返回。

commit

commit函数会先进行一个参数适配,然后判断当前type对应的action type是否存在,如果存在则调用_withCommit函数执行相应的 mutation。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 提交mutation函数
Store.prototype.commit = function commit (_type, _payload, _options) {
var this$1 = this;

// check object-style commit
// 适配commit的两种调用方式
// commit('test', object)或者commit({type: 'test', object: object})
var ref = unifyObjectStyle(_type, _payload, _options);
var type = ref.type;
var payload = ref.payload;
var options = ref.options;

var mutation = { type: type, payload: payload };
// 这里的entry取值就是我们在registerMutation函数中push到_mutations中的函数,已经经过处理
var entry = this._mutations[type];
if (!entry) {
{
console.error(("[vuex] unknown mutation type: " + type));
}
return
}
// 执行mutation中的所有方法
this._withCommit(function () {
entry.forEach(function commitIterator (handler) {
handler(payload);
});
});
// 通知所有订阅者
this._subscribers.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'
);
}
};

commit方法会根据type找到并调用_mutations中的所有type对应的mutation方法,所以当没有namespace的时候,commit方法会触发所有module中的mutation方法。
再执行完所有的mutation之后会执行_subscribers中的所有订阅者。在commit结束以后则会调用这些_subscribers中的订阅者,这个订阅者模式提供给外部一个监视state变化的可能。

最后

本文主要介绍了Vuex组件库的一些源码实现,Vuex是一个非常优秀的Vue数据管理库,代码虽然不多但是结构很清晰,非常适合学习。希望小伙伴们能够自己去研读一番,如果有不懂得可以通过微信交流。写这篇文章也希望可以帮助到更多想要学习探索Vuex内部实现原理的同学。


本文完

参考:

Vuex 源码分析

Vuex 源码解析(如何阅读源代码实践篇)