我们都知道Vue框架是通过数据驱动的,所以数据的重要性不言而喻,那么有哪些数据需要管理又是如何进行管理的呢?本节我们就来聊一聊Vue3中的数据管理。
哪些数据需要管理?
在我们的前端项目中,都会有哪些数据呢?
从组件的角度来看,我们可以将数据划分为组件内部的数据和组件之间共用的数据。
组件内部的数据很好理解,我们在介绍组件化开发时已经定义了大量这样的数据,比如控制组件的显示隐藏、弹框的标题、内容等等,这些数据一般都只在组件内部使用,与我们的业务逻辑关系不大,只是用来控制组件的各个状态。
组件之间的共用数据一般指的是与业务逻辑相关的数据,需要在各个组件之间流通共享,比如用户登录时的用户名信息,可能在多个组件中都需要显示用户名,某个地方修改用户名后,其他组件中也要同步的更新显示。
组件内的数据就交给各个组件自己去管理就好了,反正也不会对外界产生影响,所以我们主要关注的还是组件之间共用的数据要如何管理。
最容易想到的方法就是全局变量了,既然多个组件都要使用,在顶层挂载到全局变量中不就好了?
window.usename = 'xxx'
这样做确实可以在所有组件中都能访问到usename
变量,但是修改用户名后却无法做到数据同步,再想一下,要想做到数据同步我们需要的不就是一个响应式的数据嘛,联想到响应式章节中的内容,我们将一个响应式的数据绑定到全局不就解决了嘛,这就是状态管理工具Vuex的基本实现原理。
Vuex在Vue3中的使用
那么Vuex究竟是用来干什么的呢?
当我们项目中的业务数据越来越多时,不同的开发人员可能都会有自己定义的数据和更新数据的方法,同一个数据可能A定义了,B也定义了,不但资源浪费了,数据的同步也变的很困难。还有种可能,同一个数据,A用方法1去更新,B用方法2去更新,一旦数据出现了问题,数据的处理流程就非常难以跟踪。
所以我们需要对数据进行集中定义管理,统一对外暴露更新方法——这就是Vuex做的事情。
下面我们就来看一下在Vue3中是如何使用Vuex的。
- 安装Vuex。
npm install vuex@next --save
- 在store文件夹下新建index.js,创建Store实例。
import { createStore } from 'vuex';
const store = createStore({
state() {
return {
count: 0
}
}
})
export default store;
我们来定义一个共享的数据count,并新增一个修改数据的方法。
import { createStore } from 'vuex';
const store = createStore({
state() {
return {
count: 0
}
},
mutations: {
add (state) {
state.count++
}
},
})
export default store;
从上面的代码中可以看到,我们在mutations属性中新增了add方法,每次执行会让count的值加一。
- 在main.js中注入store。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router';
+ import store from './store';
...
const app = createApp(App);
+ app.use(router).use(store).mount('#app');
- 在组件中通过useStore引入store实例,访问count数据并触发count的更新事件。
<template>
<div>{{count}}</div>
<button @click="add">加1</button>
</template>
<script setup>
import {useStore} from 'vuex';
const store = useStore();
let count = computed(() => store.state.count)
function add () {
// 调用mutations中的add方法
store.commit('add');
}
</script>
我们如果在另一个组件中也使用了store.state.count
,可以发现count的值会被同步修改。
这就是Vuex的基础用法,和vue-router一样,我们明白原理和使用方法后,来尝试实现下简易版本的Vuex。
自定义Vuex的实现
学习过vue-router章节的同学应该都很清楚了,我们实现自定义的第一步就是根据暴露出来的方法来搭建基础的框架。
我们来新建一个newStore.js文件,在使用Vuex的过程中,我们一共使用了createStore
和useStore
两个方法,createStore方法返回一个实例,所以newStore.js中有如下内容。
// 根据参数创建store实例
const createStore = (params) => {
// 返回空的store实例
return {
}
}
// 返回store实例
const useStore = () => {
}
export {
createStore,
useStore
}
与createRouter方法一样,createStore方法在使用的时候传入的参数是个对象,并返回一个store实例,我们这里同样先返回一个空的对象。
我们看一下调用createStore方法时传入的参数长什么样:
createStore({
state() {
return {
count: 0
}
},
mutations: {
add (state) {
state.count++
}
},
})
传入的参数是个对象,对象中有state和mutations,所以我们将这两个内容都保存到我们的store实例中:
const createStore = (params) => {
return {
// 传入的state是个方法,我们真正需要的是返回的结果
state: params.state(),
mutations: params.mutations,
}
}
上面我们说过要将一个响应式的数据绑定到全局,Vuex中的数据都保存在state中,所以我们的state必须是个响应式对象,params.state()返回的是个复杂数据类型,那么我们就使用reactive来定义:
const createStore = (params) => {
return {
// state声明成响应式
state: reactive(params.state()),
mutations: params.mutations,
}
}
同样在app.use的时候,我们需要提供一个install方法,将store实例注册到全局。
import { reactive, provide, inject } from "vue";
...
const createStore = (params) => {
return {
state: reactive(params.state()),
mutations: params.mutations,
// 增加install方法
install(app) {
app.provide('STORE', this);
}
}
}
createStore方法我们基本上就实现了,还有个useStore方法,我们在组件中调用useStore方法时可以获取到store实例,而实例已经被我们注册到app.provide('STORE', this)
全局了,那么使用inject去获取就可以了。
// 返回store实例
const useStore = () => {
return inject('STORE');
}
使用useStore获取到实例后,我们还需要有个commit方法来更新数据store.commit('add')
,所以我们实例下还有个commit方法,接收mutations下的方法名作为参数。
const createStore = (params) => {
return {
state: reactive(params.state()),
mutations: params.mutations,
// commit用来执行mutations下的方法
commit(fun, payload) {
// mutations下的方法接受state作为参数
this.mutations[fun](this.state, payload);
},
install(app) {
app.provide('STORE', this);
}
}
}
好了,简单版本的Vuex我们就已经完成了,完整代码如下:
import { reactive, inject } from "vue";
const createStore = (params) => {
return {
state: reactive(params.state()),
mutations: params.mutations,
// commit用来执行mutations下的方法
commit(fun, payload) {
// mutations下的方法接受state作为参数
this.mutations[fun](this.state, payload);
},
install(app) {
app.provide('STORE', this);
}
}
}
const useStore = () => {
return inject('STORE');
}
export {
createStore,
useStore
}
我们修改下store/index.js文件和组件中对Vuex的引用,替换成newStore。
- import { createStore } from 'vuex';
+ import { createStore } from '../newStore';
...
这样我们就完成了自定义的Vuex,页面效果也与Vuex保持了一致,大家也可以自己去尝试下继续实现actions的功能。
Vuex的更多应用
除了上面的基本功能,Vuex还有更多更复杂的功能,我们在搭建Vue3项目的章节中提到过:
store是负责数据管理的地方。我们的业务逻辑都会在store中实现,比如业务数据接口的调用就会放在store中去做,组件内包含的都是与业务无关的内容。
与本节内容结合,store中保存的是与业务逻辑相关的数据,而与业务逻辑相关的请求,我们也在store中进行调用,与mutations下的方法不同的是,mutations下的方法都是同步执行,业务的接口请求是异步的,需要用到actions。
mutations: {
set(state, count) {
state.count = count;
}
},
actions: {
getCount ({commit}) {
// 异步接口获取count值
}
}
我们在mutations下新增一个set方法,用来设置count的值,actions下的getCount方法用来调用异步接口,我们使用promise模拟一个异步操作。
function getCountApi () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3)
}, 2000);
})
}
actions: {
getCount ({commit}) {
getCountApi().then((res) => {
commit('set', res)
})
}
}
actions不能直接操作数据,但是actions的参数解构可以得到commit方法,通过commit触发mutations下的set方法来更新数据,引用Vuex官网的规则如下:
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
可能有同学习惯在组件中去调用接口数据,拿到返回结果后直接渲染到页面上,有需要同步的数据再保存一份到store中,相比之下,将接口请求封装到store中,业务数据只保存在store中,组件里面只进行相关方法的调用会划分得更加清晰。
这样一来,业务数据不会与组件本身的数据混合在一起,并且只需要保存一份,组件只需要关注自己的逻辑,与业务方解耦。这两种模式大家可以认真的想一想,将业务请求放到store中还有什么其他的好处?
说回Vuex本身,我们将store与业务强关联了,而业务我们会根据需求进行模块的划分,比如一个商城的网页就会有用户模块、商品模块、订单模块等等,所以我们的store也需要进行模块的划分,与我们的业务模块相对应的进行数据管理。
那么我们怎么根据模块来管理store呢?
在Vuex中,提供了Module的概念,我们可以根据模块来创建store,比如我们同时存在用户模块和商品模块,需要如下创建store。
// useStore.js 用户模块
const useStore = {
state: () => {
return {
name: '小明',
age: 18
}
},
mutations: {
changeUseName(state, name) {
state.name = name;
},
changeUseAge(state, age) {
state.age = age;
}
},
getters: {
birthday: (state) => {
return new Date().getFullYear() - state.age
}
}
}
export default useStore;
// goodsStore.js 商品模块
const goodsStore = {
state: () => {
return {
goodsName: '商品1',
goodsCount: 10
}
},
mutations: {
setGoodsCount(state, count) {
state.goodsCount = count;
}
},
}
export default goodsStore
从上面的代码中,我们新增了两个store,在useStore中,存储了用户的姓名和年龄,提供了修改姓名和年龄的方法,还有一个新增的getters属性,getters的作用与computed类似,可以根据state中的值,返回计算后的响应式结果。goodsStore中则保存了商品相关的信息,这样在两个文件中,我们就分模块定义了store。
在index.js中引入各个模块:
import { createStore } from 'vuex';
+ import useStore from './useStore';
+ import goodsStore from './goodsStore';
const store = createStore({
state() {
...
},
mutations: {
...
},
actions: {
...
},
// 引入模块
modules: {
useStore,
orderStore
}
})
我们打印下store实例,看看引入的模块放在了哪里。
从控制台打印的结果可以看到,modules引入的模块与state下定义的变量在同一级,这里我们做个区分,在createStore下定义的变量和方法,我们划分到根store下,每个模块中定义的在模块store下,那我们要怎么调用模块store下的数据和方法呢?
获取模块store下的state需要携带上模块的名称:
store.state.[模块名称].[变量名称]
例:store.state.useStore.name
调用模块store下的getters、mutations和actions与调用根store一致就不多提了。
模块与根store的state在同一级中,如果模块与state存在同名情况会不会互相覆盖呢?
答案是会的,同名的模块会覆盖掉根state下的内容,我们可以通过添加命名空间来避免同名之间的覆盖。
const useStore = {
// 添加命名空间
namespaced: true,
...
}
添加了命名空间后,在访问模块getters、mutations和actions的时候,就需要添加名称来区分具体是哪个命名空间下的方法。
store.getters['useStore/birthday']
store.commit('useStore/name', '小红');
store.dispatch('useStore/xxx', '参数')
总结
本节中我们介绍了Vue3中的数据管理,首先大家需要区分数据类型,与外部业务无关,只关系到组件本身状态的数据放到组件内部,通过ref,reactive管理。与业务逻辑强关联,需要在多个组件中被使用的数据可以通过Vuex来进行管理。
然后我们介绍了Vuex的基本使用和原理,并手动实现了一个简单的Vuex,进一步加深对原理的理解。还说明了mutations和actions的使用原则,mutations中是对数据进行同步的操作,异步的操作要放到actions中,所以我们建议将业务请求的api都放到store中去处理,并说明了这种模式的优点。
最后结合实际的业务情形,对Vuex的扩展使用进一步介绍,丰富了Vuex在实战中的更多应用。