手写简版Vuex
简单使用
javascript
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
user,
app,
// ...
},
getters
})
new Vue({
el: '#app',
store,
components: { App },
render: (h) => h(App)
})
实现效果
实现思路
vuex
是通过Vue.use
使用的,所以需要实现install
方法:install
里需要在Vue
全局挂载$store
方法,因此可以用Vue.mixin
全局混入配置。
通过
$store.state
可以访问状态,状态改变时,需要更新视图。所以state
需要设置为响应式,这样在视图中使用state
的时候会进行依赖收集,绑定state
和相应Watcher
的关系。之后state
改变就会触发Watcher
更新。- 一种方法是通过实例化
Vue
,将state
作为data
进行响应式处理。 - 一种方式是用
Vue.observable
将state
对象设置为响应式。
- 一种方法是通过实例化
触发更新:
commit
处理同步,找到mutations
里的对应函数,然后执行函数更新state
。dispatch
处理异步,找到actions
里的对应函数,然后执行函数更新state
。
getters
具备计算属性,可以通过Vue
中的computed
来进行处理(可能这也是Vuex
用实例化方式处理state
响应式的原因)。modules
的实现是根据store
配置递归建立相应的module
,并且建立module
之间的父子关系。再根据namespace
来分割各个模块,使得commit/dispatch
的时候需要指定模块的namespace
。
简化版实现代码
javascript
let _Vue
class Store {
constructor(options = {}) {
const { state, mutations, actions, getters } = options
this.mutations = mutations
this.actions = actions
this.getters = getters
let computed = {}
const self = this
// 通过 computed 来实现 getters
Object.entries(getters).map(([getterName, getter]) => {
computed[getterName] = () => {
// 封装一层,添加 this.state 作为参数
return getter(this.state)
}
// 访问 getters 的 key 就是访问 computed 的 key
Object.defineProperty(this.getters, getterName, {
get() {
return self._vm[getterName]
}
})
})
this._vm = new _Vue({
data: {
$$state: state
},
computed: {
...computed
}
})
// 同样可以实现响应式,但是 getters 实现又需要单独处理,比较麻烦
// this.$$state = _Vue.observable(state)
}
get state () {
return this._vm._data.$$state
}
set state (val) {
throw new Error('无法直接修改 state')
}
commit(type, payload) {
const handler = this.mutations[type]
handler(this.state, payload)
}
// 这里没做太多处理
dispatch(type, payload) {
const handler = this.actions[type]
return handler(this.state, payload)
}
}
const install = (Vue) => {
// 类似于单例模式
if (_Vue === Vue) return
_Vue = Vue
Vue.mixin({
beforeCreate () {
const options = this.$options
// 注入 $store 变量
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
}
export default {
Store,
install
}
问题
Q:为什么要commit/dispatch两种形式来处理,都使用dispatch不行吗?
- 一种可能的原因是单一职责,
commit
主要处理同步任务,触发状态更新;dispatch
主要处理异步任务 - 另一种原因是方便
devtools
追踪状态变化。参考这里的评论。