手把手讲解:Vuex 剖析与简单实现
目录
- install 方法
- Store 类
- State
- Mutations
- Actions
- 小结一下
- Modules
闲话不多说,让我们开始实现一个简单的 Vuex
首先,回想一下 Vuex 的使用方法,这里给出一个简单的使用例子:
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
a: 1
},
getters: {
aPlus(state){
return state.a + 1
}
},
mutations: {
addA(state, payload){
state.a += payload
}
},
actions: {
asyncAddA({commit, state}, payload){
setTimeout(() => {
commit('addA', payload)
}, 1000)
}
},
modules: {
a: {
modules: {
c: {
state: {
c1: 'c1'
}
}
}
},
b: {
state: {
b: 1
},
mutations: {
bPlus(state, payload){}
},
getters: {
bPlus(state){}
}
}
}
})
// 根组件实例
new Vue({
store
})
可以看到,'vuex'
导出来的应该是一个对象,上面有至少两个属性:install
和 Store
,install
方法在 Vue.use(Vuex)
时会执行,Store
是一个类,我们先把大体的架子写出来:
let Vue // 后面要用到
class Store{
constructor(options){
// ...
}
}
function install(){}
export {
install,
Store
}
1. install 方法
接下来,我们看一下 install
方法的实现:
用过 Vuex 的都知道:当在根组件注入 store
后,每个子组件都能访问到这个 store
,这其实就是 install
帮我们做到的,先来看看这个方法的实现:
let Vue
function install(_Vue){
// 重复安装 Vuex
if(Vue) throw new Error('Vuex instance can only exist one!')
Vue = _Vue
// 将 vm.$options.$store 注入到每个子组件的 vm.$store 中
Vue.mixin({
beforeCreate() {
if(this.$options && this.$options.store){
this.$store = this.$options.store
} else {
this.$store = this.$parent && this.$parent.$store
}
}
})
}
Vue.use(Vuex)
会将 Vue
传入 install
方法,这个全局变量可以用来判断是否已经被 use
过了,重复则报错。
如果没有重复,使用 Vue.mixin
混入一段代码,这段代码会在 beforeCreate
这个钩子函数执行之前执行
核心代码是这几行:
if(this.$options && this.$options.store){
this.$store = this.$options.store
} else {
this.$store = this.$parent && this.$parent.$store
}
this
指向当前组件实例,如果当前组件实例的 $options
上有 store
属性,说明该实例是注入 store
的根实例,直接往 $store
上挂载 store
反之,else
中会去检查当前实例的父组件: $parent
上有没有 $store
,有则在实例上挂载该属性
因为组件的生命周期顺序是:父组件先创建,然后子组件再创建,也就是说子组件执行 beforeCreate
钩子函数时,父组件的 store
已经注入了,所以可以实现循环注入。
2. Store 类
接下来,我们实现一下 Store
类:
Store 中有四部分需要我们实现:State、Getter、Mutation、Action
,我们先来实现 State
:
(1) State
通过 $store.state.xxx
即可访问到 vuex
中的 state
状态
class Store{
constructor(options){
this._state = new Vue({ data:options.state })
}
get state(){
return this._state
}
}
这里我们不直接通过 store.state
提供访问,而是通过访问器形式提供访问,可以避免 state
被外部直接修改。
另外要注意的一个点是:我们借用 Vue
让 state
变成响应式数据了。这样的话,当 state
变化的时候,依赖的地方都能得到更新通知
以上,我们简单实现了 State
状态
(2) Getters
接下来实现 Getters
,Getters
是通过 store.getters.x
的形式访问的
首先,将 options
中的 getters
遍历,将每个属性逐个挂载到 store.getters
上。
因为 getters
的特点是:访问属性,返回函数执行,很容易想到可以用访问器实现。
constructor(options){
// ...
this.getters = {}
Object.keys(getters).forEach(k => {
Object.defineProperty(this._getters, k, {
get: ()=>{ // 箭头函数保证 this 能够访问到当前 store 实例
return getters[k](this.state)
}
})
})
}
(3) Mutations
mutations
通过 store.commit(type, payload)
触发,commit
内部会通过 type
取到 this._mutations
上对应的 mutation
,将 payload
传入并执行
我们需要将 options
上的 mutations
上进行遍历,定义到 this._mutations
上,之所以这样重新定义一遍是为了能够在 mutation
函数外面封装一层,方便传入 state
constructor(options){
// ...
this._mutations = {}
Object.keys(mutations).forEach(k => {
this._mutations[k] = (payload) => {
mutations[k](this.state, payload) // 注意 state 参数的传入
}
})
}
commit = (type, payload) => { // 箭头函数,保证 this 指向 store
return this._mutations[type](payload)
}
(4) Actions
actions
的实现和 mutations
相似,就不再赘述了。
不同点在于:
(1) 执行回调传入的参数不同
(2) dispatch
返回的应该是一个 Promise
constructor(options){
// ...
this._actions = {}
Object.keys(actions).forEach(k => {
this._actions[k] = (payload) => {
actions[k](this, payload) // 这里直接将整个 store 传入了
}
})
}
dispatch = (type, payload) => {
return new Promise((resolve, reject) => {
try{
resolve(this._actions[type](payload))
} catch (e){
reject(e)
}
})
}
(5) 小结一下
OK,现在让我们把上面的这些代码片段写到一块
let Vue
class Store{
constructor(options){
this._state = new Vue({
data: { state: options.state }
})
// 生成 Getters、Mutations、Actions
this.generateGetters(options.getters)
this.generateMutations(options.mutations)
this.generateActions(options.actions)
}
generateGetters(getters = {}){
this.getters = {}
Object.keys(getters).forEach(k => {
Object.defineProperty(this.getters, k, {
get: ()=>{
return getters[k](this.state)
}
})
})
}
generateMutations(mutations = {}){
this._mutations = {}
Object.keys(mutations).forEach(k => {
this._mutations[k] = (payload) => {
mutations[k](this.state, payload)
}
})
}
generateActions(actions = {}){
this._actions = {}
Object.keys(actions).forEach(k => {
this._actions[k] = (payload) => {
actions[k](this, payload)
}
})
}
commit = (type, payload) => {
return this._mutations[type](payload)
}
dispatch = (type, payload) => {
return new Promise((resolve, reject) => {
try{
resolve(this._actions[type](payload))
} catch (e){
reject(e)
}
})
}
get state(){
return this._state.state
}
}
function install(_Vue){
if(Vue) throw new Error('Vuex instance can only exist one!')
Vue = _Vue
Vue.mixin({
beforeCreate() {
if(this.$options && this.$options.store){
this.$store = this.$options.store
} else {
this.$store = this.$parent && this.$parent.$store
}
}
})
}
export default {
install,
Store
}
(6) Modules
接下来,我们要实现 Modules
。准备好,前面的代码要发生变动了
我们先贴一下前面的使用例子:
new Vuex.Store({
// ...
modules: {
a: {
modules: {
c: {
state: {
c1: 'c1'
}
}
}
},
b: {
state: {
b: 1
},
mutations: {
bPlus(state, payload){}
},
getters: {
bPlus(state){}
}
}
}
})
先总结一下各个属性的访问方式:
对于 c
模块中的状态 c1
,访问方式为:store.state.a.c.c1
对于模块的 getters
、mutations
、actions
,都被定义到 store
上了,通过形如 store.getters.xxx
这种方式访问
好了,现在目标明确了,接下来就把上面提到的两个点实现:
首先,我们需要将 store
上 getters
、mutations
、actions
进行初始化成空对象
我们不再需要用 generateGetters
等方法对 getters、mutations、actions
这些属性进行处理了,这部分代码可以删掉了。在 installModule
的过程中我们会对这些属性进行统一处理。
不过不用担心,我们前面讲到的各个属性的处理方式的核心代码,后面依旧用得上!
constructor(options){
this._state = new Vue({
data: { state: options.state }
})
// init
this.getters = {}
this.mutations = {}
this.actions = {}
// this.generateGetters(options.getters)
// this.generateMutations(options.mutations)
// this.generateActions(options.actions)
let modules = options // 这里取到的 options,实际上就是根模块
installModule(this, this.state, [], modules) // 对各个子模块进行处理的函数,重点关注!
}
重点关注一下 installModule
方法,在这个方法里我们实现了 state、getters
等属性的收集处理
先解释一下传给这个函数的四个参数:
-
store
实例 -
store
上的state
-
path
:在installModule
中需要递归来安装处理子模块,所以用path
数组来表示模块的层级关系。数组的最后一位为当前要处理的模块的模块名,最后一位前面的都是当前模块的祖先模块。举个栗子:如果是根模块,传入的path
为[]
,如果传入[a, c]
,说明当前处理模块名为c
,模块层级为:根模块 > a模块 > c模块
-
module
是当前要处理的模块
我们先来把这个函数的基本架子写好,因为模块的嵌套层数是未知的,所以必须用递归进行模块处理安装。
function installModule(store, state, path, module) {
// 如果当前模块还有 modules(即还有子模块),递归进行模块安装
if(module.modules){
// 遍历当前模块的子模块
eachObj(module.modules, (modName, mod)=>{
// 将传入的 path 拼接当前要处理的的模块名,得到模块层级数组并传入,进行子模块安装
installModule(store, state, path.concat(modName), mod)
})
}
}
注意这里的 path.concat(modName)
是为了将祖先模块名拼接到 path
中,这个模块层级数组在后面需要用到,我们后面会讲
另外,这里把遍历对象的方法封装到了 eachObj
中,让代码看起来简洁一点:
function eachObj(obj, callback){
Object.keys(obj).forEach(k => {
callback(k, obj[k])
})
}
接下来我们把模块的 getters、mutations、actions
挂载到根模块的相应属性上,这三者的处理方式大同小异,要注意的点就是:
同名 mutations、actions
不会被覆盖,他们会被依次执行。所以 store.mutations.xxx
和 store.actions.xxx
应该是一个数组,但 getters
不允许同名,直接挂载到 store.getters
上即可
function installModule(store, state, path, module) {
let getters = module.getters || {}
// 将模块的 getters 定义到 store.getters 上
eachObj(getters, (k, fn) => {
Object.defineProperty(store.getters, k, {
get(){
return fn(module.state) // 注意这里传入的 state 是当前模块的局部 state
}
})
})
let mutations = module.mutations || {}
eachObj(mutations, (k, fn) => {
const rootMut = store.mutations // 根模块的 mutations
// 先检查 rootMut[k] 是否被初始化了,没有的话初始化为空数组
if(!rootMut[k]) {
rootMut[k] = []
}
rootMut[k].push((payload)=>fn(module.state, payload))
})
// actions 类似 mutations 的实现
let actions = module.actions || {}
eachObj(actions, (k, fn) => {
const rootAct = store.actions
if(!rootAct[k]){
rootAct[k] = []
}
rootAct[k].push((payload)=>fn(store, payload))
})
if(module.modules){ // 递归处理模块
eachObj(module.modules, (modName, mod)=>{
installModule(store, state, path.concat(modName), mod)
})
}
}
接下来我们要实现 state
的挂载,这部分代码相对上面难理解一点:
function installModule(store, state, path, module) {
let parent =
path.slice(0, -1).reduce((state, cur) => {
return state[cur]
}, state)
Vue.set(parent, path[path.length - 1], module.state)
// 省略处理 getters、mutations、actions 的代码
// 省略递归处理模块的代码
}
为了保证后面的思路不会乱掉,这里还是要再强调一下 path
的含义:
path
数组来表示模块的层级关系,如果是根模块,传入的 path
为 []
,如果传入 [a, c]
,说明当前处理模块名为 c
,模块层级为:根模块 > a模块 > c模块
接下来剖析代码:
path.slice(0, -1)
返回去除了最后一个元素的数组(注意:数组本身不会被修改),这个数组剩下的元素其实就是当前处理模块的祖先模块们,将这个数组进行 reduce
处理,累计值初始为 state
(也就是 store.state
),最后返回父链。
举个栗子:
如果 path
为 []
,即当前处理模块为根模块,经过 reduce
后返回 state
如果 path
为 [a, c]
,经过 reduce
后返回 state.a
,最终 c
模块的 state
会被挂载在 state.a.c.state
上面
挂载代码如下:
Vue.set(parent, path[path.length - 1], module.state)
对于上面的例子,即:Vue.set(state.a, 'c', c.state)
另外,使用 Vue.set
是为了保证数据响应式。
以上,我们的简易版 Vuex
就实现完了。
另外,值得一提的是:在我们的代码中,直接通过 let modules = options
取得了根模块,而在 Vuex
源码中实际还有一个模块收集的过程,这个方法会将模块收集成一个如下的树结构
{
_raw: {...},
_children: {...},
state: {...}
}
_raw
表示 options
传入的模块的原生形式,_children
中包含了该模块的子模块,state
为该模块的 state
状态
感兴趣的话可以翻阅 Vuex
源码,或者看看笔者的实现。
感谢阅读,若以上讲述有所纰漏还望指正。