Vue中的状态管理

发布于 大漠

特别声明,本文根据@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组件,从而更改ParentComponentnumbers的示例:

<!-- 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用来显示来自storenumbers数组:

<!-- 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)包含四个对象:statemutationsactionsgetters

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作为第一个参数。这里有一个叫getNumbersgetter,它只返回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)状态信息(通过访问storestategetters)或者 调用(DISPATCHactions

下面创建的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通过引入显式定义的actionsmutationsgetters 扩展了简单的存储方法。这就是使用Vuex的最初标准和主要优势所在。此外,Vuex和vue-devtools集成在一起,提供了更易的调试功能。

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

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

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

如何选择最合适的方法

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

EventBus

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

简单的存储

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

Vuex

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

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