源码实现系列之Vuex

源码实现系列之Vuex

这个月会实现一下Vue, Vuex, vue-router。我会以倒推的模式边开发边写文章。话不多说开始跟着我一起撸。仓库地址

本文只是实现了一个非常基础版本的vuex,日后会进行升级。且本文所写的代码,不会每个地方都做异常判断,只满足我表达vuex的实现原理即可。

回忆下我们平时代码里用到vuex必定会有的两行代码

1
2
3
Vue.use(Vuex);

store = new Vuex.Store({});

所以vuex需要导出两个方法,一个提供useinstall方法,一个就是Store

1
2
3
4
5
6
// vuex.js

export default {
install,
Store,
};

诸如vuexvue-router等都是使用Vue.mixin,往每个组件上挂载beforeCreate生命周期来初始化。

上代码

1
2
3
4
5
6
7
8
9
10
11
12
const install = (_Vue) => {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
this.$store = this.$options.store;
} else {
this.$store = this.$parent && this.$parent.$store;
}
},
});
};

Vueuse方法我就不解释了,感兴趣的可以去看下vue源码,他是调用use参数的install方法,并传入Vue构造函数。
那么上述为什么会判断this.$options.store读取$parent.$store呢?

这个时候请回忆下我们new Vue的都会传入store,所以当this.$options.store获取不到,代表他不是根节点(当然子组件也可以加store,这里只是常规分析)。this.$store = this.$parent && this.$parent.$store;,那么他很有可能就是子组件,就会向上层获取,上层肯定会有。就这样一层层往下传递。

install讲完了,接下来就讲我们的重头戏,Store;

Store

下面我将按照以下几个属性进行讲解

  • state
  • mapState
  • getters
  • mapGetters
  • mutations
  • mapMutations
  • action
  • mapActions

state

Store中的响应式都是借助Vue来进行数据响应式。

我先在Vuex注册的时候,加个state属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
// store.js
import Vuex from './vuex/index.js';
import Vue from 'vue';

Vue.use(Vuex);

const store = new Vuex.Store({
state: {
count: 1,
},
});

export default store;

这个时候,源码上就要支持state的获取

所以获取 state 的时候,直接读取vm.state,state只能获取不能修改

1
2
3
get state() {
return this.vm.state;
}

得到代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let Vue;
const install = (_Vue) => {
// ...
};

class Store {
constructor(options) {
console.log(options, 'options');
this.vm = new Vue({
data: {
state: options.state,
},
});
}

get state() {
return this.vm.state;
}
}

export default {
install,
Store,
};

编写vue组件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div id="app">
<div>{{ $store.state.a }}{{ $store.state.b }}</div>
</div>
</template>
<script>
export default {
name: 'App',
created() {
console.log(this.$store.state, 'store');
},
};
</script>

打开页面看下效果,页面成功读取到state中的值了;

mapState

我们一般使用mapState会有如下几种用法

1
2
3
4
5
6
7
8
9
export default {
computed: {
...mapState(['count']),
...mapState({
count2: 'count',
count3: (state) => `${state.count} xixi`,
}),
},
};

我们可以看到,mapState支持数组、对象、对象中的value还可以以函数的形式存在。
所以我们实现的时候

  • 如果是数组,就直接从state中取每个值。
  • 如果是对象,就将对象的每个键的值赋值为一个函数,并传入 state 这个对象。

实现方式如下

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
function normalizeMap(map) {
return Array.isArray(map)
? map.map((key) => ({ key, val: key }))
: Object.keys(map).map((key) => ({ key, val: map[key] }));
}

export const mapState = (options) => {
if (typeof options !== 'object') {
console.error(
'[vuex] mapActions: mapper parameter must be either an Array or an Object'
);
return;
}

const res = {};
normalizeMap(options).forEach((o) => {
if (typeof o.val === 'function') {
res[o.key] = function () {
return o.val.call(this.$store, this.$store.state);
};
} else {
res[o.key] = function () {
return this.$store.state[o.val];
};
}
});
return res;
};

