在开发中,我们会让应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就 称之为是状态管理。
在前面我们是如何管理自己的状态呢?
在Vue开发中,我们使用组件化的开发方式:
在组件中我们定义data或者在setup中返回使用的数据,这些数 据我们称之为state;
在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View;
在模块中我们会产生一些行为事件,处理这些行为事件时,有可能 会修改state,这些行为事件我们称之为actions;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TS1Imi1h-1668407537860)(assets/image-20221103054848920.png)]
JavaScript开发的应用程序,已经变得越来越复杂了.JavaScript需要管理的状态越来越多,越来越复杂:
然而,当我们有多个组件共享一个共同的状态时,就没有这么简单了:单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态;
- 来自不同视图的行为需要变更同一状态;
对于情景 1,一个可行的办法是将共享状态“提升”到共同的祖先组件上去,再通过 props 传递下来。然而在深层次的组件树结构中这么做的话,很快就会使得代码变得繁琐冗长。这会导致另一个问题:Prop 逐级透传问题。
Prop 逐级透传问题:
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KG1JAzsp-1668407537862)(assets/image-20221103053130854.png)]
注意,虽然这里的
组件可能根本不关心这些 props,但为了使
能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。
对于情景 2,我们经常发现自己会直接通过模板引用获取父/子实例,或者通过触发的事件尝试改变和同步多个状态的副本。但这些模式的健壮性都不甚理想,很容易就会导致代码难以维护。
而且管理不断变化的state本身是非常困难的:
一个更简单直接的解决方案是抽取出组件间的共享状态,放在一个全局单例中来管理。这样我们的组件树就变成了一个大的“视图”,而任何位置上的组件都可以访问其中的状态或触发动作。通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪;这就是Vuex背后的基本思想.
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qL4MfW06-1668407537863)(assets/image-20221105093013001.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHju7cR9-1668407537865)(assets/image-20221103053957771.png)]
每一个 Vuex 应用的核心就是 store,里面又包括:
(1)state(数据):用来存放数据源,就是公共状态;
(2)getters(数据加工):有的时候需要对数据源进行加工,返回需要的数据;
(3)actions(事件):要执行的操作,可以进行同步或者异步事件
(4)mutations(执行):操作结束之后,actions通过commit更新state数据源
(5)modules:使用单一状态树,致使应用的全部状态集中到一个很大的对象,所以把每个模块的局部状态分装使每一个模块拥有本身的 state、mutation、action、getters、甚至是嵌套子模块;
vuex的工作流程就是:
(1)通过dispatch去提交一个actions,
(2) actions接收到这个事件之后,在actions中可以执行一些异步|同步操作,根据不同的情况去分发给不同的mutations,
(3)actions通过commit去触发mutations,
(4)mutations去更新state数据,state更新之后,就会通知vue进行渲染。
第一步: 安装vuex
npm install vuex
- 1
第二步: 在src目录下创建一个store目录,在该目录下创建index.js文件,用于创建Store对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vrZeYtHX-1668407537867)(assets/image-20221105112705487.png)]
//用于创建Vuex的核心对象Store //1.导入 import {createStore } from 'vuex' //2.创建Store对象 const store = createStore({ //state 用于存储数据 state(){ return { count:1, } }, //actions 用于响应组件中的事件 actions:{ }, //mutations 用于操作数据 mutations:{ } }); //3.暴露出store对象 export default store;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
第三步: 在main.js 使用store对象
import store from './store'; app.use(store);
- 1
- 2
第四步: 在 Vue 组件中, 可以通过
this.$store
访问store实例<template> <h2>{{$store.state.count}}h2> template> <script> export default { name: 'App', data() { return { }; }, components: { }, methods: { }, mounted() { console.log(this.$store.state.count); } }; script> <style lang="css" scoped> style>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
接下来,我们使用一个案例,熟悉vuex的基本使用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WSLsKQqR-1668407537868)(assets/1.gif)]
CalcSum.vue
<template>
<div>
<h2>和:{{$store.state.sum}}h2>
<input type="number" length="3" v-model="num"/>
<input type="button" value="+" @click="add"/>
<input type="button" value="-" @click="sub"/>
div>
template>
<script>
export default {
name: 'CalcSum',
data() {
return {
num:1,
};
},
mounted() {
},
methods: {
add(){
//通过dispatch去提交一个action
this.$store.dispatch('increment', this.num);
},
sub(){
this.$store.dispatch('decrement', this.num);
},
},
};
script>
<style lang="css" scoped>
style>
App.vue
<template>
<CalcSum/>
template>
<script>
import CalcSum from './components/CalcSum.vue'
export default {
name: 'App',
data() {
return {
num:1,
};
},
components: {
CalcSum
},
methods: {
},
mounted() {
console.log(this.$store.state.sum);
}
};
script>
<style lang="css" scoped>
style>
store/index.js
//用于创建Vuex的核心对象Store
//1.导入
import {createStore } from 'vuex'
//2.创建Store对象
const store = createStore({
//state 用于存储数据
state(){
return {
sum:0,
}
},
//actions 用于响应组件中的事件
actions:{
increment:function(context,value){
//actions通过commit去触发mutations
context.commit("INCREMENT",value)
},
decrement(context,value){
context.commit("DECREMENT",value)
}
},
//mutations 用于操作数据
mutations:{
//mutations去更新state数据
INCREMENT(store,value){
store.sum +=value;
},
DECREMENT(store,value){
store.sum -= value;
}
}
});
//3.暴露出store对象
export default store;
注意:
每次我们在组件获取vuex的状态数据需要写
{{$store.state.属性名}}
,太复杂了,由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。computed: { count () { return this.$store.state.sum } }
- 1
- 2
- 3
- 4
- 5
{{count}}
- 1
Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
单一状态树的优势:
注意:
使用 Vuex 并不意味着你需要将**所有的**状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
- 1
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性,让你少按几次键:
比如我们在vuex的state中存储了多个数据,在某个组件我们需要获取到这些数据,这时就会看到如下代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gSXVJTB8-1668407537869)(assets/image-20221107060923278.png)]
我们发现$store.state
一直在重复写,那我们可以把这些设置为计算属性:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nqNUhTiF-1668407537870)(assets/image-20221107061214983.png)]
但是我们还是发现计算属性中,也在重复写this.$store.state
。vuex提供了mapState
辅助函数帮助我们生成计算属性.
computed:mapState({
//箭头函数可使代码更简练
name: state => state.name,
// 传字符串参数 'name' 等同于 `state => state.name`
//给name定义一个别名
xingming: 'name',
//扩展函数,可以对name进行操作
namePlusLocalState (state) {
return this.hello + state.name;
},
sex:state=>state.sex,
age:state=>state.age,
}),
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState
传一个字符串数组
computed:mapState(["name","sex","age"]),
mapState
函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed
属性。但是自从有了对象展开运算符,我们可以极大地简化写法:
//对象展开运算符
computed:{
//局部计算属性
localComputed () { /* ... */ },
...mapState(["name","sex","age"]),
},
某些属性我们可能需要经过变化后来使用,这个时候可以使用getters:Vuex 允许我们在 store 中定义“getters”(可以认为是 store 的计算属性)
上图我们中函数Getter 接受 state 作为其第一个参数:
getters:{
totalPrice(state){
let total= 0;
state.books.forEach((book)=>{
total += book.count*book.price;
})
return total;
}
},
我们在其他组件可以访问Getter 对象,Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
<div>
<h2>总价:{{$store.getters.totalPrice}}h2>
div>
getters可以接收第二个参数
getters:{
totalPrice(state,getters){
let total= 0;
state.books.forEach((book)=>{
total += book.count*book.price;
})
return total+","+getters.myName;
},
myName(state){
return state.name;
}
},
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters:{
totalPrice(state,getters){
let total= 0;
state.books.forEach((book)=>{
total += book.count*book.price;
})
return total+","+getters.myName;
},
myName(state){
return state.name;
},
getBookById(state){
return (id)=>{
return state.books.find(book=>
book.id === id
);
}
}
},
访问:
<h2>书籍信息:{{$store.getters.getBookById(3)}}h2>
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
<template>
<div>
<h2>总价:{{totalPrice}}h2>
<h2>书籍信息:{{getBookById(3)}}h2>
div>
template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Book',
computed:{
...mapGetters(['totalPrice','getBookById']),
},
};
script>
<style lang="scss" scoped>
style>
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:你可以向 store.commit
传入额外的参数,即 mutation 的载荷(payload):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwD7qtlY-1668407537873)(assets/image-20221107115148803.png)]
你不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:“当触发一个类型为 INCREMENT
的 mutation 时,调用此函数。”要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法。
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
increment:function(context,value){
context.commit("INCREMENT",{
value
});
},
INCREMENT(store,payload){
store.sum +=payload.value;
},
提交 mutation 的另一种方式是直接使用包含 type
属性的对象:
store.commit({
type: 'increment',
amount: 10
})
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变:
你可以在组件中使用 this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations
辅助函数将组件中的 methods 映射为 store.commit
调用(需要在根节点注入 store
)
<template>
<div class="box">
<h2>和:{{$store.state.sum}} --{{count}}h2>
<input type="number" length="3" v-model="num"/>
<input type="button" value="+" @click="add({value:num})"/>
<input type="button" value="-" @click="sub({value:num})"/>
<Counter/>
div>
template>
<script>
import {mapMutations } from 'vuex'
export default {
name: 'CalcSum',
data() {
return {
num:1,
};
},
mounted() {
},
methods: {
/*
add(){
//通过dispatch去提交一个action
this.$store.dispatch('increment', this.num);
console.log(this.$store.state.sum)
//强制刷新页面
//this.$forceUpdate();
},
sub(){
this.$store.dispatch('decrement', this.num);
//this.$forceUpdate();
},*/
...mapMutations({
add:'INCREMENT',
sub:'DECREMENT'
}),
},
computed: {
count () {
return this.$store.state.sum
}
}
};
script>
<style lang="css" scoped>
.box{
width:70%;
margin: auto;
}
style>
如果组件的事件函数名与Mutation中修改状态的函数名一样,我们可以简写:
...mapMutations(['INCREMENT','DECREMENT']),
- 1
mutation重要原则:
一条重要的原则就是要记住 mutation 必须是同步函数,这是因为devtool工具会记录mutation的日记;每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;但是在mutation中执行异步操作,就无法追踪到数据的变化;
所以Vuex的重要原则中要求 mutation必须是同步函数;
如果我们希望在Vuex中发送网络请求的话需要如何操作呢?那我们就使用Action
Action 类似于 mutation,不同在于:
让我们来注册一个简单的 action:
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。
如何使用action呢?进行action的分发:
分发使用的是 store 上的dispatch函数;
同样的,它也可以携带我们的参数。
也可以以对象的形式进行分发。
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
action也有对应的辅助函数:使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
)
ttps://vuex.vuejs.org/zh/guide/modules.html) 时,你就知道 context 对象为什么不是 store 实例本身了。
如何使用action呢?进行action的分发:
分发使用的是 store 上的dispatch函数;
同样的,它也可以携带我们的参数:
也可以以对象的形式进行分发:
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
action也有对应的辅助函数:使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
)