Vue中的状态管理

目录
  1. 1. 状态管理
  2. 2. Props和自定义事件
  3. 3. EventBus
  4. 4. 全局存储
  5. 5. Vuex
  6. 6. 如何选择最合适的方法

Vue中管理应用程序的状态有多种不同的方法,了解状态管理也是学习Vue知识的基础部分,也是很重要的一部分。从这篇文章开始,我们来开始学习Vue应用程序中的状态管理。在这篇文章中会先简单的介绍Vue应用程序中状态管理的大多数方法。希望对Vue的学习者有所帮助。

状态管理

Vue组件是Vue应用程序的构建中的一部分,允许我们在其中结合标记(HTML)、样式(CSS)和逻辑(JavaScript)。

接下来的示例将以单文件构建Vue组件的方式向大家呈现,该组件显示data属性中numbers中的一系列数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- NumberComponent.vue -->
<template>
<div>
<h2>The numbers are {{ numbers }}</h2>
</div>
</template>

<script>
export default {
name: 'NumberComponent',
data: () => ({
numbers: [1, 2, 3]
})
}
</script>

每个Vue组件都包含一个data()函数,用于要响应的组件。如果模板中使用的data()属性值发生更改,组件视图将重新呈现以显示更改。

在上面的示例中,numbers是存储在data()函数中的一个数组。如果另一个组件要访问data()函数中的numbers,该怎么办呢?例如,我们可能需要一个组件负责显示numbers(比如上面的示例),另一个组件负责操作numbers的值。

如果我们想在多个组件之间共享numbers,那么numbers则不仅仅是组件组别的data,而是应用程序级别的data。这就把我们带到了状态管理的主题 —— 应用程序级别数据的管理。

在我们讨论如何在应用程序中管理状态之前,我们首先要了解Vue中的props和自定义事件是如何在父组件和子组件之间共享数据

Props和自定义事件

假设我们有一个应用程序,它包含父组件和子组件。和其他的前端框架一样,Vue允许我们使用props将数据从父组件传递到子组件。

使用props非常简单。我们实际上需要做的就是将一个值绑定到正在呈现的子组件的prop属性上。下面是一个使用v-bind指令向下传递一个数组值的示例:

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
27
28
29
30
31
32
33
34
35
36
<!-- ParentComponent.vue -->
<template>
<div>
<ChildComponent :numbers="numbers" />
</div>
</template>

<script>
import ChildComponent from './ChildComponent'

export default {
name: 'ParentComponent',
data: () => ({
numbers: [1, 2, 3]
}),
components: {
ChildComponent
}
}
</script>

<!-- ChildComponent.vue -->
<template>
<div>
<h2>{{ numbers }}</h2>
</div>
</template>

<script>
export default {
name: 'ChildComponent',
props: {
numbers: Array
}
}
</script>

ParentComponent组件把numbers数组作为同名的props传递给ChildComponent组件。ChildComponent组件借助Mustache语法将numbers值绑定到其模板上。

props 可以用于将数据从父组件传递到子组件!

如果我们需要一个相反方向传递数据的方法(从子组件传到父组件),应该怎么办?比如上面的例,允许我们从子组件的data()函数中引入一个新的number数组。

我们不能再使用props来传递数据了,因为props只能单向传输数据(你从父到子到孙…等等)。为了便于让子组件通知父组件一些事情,我们可以使用Vue自定义事件。

Vue中的自定义事件与JavaScript原生的自定义事件非常相似,但有一个关键性的区别:Vue中的自定义事件主要用于组件之间的通讯,而不是DOM节点之间的通讯!

下面这个示例就是使用自定义事件,把ChildComponent中的number值传递给ParentComponent组件,从而更改ParentComponent的numbers的示例:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- ParentComponent.vue -->
<template>
<div>
<ChildComponent :numbers="numbers" @number-added="numbers.push($event)" />
</div>
</template>

<script>
import ChildComponent from './ChildComponent';

export default {
name: 'ParentComponent',
data: () => ({
numbers: [1, 2, 3]
}),
components: {
ChildComponent
}
}
</script>

<!-- ChildComponent.vue -->
<template>
<div>
<h2>{{ numbers }}</h2>

<div class="form">
<input v-model="number" type="number" />
<button @click="$emit('number-added', Number(number))"> Add new number</button>
</div>
</div>
</template>