getters

getters其实非常的简单,就是调用他的方法,传入state参数

1
<div>getsname: {{ $store.getters.getsname }}</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Store {
constructor(options) {
console.log(options, 'options');
+ this.getters = options.getters;
this.vm = new Vue({
data: {
state: options.state
}
});

+ Object.keys(this.getters).forEach(key => {
+ this.getters[key] = this.getters[key].call(this, this.vm.state);
+ });
+ }

get state() {
return this.vm.state;
}
}

mapGetters

mapGetters其实与mapState非常的相似,只是传参发生了变化。变成了(state, getters),我就不过多解释了。

1
2
3
<div>getsname: {{ getsname }}</div>
<div>getsname2: {{ getsname2 }}</div>
<div>getsname3: {{ getsname3 }}</div>
1
2
3
4
5
6
7
8
9
10
11
export default {
computed: {
// ...
...mapGetters(['getsname']),
...mapGetters({
getsname2: 'getsname',
getsname3: (state, getters) =>
`count: ${state.count}, getsname: ${getters.getsname}`,
}),
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const mapGetters = (options) => {
if (typeof options !== 'object') {
console.error(
'[vuex] mapActions: mapper parameter must be either an Array or an Object'
);
return;
}

const res = {};
normalizeMap(options).forEach((o) => {
if (typeof o.val === 'function') {
res[o.key] = function () {
return o.val.call(this.$store, this.$store.state, this.$store.getters);
};
} else {
res[o.key] = function () {
return this.$store.getters[o.val];
};
}
});
return res;
};

mutations

mutations其实也是非常的简单,只需要写个commit方法,并触发下mutations里对应的方法,并传入state跟用户的参数两个变量就行了。

1
2
3
<button @click="$store.commit('increment', 1)">
mutation:increment => {{ count }}
</button>
1
2
3
4
5
6
class Store {
// ...
commit(event, payload) {
this.mutations[event].call(this, this.state, payload);
}
}

mapMutations

mapMutations与上面的mapState, mapGetters差不多,就是遍历mutations返回一个对象。

1
2
<button @click="increment(1)">mapMutations mutation:increment => {{ count }}</button>
<button @click="increment2(2)">increment2 mutation:increment => {{ count }}</button>
1
2
3
4
5
6
7
8
export default {
methods: {
...mapMutations(['increment']),
...mapMutations({
increment2: 'increment'
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
export const mapMutations = options => {
if (typeof options !== 'object') {
console.log('本实例暂不支持module方式');
return {};
}
const res = {};
normalizeMap(options).forEach(o => {
res[o.key] = function(option) {
return this.$store.commit(o.val, option);
};
});
return res;
};

normalizeMap在上述map方法中使用到了,主要用途就是将数组或者对象处理成一个包含key, value对象。

actions

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作;

1
2
3
4
5
asyncIncrement(context, payload) {
setTimeout(() => {
context.commit('increment', payload.count);
}, 5000);
}

实现其实也是非常简单的,就是触发下注册的action而已。

先上代码

1
2
3
4
5
6
7
8
dispatch(event, payload) {
if (isObject(event)) {
const { type, ...res } = event;
event = type;
payload = res;
}
this.actions[event].call(this, this, payload);
}

这里加了层判断第一个参数是否是对象,如果是对象,事件名就取对象中的type字段,payload就取剩下的。

三种基本操作(常规操作、参数放在一个对象中传、异步操作)例子如下

1
2
3
<button @click="$store.dispatch('actionIncrement', { count: 3 })">test dispatch ===> actionIncrement</button>
<button @click="$store.dispatch({ type: 'actionIncrement', count: 4 })">test dispatch ===> actionIncrement</button>
<button @click="$store.dispatch({ type: 'asyncIncrement', count: 4 })">test dispatch ===> asyncIncrement</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
const store = new Vuex.Store({
// ...
actions: {
actionIncrement(context, payload) {
context.commit('increment', payload.count);
},
asyncIncrement(context, payload) {
setTimeout(() => {
context.commit('increment', payload.count);
}, 5000);
}
}
});

mapActions

mapActionsmapMutations基本逻辑是一致的,一个是用commit触发mutations中的方法,一个是用dispatch触发actions中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
export const mapActions = options => {
if (typeof options !== 'object') {
console.log('本实例暂不支持module');
return {};
}
const res = {};
normalizeMap(options).forEach(o => {
res[o.key] = function(option) {
return this.$store.dispatch(o.val, option);
};
});
return res;
};

vue页面例子如下

1
2
3
4
5
6
<div>
<p>测试 mapActions</p>
<button @click="actionIncrement({count: 5})">常规操作actionIncrement</button>
<button @click="asyncIncrement({count: 6})">异步操作asyncIncrement</button>
<button @click="actionIncrement3({count: 7})">更换名字actionIncrement3</button>
</div>
1
2
3
4
5
6
7
8
export default {
methods: {
...mapActions(['actionIncrement', 'asyncIncrement']),
...mapActions({
actionIncrement3: 'actionIncrement'
})
}
}

到此vuex基本功能已经全部满足了。日后有心思的话,会对文章及代码进行升级,并实现一套完整的vuex

讲解代码如下

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
let Vue;
const install = _Vue => {
if (Vue) {
return console.log('请勿重复安装');
}
Vue = _Vue;
_Vue.mixin({
beforeCreate() {
if (this.$options.store) {
this.$store = this.$options.store;
} else {
this.$store = this.$parent && this.$parent.$store;
}
}
});
};

function normalizeMap(map) {
return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] }));
}

function isObject(obj) {
return Object.prototype.toString.call(obj).slice(8, -1) === 'Object';
}
class Store {
constructor(options) {
this.getters = options.getters || {};
this.mutations = options.mutations || {};
this.actions = options.actions || {};

this.vm = new Vue({
data: {
state: options.state
}
});

Object.keys(this.getters).forEach(key => {
this.getters[key] = this.getters[key].call(this, this.vm.state);
});
}

get state() {
return this.vm.state;
}

commit(event, payload) {
this.mutations[event].call(this, this.state, payload);
}

dispatch(event, payload) {
console.log(event, 'event')
if (isObject(event)) {
const { type, ...res } = event;
event = type;
payload = res;
}
this.actions[event].call(this, this, payload);
}
}

export const mapState = options => {
if (typeof options !== 'object') {
console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object');
return;
}

const res = {};
normalizeMap(options).forEach(o => {
if (typeof o.val === 'function') {
res[o.key] = function() {
return o.val.call(this.$store, this.$store.state);
};
} else {
res[o.key] = function() {
return this.$store.state[o.val];
};
}
});
return res;
};

export const mapGetters = options => {
if (typeof options !== 'object') {
console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object');
return;
}

const res = {};
normalizeMap(options).forEach(o => {
if (typeof o.val === 'function') {
res[o.key] = function() {
return o.val.call(this.$store, this.$store.state, this.$store.getters);
};
} else {
res[o.key] = function() {
console.log(this.$store.state, o.value, '$sotre');
return this.$store.getters[o.val];
};
}
});
return res;
};

export const mapMutations = options => {
if (typeof options !== 'object') {
console.log('本实例暂不支持module');
return {};
}
const res = {};
normalizeMap(options).forEach(o => {
res[o.key] = function(option) {
return this.$store.commit(o.val, option);
};
});
return res;
};

export const mapActions = options => {
if (typeof options !== 'object') {
console.log('本实例暂不支持module');
return {};
}
const res = {};
normalizeMap(options).forEach(o => {
res[o.key] = function(option) {
return this.$store.dispatch(o.val, option);
};
});
return res;
};

export default {
install,
Store
};

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×