来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的
前言
作为前端开发,我们经常会遇到使用vuex的情况,那么大家有没有好奇vuex是怎么实现的呢,又是如何实现全局状态管理的能力的呢? 下面我来手撕一下vuex的实现,帮助你更好的理解vuex,并帮助你在后续的开发中遇到相关问题可以找到破局之法。
基础知识
在认识vuex之前我们需要了解下,什么是vuex,以及如何使用vuex,实现时涉及的api有哪些?
什么是vuex
vuex是为vue.js创建的一个状态管理工具。
我们可以在vuex中暂存一些组件状态,然后通过vuex使得各个组件获取这些组件状态,并支持在任意组件中更新组件状态,同步到其他的使用了这些组件状态的组件中。
如何使用vuex
在使用vuex之前我们需要初始化一个store文件
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: { // 数据源
count: 1
},
getters: { // store的计算属性
doneTodos (state) {
return state.count + 1
}
},
mutations: { // 更改数据源
increment (state) {
// 变更状态
state.count++
}
},
actions: { // 类似mutations,通过提交mutations更改数据源,但可以包含异步操作
increment (context) {
context.commit('increment')
}
},
modules: {}, // 模块
});
在入口文件处再使用这个store文件
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
new Vue({
router,
store, // 使用初始化的store文件
render: (h) => h(App),
}).$mount("#app");
vuex中主要包括state,getters,mutations,actions等功能,modules是模块但是通常使用情况不多,这里按下不表。
state
state代表的是数据源,我们使用vuex的时候读取的就是这里的参数,比如:
this.$store.state.count // 读取到上面的count字段
getters
getters可以看成是store的计算属性,我们可以定义一个函数将计算后的结果返回出去。这个函数第一个参数是state ,收store中内容也可以接收其他getter作为参数。比如:
this.$store.getters.doneTodos // 读取上面count + 1的结果
mutations
mutations是更改vuex中状态的唯一方法,提供一个事件状态及回调,可以通过调用回调函数的方式去更新组件状态,只能包含同步操作,比如:
this.$store.commit('increment') // 调用mutations的increment,使得count++
actions
actions同样是用于变更vuex状态,但不同的是actions通过提交mutations去更新vuex状态,actions可以包含异步操作,通过dispatch触发,比如:
this.$store.dispatch('increment') // 通过action的increment 去触发 mutations的increment
涉及的知识
Vue.util.defineReactive
defineReactive这个方法接收三个参数
- 第一个参数:要给哪个对象添加属性
- 第二个参数:要给指定的对象添加什么属性
- 第三个参数:要给这个属性添加什么值
通过这个方法,可以快速将一个数据变成双向绑定的数据
Vue.set
set这个方法接收三个参数
-
参数说明:
target
:要更改的数据源(可以是对象或数组)propertyName/index
:属性名或索引value
:新值
可以向响应式对象中加新的property,并保证这个property是响应式的
Vue.use()
我们在使用vuex的时候都知道,我们会先执行vue.use(),这一步做了啥呢?
- 如果入参是一个对象,那就执行这个对象下的
install
方法 - 如果入参是一个函数,那就直接执行这个函数
不妨联想下,为什么我们安装组件库的时候,可以通过vue.use(xxComponents)
这种方式去导入按需引入组件。
实际上我们在vue.use(xxComponents)
的时候,其实是传进去了一个对象。
import Vue from "vue";
import xxComponents from "./xxComponents";
export const xxComponents = {
install() {
Vue.components(xxComponents.name, xxComponents)
}
}
在这里执行了install方法把xxComponents挂载到了全局
new Vue()
我们在使用Vue的时候都会用new Vue去进行初始化操作,包括执行init方法等。
其实new Vue()做了一个很主要的事情,就是将传入的选项挂载到了$option上
,如下:
new Vue({
router,
store, // 使用初始化的store文件
render: (h) => h(App),
}).$mount("#app");
后续store和router都会被挂载到this.$option下
相当于 this.$option.store 可以获取到 我们传入的 store
实现Vuex
初始化Vuex基本结构及挂载Vuex
在上面的例子中我们可以看到
我们这里抛出了一个实例化的Vuex.Store的对象。
import Vue from "vue";
import Vuex from "Vuex";
Vue.use(Vuex);
export default new Vuex.Store({
...
});
然后在入口文件处,将store作为参数传入new Vue中
new Vue({
router,
store, // 使用初始化的store文件
render: (h) => h(App),
}).$mount("#app");
到这里其实Vuex就已经完成了初始化操作。
下面我们来完成Vuex的初始化操作,并介绍初始化的时候Vuex做了什么。
一、创建自己的Vuex文件
需要创建一个Vuex,其中包含用于创建store的类。
myVuex.js
// 创建一个store类,用于初始化Vuex
class store {}
// 抛出自己的vuex
export default {
store,
};
二、创建实例化的Vuex.Store的对象,并传入到new Vue中
因为在使用Vuex之前我们都需要实例化Vuex.Store对象,然后将结果传入到new Vue中,挂载到全局的\$option下。
这里创建一个Store.js 用于抛出 实例化Vuex.Store对象。
Store.js
import myVuex from "./myVuex";
export const myStore = new myVuex.Store({});
在main.js中将Store.js导入,并传入到new Vue中,这样myStore就挂载到全局的$option下。
main.js
import { myStore } from "./Store";
new Vue({
router,
myStore, // 传入初始化的store对象
render: (h) => h(App),
}).$mount("#app");
在这一步之后实例化Vuex.Store对象就挂载到全局的\$option下了。
(重点)
三、创建install方法全局挂载store
因为我们知道Vuex的数据方法都可以通过this.$store去获取,因此我们需要想办法把 this.$store 挂载到全局让所有组件都可以访问到。
而且我们之前已经将 myStore 挂载到 全局的\$option下了
因此这里可以用 Vue.use 的方法, 去实现这个功能。
myVuex新增install方法
因为在Vue.use的时候,如果参数是对象,那么会执行参数的insatll方法,因此我们可以通过insatll方法将this.\$store挂载上去。
这里创建一个install方法,通过Vue.mixin去全局注入到每一个组件中,这样在组件beforeCreate的时候就会去将我们创建myStore对象存放到this.$store中
myVuex.js
import Vue from "vue";
const install = () => {
Vue.mixin({
beforeCreate() {
// Vue创建实例的时候是递归创建的,因此先创建父组件,再创建子组件
// 对于根组件而言,默认 myStore 就存在 this.$options下
if (this.$options && this.$options.myStore) {
this.$store = this.$options.myStore;
}
// 对于子组件而言,默认没有store,此时存储父组件的store即可
else {
this.$store = this.$parent.$store;
}
},
});
};
// 创建一个store类,用于初始化Vuex
class Store {
constructor(options) {
}
}
// 抛出自己的vuex
export default {
install,
Store,
};
main.js中调用Vue.use()
因此我们在main.js中调用下Vue.use(myVuex) ,通过install方法,用Vue.mixin去全局注入到每一个组件中,这样在组件beforeCreate的时候就会去将我们创建myStore对象存放到this.$store中。
main.js
import { myStore } from "./Store";
import myVuex from "./myVuex";
import Vue from "vue";
Vue.use(myVuex) // 调用install方法全局混入
new Vue({
router,
myStore, // 传入初始化的store对象
render: (h) => h(App),
}).$mount("#app");
到这里Vuex的基本初始化就完成了。
四、简单验证
将上述代码运行可以得到结果
展示
打印
其中 a: 1 是在构造函数中随意设置的值
所以通过上面我们可以知道,已经完成了实例化myStore对象的全局挂载,这时候你就可以在任何组件中使用实例化myStore对象下的属性了。
完善store类,实现包括state,getters在内的功能
我们知道使用Vuex之前需要去初始化一个store文件,就是我们在Store.js中做的事情。
回到上面的例子,可以看到初始化文件的时候我们传入了一个配置项,里面包括了state,getters这些,下面我们就会去处理这部分的逻辑。
Store.js
export default new Vuex.Store({
state: { // 数据源
count: 1
},
getters: { // store的计算属性
doneTodos (state) {
return state.count + 1
}
},
mutations: { // 更改数据源
increment (state) {
// 变更状态
state.count++
}
},
actions: { // 类似mutations,通过提交mutations更改数据源,但可以包含异步操作
increment (context) {
context.commit('increment')
}
},
modules: {}, // 模块
});
myVuex中Store是一个类 用于实例化store对象,因此需要去接收传入的参数并进行处理
state
可以在构造函数中完成对state的接收,值得注意的是state中的数据都是响应式的
所以我们需要使用defineReactive将state中的数据转成响应式的
注:这里可能并不一定是用defineReactive将数据转成响应式,这只是其中一种方法
myVuex.js
// 创建一个store类,用于初始化Vuex
class Store {
constructor(options) {
// 将state挂载到store对象中,并转成双向绑定的数据
Vue.util.defineReactive(this, "state", options.state);
}
}
不妨测试下,在子组件中加入一个change事件去改变state的值。
注:mutation是唯一更改state的方式并不确定,其实是可以直接改的,但是严格模式下会报错,不建议这样子干
Vuex源码而言:其底层通过执行 `this._withCommit(fn)` 设置_committing标志变量为true,然后才能修改state,修改完毕还需要还原_committing变量。外部修改虽然能够直接修改state,但是并没有修改_committing标志位,所以只要watch一下state,state change时判断是否_committing值为true,即可判断修改的合法性。
<template>
<div @click="change" class="item">
子组件
{{ this.$store.state.a }}
</div>
</template>
<script>
export default {
methods: {
change() {
this.$store.state.a = 2;
},
},
};
我们可以看到结果如下所示:
我们在点击的时候父子组件同时完成了数据更新。
getters
上面我们已经处理完state了,这里处理下getters的逻辑。
getters相当于state的计算属性,我们可以通过getters对state做一些诸如计算等处理。
因此在接收完store传入的getters后,可以通过Object.defineProperty去对getters对象进行劫持
,当get的时候通过key去匹配
我们接收的getters,并执行对应的key的value函数即可
。
myVuex.js
class Store {
constructor(options) {
Vue.util.defineReactive(this, "state", options.state);
this.initGetters(options);
}
// 初始化Getters
initGetters(options) {
// 获取传入的getters
let initGetters = options.getters || {};
// Store对象上创建一个getters属性
this.getters = {};
Object.keys(initGetters).forEach((key) => {
// 通过 Object.defineProperty 去监听Store的getters 在get的时候执行函数并返回结果
Object.defineProperty(this.getters, key, {
get: () => {
return initGetters[key](this.state);
},
});
});
}
}
可以看到我们在构造函数处调用了initGetters方法,在函数中我们初始化了getters,并对其完成了监听。
同样的我们来验证一下,在store.js中加入getters
Store.js
export const myStore = new myVuex.Store({
state: {
a: 1,
},
getters: {
doneTodos(state) {
return state.a + 1;
},
},
});
更新子组件代码
<template>
<div @click="change" class="item">
子组件
<div>state{{ this.$store.state.a }}</div>
<div>getters{{ this.$store.getters.doneTodos }}</div> // 新增getters
</div>
</template>
<script>
export default {
methods: {
change() {
this.$store.state.a = 2;
},
},
};
可以看到效果如下所示:
getters成功计算出了doneTodos的结果
doneTodos(state) {
return state.a + 1;
},
并且当state更新的时候,getters也同步更新,因此我们的getters就完成了。
mutations
mutations 是我们用来更新Vuex状态的工具,我们可以在里面设置函数,在函数的执行中去修改我们的state的结果。
值得注意的是mutations允许相同的方法名
,会用方法名做key,去映射一个数组,当我们执行的时候就依次遍历数组里面的mutations方法
同样的我们可以创建一个initMutations
方法,在myVuex文件中。
myVuex.js
// 创建一个store类,用于初始化Vuex
class Store {
constructor(options) {
Vue.util.defineReactive(this, "state", options.state);
this.initGetters(options);
this.initMutations(options);
}
// 初始化Getters
initGetters(options) {
...
}
// 初始化mutations
initMutations(options) {
// 获取传入的mutations
let initMutations = options.mutations || {};
// Store对象上创建一个mutations属性
this.mutations = {};
// 遍历传入的mutations
Object.keys(initMutations).forEach((key) => {
// 初始化mutations,将key映射到一个数组上,允许同名key的存在
this.mutations[key] = this.mutations[key] || [];
this.mutations[key].push((payload) => {
// 将对应的函数都push到数组中
initMutations[key](this.state, payload);
});
});
}
}
值得注意的是mutations通过commit去调用。
因此需要额外提供个commit方法,调用时映射到对应的mutations数组,遍历执行即可
commit(key, payload) { // 调用时映射到对应的mutations数组,遍历执行即可
this.mutations[key].forEach((item) => {
item(payload);
});
}
因此我们不妨验证一下,通过mutations去更新state的参数。
store.js
export const myStore = new myVuex.Store({
state: {
a: 1,
},
getters: {
doneTodos(state) {
return state.a + 1;
},
},
mutations: {
// 更改数据源
increment(state, data) {
// 变更状态
state.a = data;
},
},
});
更新子组件代码:(提供了一个mutations,调用之后state的a会变成10)
<template>
<div @click="change" class="item">
子组件
<div>state: {{ this.$store.state.a }}</div>
<div>getters: {{ this.$store.getters.doneTodos }}</div>
</div>
</template>
<script>
export default {
methods: {
change() {
this.$store.commit("increment", 10);
},
},
};
</script>
效果如下:
调用之后state的a成功变成10,getters也成功更新。
注:为什么要用mutations去更新数据,因为mutations可以看成一个数据更新的最小单位操作,通过mutations去更新我们可以知道每一步更新了什么,方便后续的数据处理。
actions
actions 和 mutations 差不多,都是用于更新数据的,不过 actions 用来处理异步的操作,如请求等
。
通过上面的学习我们其实可以知道,如果想改state的值其实不一定要走mutations,但是为什么Vuex要创建一个mutations这样子的东西呢?主要是用于数据的追踪,因此 actions 实际上对某个 mutations 进行了包裹,当目标数据拿到时就走mutations这个最小的操作单位去更新,后续就可以方便追踪数据。
同样的我们可以创建一个initActions
方法,在myVuex文件中,和前面的initMutations很相似,也是key映射到一个数组
myVuex.js
// 创建一个store类,用于初始化Vuex
class Store {
constructor(options) {
Vue.util.defineReactive(this, "state", options.state);
this.initGetters(options);
this.initMutations(options);
this.initActions(options);
}
// 初始化Getters
initGetters(options) {
...
}
// 调用更新
commit(key, payload) {
...
}
// 初始化mutations
initMutations(options) {
...
}
// 初始化actions
initActions(options) {
// 获取传入的actions
let initActions = options.actions || {};
// Store对象上创建一个mutations属性
this.actions = {};
// 遍历传入的actions
Object.keys(initActions).forEach((key) => {
// 初始化actions,将key映射到一个数组上,允许同名key的存在
this.actions[key] = this.actions[key] || [];
this.actions[key].push((payload) => {
// 将对应的函数都push到数组中
initActions[key](this, payload);
});
});
}
}
值得注意的是actions通过dispatch去调用。
因此需要额外提供个dispatch方法,调用时映射到对应的actions数组,遍历执行即可
// 调用异步更新
dispatch(key, payload) {
this.actions[key].forEach((item) => {
item(payload);
});
}
因此我们不妨验证一下,通过actions异步去更新state的参数。
store.js
export const myStore = new myVuex.Store({
state: {
a: 1,
},
getters: {
doneTodos(state) {
return state.a + 1;
},
},
mutations: {
// 更改数据源
increment(state, data) {
// 变更状态
state.a = data;
},
},
actions: {
// 类似mutations,通过提交mutations更改数据源,但可以包含异步操作
increment(context, data) {
setTimeout(() => {
context.commit("increment", data);
}, 5000);
},
},
});
这里提供了actions调用之后在5s后会更新state.a的值
更新子组件代码:(提供了一个actions,调用dispatch之后state的a会变成100)
<template>
<div @click="change" class="item">
子组件
<div>state: {{ this.$store.state.a }}</div>
<div>getters: {{ this.$store.getters.doneTodos }}</div>
</div>
</template>
<script>
export default {
methods: {
change() {
this.$store.dispatch("increment", 100);
},
},
};
</script>
效果如下:
五秒后成功更新了state.a的值
总结
到这里Vuex的基本原理就全部讲完了,这个是简化版的原理讲解,真实的Vuex实际上还做了很多特殊逻辑,对应模块处也做了很多处理,只是平时用模块的场景不多,这里就不展开说说。
Vuex原理
- 首先将store对象通过new Vue挂载到this.$options的store中
- 然后在Vuex抛出的对象中创建一个install方法,
- install方法中全局mixins了生命周期在beforecreate的一个函数。
- 通过这个函数对当前的组件实例对象创建一个this.$store的值
- 如果是根节点就指向this.options的store,如果是子节点就指向父亲的this.options的store,如果是子节点就指向父亲的this.options的store,如果是子节点就指向父亲的this.store
- 通过这种方法实现全局调用一个对象
- 在初始化store对象的时候将传入的state的值转成个响应式类型的数据,就实现了一处更新处处更新。
完整代码
import Vue from "vue";
const install = () => {
Vue.mixin({
beforeCreate() {
// Vue创建实例的时候是递归创建的,因此先创建父组件,再创建子组件
// 对于根组件而言,默认 myStore 就存在 this.$options下
if (this.$options && this.$options.myStore) {
this.$store = this.$options.myStore;
}
// 对于子组件而言,默认没有store,此时存储父组件的store即可
else {
this.$store = this.$parent.$store;
}
},
});
};
// 创建一个store类,用于初始化Vuex
class Store {
constructor(options) {
Vue.util.defineReactive(this, "state", options.state);
this.initGetters(options);
this.initMutations(options);
this.initActions(options);
}
// 初始化Getters
initGetters(options) {
// 获取传入的getters
let initGetters = options.getters || {};
// Store对象上创建一个getters属性
this.getters = this.getters || {};
Object.keys(initGetters).forEach((key) => {
// 通过 Object.defineProperty 去监听Store的getters 在get的时候执行函数并返回结果
Object.defineProperty(this.getters, key, {
get: () => {
return initGetters[key](this.state);
},
});
});
}
// 调用更新
commit(key, payload) {
this.mutations[key].forEach((item) => {
item(payload);
});
}
// 初始化mutations
initMutations(options) {
// 获取传入的mutations
let initMutations = options.mutations || {};
// Store对象上创建一个mutations属性
this.mutations = {};
// 遍历传入的mutations
Object.keys(initMutations).forEach((key) => {
// 初始化mutations,将key映射到一个数组上,允许同名key的存在
this.mutations[key] = this.mutations[key] || [];
this.mutations[key].push((payload) => {
// 将对应的函数都push到数组中
initMutations[key](this.state, payload);
});
});
}
// 调用异步更新
dispatch(key, payload) {
this.actions[key].forEach((item) => {
item(payload);
});
}
// 初始化actions
initActions(options) {
// 获取传入的actions
let initActions = options.actions || {};
// Store对象上创建一个mutations属性
this.actions = {};
// 遍历传入的actions
Object.keys(initActions).forEach((key) => {
// 初始化actions,将key映射到一个数组上,允许同名key的存在
this.actions[key] = this.actions[key] || [];
this.actions[key].push((payload) => {
// 将对应的函数都push到数组中
initActions[key](this, payload);
});
});
}
}
// 抛出自己的vuex
export default {
install,
Store,
};
转载自:https://juejin.cn/post/7230078756626743356