<script>
export default {
name: 'ChildComponent',
props: {
numbers: Array
},
data: () => ({
number: 0
})
}
</script>

ChildComponent组件有一个捕获number值的input和捕获number值发出一个number-added自定义事件的按钮。

在ParentComponent组件上指定了由@number-added表示的自定义事件的监听器,其主要用于呈现子组件。当该事件在子组件中发出时,它将number的值推送到ParentComponent组件的numbers数组中。

自定义事件用于从子组件到父组件的通讯。

我们可以使用props向下传递数据,使用自定义事件向上发送消息。我们如何能够传递数据和实现两个不同兄弟组件之间的通讯呢?

我们不能像上面那样使用自定义事件,因为这些事件是在特定组件的接口中发出的,因此需要在组件渲染的位置声明自定义事件侦听器。在两个独立的组件中,一个组件不会在另一个组件中渲染。

在Vue中大致有三种方式可以管理兄弟组件之间的数据通讯,从而处理应用程序的状态管理:

  • 使用全局的EventBus
  • 使用简单的全局存储
  • 使用类似于Flux库的Vuex

EventBus

EvemtBus是一个Vue实例,用于支持独立组件之间订阅和发布自定义事件。

等等,我们不是说独立的组件不能触发和监听彼此之间的自定义事件吗?他们通常不能,但是一个EventBus帮助我们实现这个目标,因为它是全局的,可以通用。

下面的示例在event-bus.js创建了一个EventBus的实例:

1
2
3
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

我们现在可以使用EventBus的接口来发出事件(Emit Events)。假设我们有一个NumberSubmit组件,它负责在单击按钮时发送自定义事件。这个自定义事件number-added将传递用户在input中输入的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- NumberSubmit.vue -->
<template>
<div class="form">
<input v-model="number" type="number" />
<button @click="addNumber">Add new number</button>
</div>
</template>

<script>
import { EventBus } from '../event-bus.js';

export default {
name: 'NumberSubmit',
data: () => ({
number: 0
}),
methods: {
addNumber(newNumber) {
EventBus.$emit('number-added', Number(this.number))
}
}
}
</script>

现在我们可以有一个完全独立的组件,比如NumberDisplay,它会显示一个数字值的列表,并监听NumberSubmit组件中是否输入了一个新数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- NumberDisplay.vue -->
<template>
<div>
<h2>{{ numbers }}</h2>
</div>
</template>

<script>
import { EventBus } from '../event-bus.js';

export default {
name: 'NumberDisplay',
data: () => ({
numbers: [1, 2, 3]
}),
created() {
EventBus.$on('number-added', number => {
this.numbers.push(number)
})
}
}
</script>

我们在NumberDisplay 组件的created()钩子中(它是Vue生命周期中的一个钩子函数)创建了一个EventBus监听器:EventBus.$on。当NumberSubmit组件发送事件时,它将在事件对象中传递一个number值。NumberDisplay侦听并将该新number推送到其numbers数组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- App.vue -->
<template>
<div id="app">
<NumberDisplay />
<NumberSubmit />
</div>
</template>

<script>
import NumberDisplay from './components/NumberDisplay';
import NumberSubmit from './components/NumberSubmit';

export default {
name: 'App',
components: {
NumberDisplay,
NumberSubmit
}
}
</script>

上面的示例回答了前面提出的问题:EventBus可以用来实现兄弟组件之间的数据通讯!

是不是觉得设置和使用EventBus很容易,对吧?不幸的是,EventBus有一个明显的劣抛。假如我们的应用程序下面这样的:

假设所有的白线箭头都是从父组件向下传递到所有子组件的props,而黄色的虚线箭头则是从组件发出和监听事件。这些事件都没有被跟踪,并且可以在应用程序的任何地方触发。这使得维护工作变得非常困难,这可能会使代码难以工作,并且成为bug的来源。

这是为什么Vue指南声明EventBus不是Vue应用程序数据管理方法的主要原因之一。

EventBus是让所有组件相互通讯的一种简单方法,但并不适合中、大型的应用程序。

全局存储

让我们看看另一种处理应用程序数据通讯的方法。

通过创建包含在组件之间共享数据存储的存储模式,可以实现一些简单的状态管理。存储(Store)可以管理应用程序的状态以及负责更改状态的方法。

例如,我们可以有一个像下面这样简单的存储:

1
2
3
4
5
6
7
8
9
// store.js
export const store = {
state: {
numbers: [1, 2, 3]
},
addNumber(newNumber) {
this.state.numbers.push(newNumber)
}
}

