Vuex是专为Vue.js应用程序开发的全局状态管理器。这里我们可以把它拆成两部分来理解:一个是“全局状态管理”,一个是“专为Vue开发”。
首先来看全局状态管理。假设我们有一个购物网站的需求,在商品推荐、购物车等许多页面都需要显示商品的价格,并且同一商品在不同页面显示的价格应该是相同的,同时价格的变动也需要是同步变化。在这种场景下,我们往往会把商品的价格定义为一个全局变量,不管哪个页面需要获取商品的价格都去读取这个变量,不管因为什么原因导致了价格变动都去修改这个变量,这样我们就实现了多个页面价格数据的同步。不过,在实际开发中我们不会简单地定义一个全局变量,越来越多的全局变量会导致我们的代码难以维护;我们往往会给这些需要放到全局的数据定义一个命名空间,并且按照一些约定对操作这些数据的行为进行封装。这样,我们就实现了对全局状态的管理。
“专为Vue开发”是因为Vuex使用了Vue对数据的“变化侦测”系统。上面我们实现了全局状态管理,但是全局状态的变化并不会主动通知使用它的地方,比如它不会触发dom重新渲染,如果购物车页面想要在商品价格变动时实时渲染页面,则它需要通过一定的机制(比如轮询、全局状态修改的回调等)来获取全局状态的改变,然后在得到全局状态发生改变的消息后“拉取”最新的价格数据,从而实现dom的更新。Vuex正是通过Vue对数据的“变化侦测”系统实现这个过程的,在初始化存储在Vuex中的state时,会使用Vue初始化data时一样的机制来给state收集依赖,在state的属性发生变化时,会触发相应依赖的更新事件,从而实现渲染dom等操作。
接下来我们来看一下Vuex的使用方法,然后再根据它的使用方法反推它的内部实现,手写一个简单的Vuex。
使用Vuex首先需要创建一个Vuex示例:
new Vuex.store({
// Vuex模块,如果需要存储的全局状态非常多,可以分成不同的模块,然后以模块的方式进行注册
modules: {},
// 需要保存的全局状态
state: {
value: 'abc'
},
// 类似于计算属性,可以对数据进行格式化
getters: {
formatValue (state, getters) {
return `currentValue: ${state.value}`
}
}
// 修改全局状态的方法,所有全局状态的修改,都应该定义为方法写在这里
// 此处注册的方法,第一个参数是存储的全局状态数据对象,后面的参数是提交修改时传递的参数
mutations: {
updateValue (state, newValue) {
state.value = newValue
}
},
// 异步修改全局状态的方法,该方法主要封装异步逻辑,不能直接修改state,需要通过commit调用mutations中的方法进行修改
// 此处注册的方法,第一个参数是当前的Vuex实例,后面的参数是提交修改时传递的参数
actions: {
asyncUpdateValue (context, newValue) {
setTimeout(() => {
context.commit('updateValue', newValue)
}, 10)
}
}
})
在页面上使用Vuex存储的全局状态:
/************** 获取值 **************/
// 直接取值
console.log(this.$store.state.value) // 'abc'
// 为了防止Vuex存储的值被意外修改,一般会把它转换为计算属性使用
computed: {
value () {
return this.$store.state.value
}
}
// 通过getters获取被处理过的值
console.log(this.$store.getters.formatValue) // 'currentValue: abc'
/************** 修改值 *************/
// 执行mutations中的方法修改值
// commit方法的第一个参数是注册在mutations中的方法名,其他参数会在执行该方法时传给它
// 如果需要传递多个参数,建议通过对象字面量的方式进行传参
this.$store.commit('updateValue', 'ABC')
// 执行actions中的方法修改值
// dispatch方法的第一个参数是注册在actions中的方法名,其他参数会在执行该方法时传给它
this.$store.dispatch('asyncUpdateValue', 'AAA')
首先说明一下,笔者并没有看过Vuex源码,以下代码是根据Vuex的使用方法反推的代码实现;如果代码有问题或者有不合理的地方,欢迎在评论区留言。另外,本文只实现Vuex的核心功能,对state进行的数据侦测、Vuex模块、以及Vuex的一些工具方法暂时不作讨论。
首先,初始化Vuex是用new Vuex.store()操实例化出一个对象,并且这个对象具有commit和dispatch方法。所以我们推断Vuex.store是一个构造函数,并且它的prototype上有commit、dispatch方法:
// 注:为了代码简洁,我们用Store来代替Vuex.store,后面的代码一样
class Store {
constructor (options) {},
commit () {},
dispatch () {}
}
初始化对象时,需要对传入的选项对象进行初始化。
// 注:为了代码简洁,我们用Store来代替Vuex.store,后面的代码一样
class Store {
constructor (options) {
// 给state赋值后,需要调用Vue的数据监听方法,将其转换成响应式数据,这里省略了这部分逻辑
this.state = options.state || {}
// 保存mutations方法
this.mutations = options.mutations || {}
// 保存actions方法
this.actions = options.actions || {}
// getters比较特殊,我们放到后面讨论
},
commit () {},
dispatch () {}
}
现在我们已经可以通过$store.state.value来取到当前状态的值了,接下来我们实现commit和dispatch方法。从调用方式来看,这两个方法非常相似,都是通过一个字符串值来从已注册的方法中找到需要调用的那个,在调用的时候他们都会把第2个及以后的参数原封不动地传给回调;不同的是commit会把state作为第一个参数、dispatch会把当前的Vuex实例作为第一个参数传给回调。
// 注:为了代码简洁,我们用Store来代替Vuex.store,后面的代码一样
class Store {
constructor (options) {
// 给state赋值后,需要调用Vue的数据监听方法,将其转换成响应式数据,这里省略了这部分逻辑
this.state = options.state || {}
// 保存mutations方法
this.mutations = options.mutations || {}
// 保存actions方法
this.actions = options.actions || {}
// getters比较特殊,我们放到后面讨论
},
commit (callbackName, ...options) {
return this.mutations[callbackName] && this.mutations[callbackName](this.state, ...options)
},
dispatch (callbackName, ...options) {
return this.actions[callbackName] && this.actions[callbackName](this, options)
}
}
现在我们可以通过$store.commit()、$store.dispatch()方法来修改状态了,接下来我们看一下getters。getters之所以特殊,是因为它是以方法的形式进行注册的,但确却是以对象属性的方式进行取值的。对于这个需求,我的第一想法是通过对象属性的get来实现:
// 注:为了代码简洁,我们用Store来代替Vuex.store,后面的代码一样
class Store {
constructor (options) {
// 给state赋值后,需要调用Vue的数据监听方法,将其转换成响应式数据,这里省略了这部分逻辑
this.state = options.state || {}
// 保存mutations方法
this.mutations = options.mutations || {}
// 保存actions方法
this.actions = options.actions || {}
// 处理getters
const getters = options.getters || {}
this.getters = Object.create(null)
for (const key in getters) {
Object.defineProperty(this.getters, key, {
enumerable: true,
get: () => {
return getters[key] && getters[key](this.state, this.getters)
}
})
}
},
commit (callbackName, ...options) {
return this.mutations[callbackName] && this.mutations[callbackName](this.state, ...options)
},
dispatch (callbackName, ...options) {
return this.actions[callbackName] && this.actions[callbackName](this, options)
}
}
上面的代码看上去实现了getters,可以通过$store.getters.formatValue来取值了,但它存在两个问题:
现在,我们已经实现了一个并不完美的简易版Vuex。关于最后的两个问题,以及前面代码实现中的问题,欢迎大家留言讨论。