Vue中管理应用程序的状态有多种不同的方法,了解状态管理也是学习Vue知识的基础部分,也是很重要的一部分。从这篇文章开始,我们来开始学习Vue应用程序中的状态管理。在这篇文章中会先简单的介绍Vue应用程序中状态管理的大多数方法。希望对Vue的学习者有所帮助。
状态管理
Vue组件是Vue应用程序的构建中的一部分,允许我们在其中结合标记(HTML)、样式(CSS)和逻辑(JavaScript)。
接下来的示例将以单文件构建Vue组件的方式向大家呈现,该组件显示data属性中numbers中的一系列数字:
1 | <!-- NumberComponent.vue --> |
每个Vue组件都包含一个data()函数,用于要响应的组件。如果模板中使用的data()属性值发生更改,组件视图将重新呈现以显示更改。
在上面的示例中,numbers是存储在data()函数中的一个数组。如果另一个组件要访问data()函数中的numbers,该怎么办呢?例如,我们可能需要一个组件负责显示numbers(比如上面的示例),另一个组件负责操作numbers的值。
如果我们想在多个组件之间共享numbers,那么numbers则不仅仅是组件组别的data,而是应用程序级别的data。这就把我们带到了状态管理的主题 —— 应用程序级别数据的管理。
在我们讨论如何在应用程序中管理状态之前,我们首先要了解Vue中的props和自定义事件是如何在父组件和子组件之间共享数据。
Props和自定义事件
假设我们有一个应用程序,它包含父组件和子组件。和其他的前端框架一样,Vue允许我们使用props将数据从父组件传递到子组件。
使用props非常简单。我们实际上需要做的就是将一个值绑定到正在呈现的子组件的prop属性上。下面是一个使用v-bind指令向下传递一个数组值的示例:
1 | <!-- ParentComponent.vue --> |
ParentComponent组件把numbers数组作为同名的props传递给ChildComponent组件。ChildComponent组件借助Mustache语法将numbers值绑定到其模板上。
props 可以用于将数据从父组件传递到子组件!
如果我们需要一个相反方向传递数据的方法(从子组件传到父组件),应该怎么办?比如上面的例,允许我们从子组件的data()函数中引入一个新的number数组。
我们不能再使用props来传递数据了,因为props只能单向传输数据(你从父到子到孙…等等)。为了便于让子组件通知父组件一些事情,我们可以使用Vue自定义事件。
Vue中的自定义事件与JavaScript原生的自定义事件非常相似,但有一个关键性的区别:Vue中的自定义事件主要用于组件之间的通讯,而不是DOM节点之间的通讯!
下面这个示例就是使用自定义事件,把ChildComponent中的number值传递给ParentComponent组件,从而更改ParentComponent的numbers的示例:
1 | <!-- ParentComponent.vue --> |
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 | // event-bus.js |
我们现在可以使用EventBus的接口来发出事件(Emit Events)。假设我们有一个NumberSubmit组件,它负责在单击按钮时发送自定义事件。这个自定义事件number-added将传递用户在input中输入的值:
1 | <!-- NumberSubmit.vue --> |
现在我们可以有一个完全独立的组件,比如NumberDisplay,它会显示一个数字值的列表,并监听NumberSubmit组件中是否输入了一个新数值:
1 | <!-- NumberDisplay.vue --> |
我们在NumberDisplay 组件的created()钩子中(它是Vue生命周期中的一个钩子函数)创建了一个EventBus监听器:EventBus.$on
。当NumberSubmit组件发送事件时,它将在事件对象中传递一个number值。NumberDisplay侦听并将该新number推送到其numbers数组中。
1 | <!-- App.vue --> |
上面的示例回答了前面提出的问题:EventBus可以用来实现兄弟组件之间的数据通讯!
是不是觉得设置和使用EventBus很容易,对吧?不幸的是,EventBus有一个明显的劣抛。假如我们的应用程序下面这样的:
假设所有的白线箭头都是从父组件向下传递到所有子组件的props,而黄色的虚线箭头则是从组件发出和监听事件。这些事件都没有被跟踪,并且可以在应用程序的任何地方触发。这使得维护工作变得非常困难,这可能会使代码难以工作,并且成为bug的来源。
这是为什么Vue指南声明EventBus不是Vue应用程序数据管理方法的主要原因之一。
EventBus是让所有组件相互通讯的一种简单方法,但并不适合中、大型的应用程序。
全局存储
让我们看看另一种处理应用程序数据通讯的方法。
通过创建包含在组件之间共享数据存储的存储模式,可以实现一些简单的状态管理。存储(Store)可以管理应用程序的状态以及负责更改状态的方法。
例如,我们可以有一个像下面这样简单的存储:
1 | // store.js |
在store中的state中包含了一个numbers数组,以及一个addNumbers方法,该方法接受接受有效负载并直接更新state.numbers
的值。
我们可以有一个组件NumberDisplay用来显示来自store的numbers数组:
1 | <!-- NumberDisplay.vue --> |
我们现在可以创建另一个组件NumberSubmit,它允许用户向我们数据数组中添加一个新的数字:
1 | <!-- NumberSubmit.vue --> |
NumberSubmit组件中有一个addNumber()方法,它调用store.addNumber()
变量并传递预期的有效负载。
store方法接收有效负载并直接改变store.numbers
数组。由于Vue的响应性(Vue reactivity),当存储状态中的number数组发生更改时,依赖于此值的相关DOM(NumberDisplay组件中的<template>
)会自动更新。
当我们说组件相互交互时。这些组件不会对彼此做任何事情,而是通过存储相互调用更改。
然后在App.vue
中引入刚才创建的组件:
1 | <!-- App.vue --> |
如果我们仔细观察所有与存储直接交互的所有部分,我们可以建立一个模式:
- 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 | // store.js |
Vuex存储(Vuex Store)包含四个对象:state、mutations、actions和getters。
state 只是一个包含需要在应用程序中共享的属性的对象。
1 | // store.js |
这个state对象只包含了一个numbers数组。
mutations是负责直接改变存储状态的函数。在Vuex中,mutations总是以state作为第一个参数。此外,actions也可以不作为第二个参数传递有效负载:
1 | // store.js |
在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 | // store.js |
Vuex存储中的getters就像组件中的计算属性一样。getters主要用于执行一些计算和操作,以便在组件访问这些信息之前存储状态。
像mutations一样,getters可以访问state作为第一个参数。这里有一个叫getNumbers的getter,它只返回state.numbers
数组:
1 | // store.js |
最后store.js的代码如下所示:
1 | import Vue from "vue"; |
对于这样简单的一个示例,可能不一定需要Vuex存储。上面的示例只是用来向大家展示如何使用Vuex和简单的全局存储在实现上的直接区别。
当Vuex存储准备好之后,Vue应用程序可以在Vue实例中声明store对象,可以提供给Vue应用程序使用。
1 | // main.js |
有了Vuex存储之后,组件通常可以执行以下两种操作之一。他们要么:获取(GET)状态信息(通过访问store中state或getters)或者 调用(DISPATCH)actions。
下面创建的NumberDisplay组件,它通过将getNumbers存储getter映射到组件getNumbers计算属性来直接显示state.numbers
数组。
1 | <!-- NumberDisplay.vue --> |
接着再创建一个NumberSubmit组件,允许用户通过addNumber方法映射到同名的actions,然后将新输入的数字添加到state.numbers
:
1 | <!-- NumberSubmit.vue --> |
最后在App.vue
中引入前面创建的组件:
1 | <!-- App.vue --> |
我们可以看到,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