在store中的state中包含了一个numbers数组,以及一个addNumbers方法,该方法接受接受有效负载并直接更新state.numbers的值。

我们可以有一个组件NumberDisplay用来显示来自store的numbers数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- NumberDisplay.vue -->
<template>
<div>
<h2>{{ storeState.numbers }}</h2>
</div>
</template>

<script>
import { store } from '../store.js';

export default {
name: 'NumberDisplay',
data: () => ({
storeState: store.state
})
}
</script>

我们现在可以创建另一个组件NumberSubmit,它允许用户向我们数据数组中添加一个新的数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- NumberSubmit.vue -->
<template>
<div class="form">
<input v-model="numberInput" type="number" />
<button @click="addNumber(numberInput)">Add new number</button>
</div>
</template>

<script>
import { store } from '../store.js';

export default {
name: 'NumberSubmit',
data: () => ({
numberInput: 0
}),
methods: {
addNumber(numberInput) {
store.addNumber(Number(numberInput))
}
}
}
</script>

NumberSubmit组件中有一个addNumber()方法,它调用store.addNumber()变量并传递预期的有效负载。

store方法接收有效负载并直接改变store.numbers数组。由于Vue的响应性(Vue reactivity),当存储状态中的number数组发生更改时,依赖于此值的相关DOM(NumberDisplay组件中的<template>)会自动更新。

当我们说组件相互交互时。这些组件不会对彼此做任何事情,而是通过存储相互调用更改。

然后在App.vue中引入刚才创建的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- App.vue -->
<template>
<div id="app">
<NumberDisplay />
<NumberSubmit />
</div>
</template>

<script>
import NumberDisplay from './components/NumberDisplay';
import NumberSubmit from './components/NumberSubmit';

export default {
name: 'App',
components: {
NumberDisplay,
NumberSubmit
}
}
</script>

如果我们仔细观察所有与存储直接交互的所有部分,我们可以建立一个模式:

  • NumberSubmit中的方法有责任直接对存储方法进行操作,因此我们可以将其标记为 存储操作 (Store action)
  • 存储方法也有一定的责任 —— 直接改变存储状态。 所以我们会说这是一个 存储变量 (Store mutation)
  • NumberDisplay并不真正关心存储或NumberSubmit中方法类型,只关心存储中获取信息。所以我们会说组件A是各种 Store getter

一个动作(Action)提交给一个变量(Mutation)。变量会改变状态,然后影响视图或组件。视图或组件使用 getter 检索存储数据。我们开始很接近类似Flux的状态管理。

允许组件依赖于外部存储,简单存储可以更易于管理应用程序的状态。

Vuex

Vuex是类似Flux的状态管理库,专门用于Vue的状态管理。

对于那些不熟悉的人来说,Flux是Facebook创造的一种设计模式。Flux模式由四个部分组成,组成单向数据管道:

Vuex的灵感主要来自Flux和Elm Architecture。Vuex集成的核心是Vuex存储。

1
2
3
4
5
6
7
// store.js
const store = new Vuex.Store({
state,
mutations,
actions,
getters
})

Vuex存储(Vuex Store)包含四个对象:state、mutations、actions和getters。

state 只是一个包含需要在应用程序中共享的属性的对象。

1
2
3
4
// store.js
const state = {
numbers: [1, 2, 3]
}

这个state对象只包含了一个numbers数组。

mutations是负责直接改变存储状态的函数。在Vuex中,mutations总是以state作为第一个参数。此外,actions也可以不作为第二个参数传递有效负载:

1
2
3
4
5
6
// store.js
const mutations = {
ADD_NUMBER(state, payload) {
state.numbers.push(payload)
}
}

在Flux架构中,mutations中的函数通常用大写字母表示,以区别于其他函数,并用于工具和lint目的。在上面的示例中,创建了一个ADD_NUMBER()的mutations,它需要一个有效的payload并将该有效的payload推送到state.numbers数组中。

actions可以调用mutations。在提交mutations之前,actions还负责所有异步调用。actions可以访问context对象,该对象提供对state(使用context.state)、getter(使用context.getters)和commit函数(context.commit)的访问。

下面是一个简单的actions的示例,它只是传递预期的有效负载时直接提交mutations:

1
2
3
4
5
6
// store.js
const actions = {
addNumber(context, number) {
context.commit('ADD_NUMBER', number)
}
}

