手写一个简单的 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
<div id="demo">
<div>{{$store.state.counter}}</div>
<div>{{$store.getters.doubleCounter}}</div>
<button @click="add">Add</button>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="./vuex.js"></script>
<script>
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
counter: 0,
},
mutations: {
add(state) {
state.counter++
},
},
actions: {
add({commit}) {
setTimeout(() => {
commit('add')
}, 1000)
},
},
getters: {
doubleCounter: (state) => {
return state.counter * 2
},
},
})

new Vue({
el: '#demo',
store,
methods: {
add() {
this.$store.dispatch('add')
},
},
})
</script>

插件基本结构

根据我们 vuex 的使用方式,我们写出插件的基本结构如下:

1
2
3
4
5
6
7
8
9
10
class Store {
constructor(options) {}
}

const install = function (Vue) {}

const Vuex = {
Store,
install,
}

实现取值功能

先不考虑 getters, 我们实现下最基本的取值功能:

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
class Store {
constructor(options) {
const {state, mutations, actions} = options
Vue.util.defineReactive(this, '_state', state)
}

get state() {
return this._state
}

set state(val) {
console.warn('这样做不太好吧')
}
}

const install = function (Vue) {
Vue.mixin({
beforeCreate() {
// 只有根组件上面会有这个
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
},
})
}

我们在 Store 中通过拦截器实现了外部对 state 的只读功能,内部则通过一个变量 _state 来进行数据的存储和修改。这里必须要将该数据定义成响应式数据,因为视图的更新是依赖于 _state 的变化的。同时,我们在插件安装的时候混入了生命周期 beforeCreate,因为 this.$options.store 只会存在于根 Vue 实例,所以这里只会执行一次,并将 store 这个实例挂载到原型上共享给所有子组件。

实现数据操作功能

数据操作功能主要涉及到 commitdispatch 两个函数,这两个函数很简单,就是找到对应的 mutationaction, 并执行。这里为了 hold 住用户各种奇怪的调用场景,直接把这两个函数的执行上下文绑定为当前 Store 实例,避免出错。

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
class Store {
constructor(options) {
...

this._mutations = mutations
this._actions = actions
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}

commit(type, payload) {
const entry = this._mutations[type]

if (!entry) {
console.error('没有这个mutation')
return
}

entry(this.state, payload)
}

dispatch(type, payload) {
const entry = this._actions[type]

if (!entry) {
console.error('没有这个action')
return
}

entry(this, payload)
}
...
}

实现 getters

注意到我们的每一个 getter 是一个函数,但是我们在使用的时候是直接访问的 getter 的属性名,所以在 Store 类中,需要把访问属性转换为执行函数,并返回结果。要实现这个功能,很快想到可以使用 defineProperty。同时,每一个 getter 可以接受 state 作为函数的第一个参数,所以我们还得再封装一层,把当前实例的 _state 传递过去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Store {
constructor(options) {
const {state, mutations, actions, getters} = options

...

this.getters = {}
Object.keys(getters).forEach((key) => {
const fn = () => getters[key](this._state)
Object.defineProperty(this.getters, key, {
get() {
return fn()
},
})
})
}
...

至此,一个简单的 vuex 就实现了。