特别声明,本文根据@Hassan Djirdeh的《Managing State in Vue.js》一文所整理。
Vue中管理应用程序的状态有多种不同的方法,了解状态管理也是学习Vue知识的基础部分,也是很重要的一部分。从这篇文章开始,我们来开始学习Vue应用程序中的状态管理。在这篇文章中会先简单的介绍Vue应用程序中状态管理的大多数方法。希望对Vue的学习者有所帮助。
状态管理
Vue组件是Vue应用程序的构建中的一部分,允许我们在其中结合标记(HTML)、样式(CSS)和逻辑(JavaScript)。
接下来的示例将以单文件构建Vue组件的方式向大家呈现,该组件显示data
属性中numbers
中的一系列数字:
<!-- 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
指令向下传递一个数组值的示例:
<!-- 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
的示例:
<!-- 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的实例:
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
我们现在可以使用EventBus
的接口来发出事件(Emit Events)。假设我们有一个NumberSubmit
组件,它负责在单击按钮时发送自定义事件。这个自定义事件number-added
将传递用户在input
中输入的值:
<!-- 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
组件中是否输入了一个新数值:
<!-- 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
数组中。
<!-- 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)可以管理应用程序的状态以及负责更改状态的方法。
例如,我们可以有一个像下面这样简单的存储:
// 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
数组:
<!-- 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
,它允许用户向我们数据数组中添加一个新的数字:
<!-- 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
中引入刚才创建的组件:
<!-- 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存储。
// store.js
const store = new Vuex.Store({
state,
mutations,
actions,
getters
})
Vuex存储(Vuex Store)包含四个对象:state
、mutations
、actions
和getters
。
state
只是一个包含需要在应用程序中共享的属性的对象。
// store.js
const state = {
numbers: [1, 2, 3]
}
这个state
对象只包含了一个numbers
数组。
mutations
是负责直接改变存储状态的函数。在Vuex中,mutations
总是以state
作为第一个参数。此外,actions
也可以不作为第二个参数传递有效负载:
// 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
:
// store.js
const actions = {
addNumber(context, number) {
context.commit('ADD_NUMBER', number)
}
}
Vuex存储中的getters
就像组件中的计算属性一样。getters
主要用于执行一些计算和操作,以便在组件访问这些信息之前存储状态。
像mutations
一样,getters
可以访问state
作为第一个参数。这里有一个叫getNumbers
的getter
,它只返回state.numbers
数组:
// store.js
const getters = {
getNumbers(state) {
return state.numbers
}
}
最后store.js
的代码如下所示:
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应用程序使用。
// 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
数组。
<!-- 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
:
<!-- 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
中引入前面创建的组件:
<!-- 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的状态管理的同学有所帮助。