Vuex存储中的getters就像组件中的计算属性一样。getters主要用于执行一些计算和操作,以便在组件访问这些信息之前存储状态。

像mutations一样,getters可以访问state作为第一个参数。这里有一个叫getNumbers的getter,它只返回state.numbers数组:

1
2
3
4
5
6
// store.js
const getters = {
getNumbers(state) {
return state.numbers
}
}

最后store.js的代码如下所示:

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
27
28
29
30
31
32
33
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const state = {
numbers: [1, 2, 3]
};

const mutations = {
ADD_NUMBER(state, payload) {
state.numbers.push(payload);
}
};

const actions = {
addNumber(context, number) {
context.commit("ADD_NUMBER", number);
}
};

const getters = {
getNumbers(state) {
return state.numbers;
}
};

export default new Vuex.Store({
state,
mutations,
actions,
getters
});

对于这样简单的一个示例,可能不一定需要Vuex存储。上面的示例只是用来向大家展示如何使用Vuex和简单的全局存储在实现上的直接区别。

当Vuex存储准备好之后,Vue应用程序可以在Vue实例中声明store对象,可以提供给Vue应用程序使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.js
import Vue from "vue";
import App from "./App";
import store from "./store";

new Vue({
el: '#app',
store,
components: {
App
},
template: '<App />'
})

有了Vuex存储之后,组件通常可以执行以下两种操作之一。他们要么:获取(GET)状态信息(通过访问store中state或getters)或者 调用(DISPATCH)actions。

下面创建的NumberDisplay组件,它通过将getNumbers存储getter映射到组件getNumbers计算属性来直接显示state.numbers数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- NumberDisplay.vue -->
<template>
<div>
<h2>{{ getNumbers }}</h2>
</div>
</template>

<script>
export default {
name: 'NumberDisplay',
computed: {
getNumbers() {
return this.$store.getters.getNumbers
}
}
}
</script>

接着再创建一个NumberSubmit组件,允许用户通过addNumber方法映射到同名的actions,然后将新输入的数字添加到state.numbers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- NumberSubmit.vue -->
<template>
<div class="form">
<input v-model="numberInput" type="number" />
<button @click="addNumber(numberInput)">Add new number</button>
</div>
</template>

<script>
export default {
name: 'NumberSubmit',
data: () => ({
numberInput: 0
}),
methods: {
addNumber(numberInput) {
this.$store.dispatch('addNumber', Number(numberInput))
}
}
}
</script>

最后在App.vue中引入前面创建的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- App.vue -->
<template>
<div id="app">
<NumberDisplay/>
<NumberSubmit/>
</div>
</template>

<script>
import NumberDisplay from "./components/NumberDisplay";
import NumberSubmit from "./components/NumberSubmit";

export default {
name: "App",
components: {
NumberDisplay,
NumberSubmit
}
};
</script>

我们可以看到,Vuex通过引入显式定义的actions、mutations和getters 扩展了简单的存储方法。这就是使用Vuex的最初标准和主要优势所在。此外,Vuex和vue-devtools集成在一起,提供了更易的调试功能。

下图就是一个关于vue-devtools如何帮助我们在发生突变时观察存储信息:

Vuex不是唯一个用来管理Vue状态的库,类似于Flux的库在社区中还有很多种,比如redux-vue或vuejs-redux,用于扩展Redux。然而,由于Vuex是专门为Vue应用程序而定制的,因此它无疑是最容易与Vue应用程序集成在一起。

Vuex扩展了简单的存储方法,使我们的应用程序的状态管理变得更简单。

如何选择最合适的方法

很多时候,你会发现大家试图了解最佳方法是什么?我不一定相信有正确或错误的方法,因为每种方法都有其优点和缺点。

EventBus
优点: 非常容易设置
缺点: 无法正确跟踪发生的变化

简单的存储
优点: 相对容易建立
缺点: 状态和可能的状态变化没有明确定义

Vuex
优点: 管理应用程序最强大的方法,并且与Vue开发工具集成在一起
缺点:额外的文件,需要花时间学习

不管哪一种方法,都没有最好的方法,只有最适合的方法。我们应该根据自己的项目选择最适合项目的最佳方法。最后希望这篇文章对于想学习Vue的状态管理的同学有所帮助。

转自:Vue中的状态管理
扩展阅读:技术胖的vuex视频教程已经在本地生成PDF