详细

Redux和Vuex的区别

前言

对于任何应用,在自身数据传递中, state 扮演了重要的角色。随着应用扩增, 状态管理也变的复杂,使用中央仓库管理应用的数据也迫在眉睫。中央仓库可以让 state 的管理更加简单、原子化以及方便扩展。

image

在本文中,我们将仔细研究并比较当前前端生态系统中两个最流行的状态管理库: ReduxVuex

什么是状态管理,为什么需要它?

状态管理是在应用的组件间读取和变更 state (即数据)的方式。 state 并不全是复杂的,在一些场景中, state 可能规模非常小并且方便本地管理。例如从父组件A到子组件B直接透传数据是更简洁的。 但是,假设我们需要从组件A传递一些数据到外部组件J,该怎么办呢? 在这种情况下,管理状态的一种方式,可能是广播数据。然而,这种方式并不是最佳实践。 我们先前提到过,当应用变的越来越大,数据管理往往会更复杂。这种情况下往往会诞生一种需求,用中央仓库存储各种组件(甚至不是组件)的 state ,而不是用prop透传,毕竟prop透传会成为深度嵌套组件的麻烦。 基于以上原因,产生了对全局状态的需求,希望有一个可靠的中心数据源,能让所有程序中的组件都可以轻松访问任意一个状态,而且几乎没有任何限制。

何为Redux?

Redux 自从2015年发布以来,已经变成了最受欢迎的 React 应用开发的状态管理库。它是一个可控的状态容器,允许我们使用纯 JavaScript,并采用强制执行一致的模式,来确保我们的应用程序在客户端、服务器和本机环境中可靠,和易于测试。 除了 React , Redux 也可以用到其他JS框架和库里面,例如 Vue.js。 然而, Redux 近来也受到一些质疑,关于它使用起来有有一定门槛。出现这种负反馈,或许是因为,编写 actions 来定义每个状态的更改,然后创建多个 reducers 来控制这些 actions 可能会导致大量代码,并且这些代码很快就会变得难以管理。 为了解决这个问题,Facebook 和 React 核心团队成员,软件工程师 Dan Abramov 和 Andrew Clark 创造了 Redux 工具箱。它简化了存储设置和减少了必须的字段,使其成为高效 Redux 开发的首选工具。Redux 工具箱默认也遵循最佳实践。

Redux工作原理

在我们探究如何使用 Redux 在 React 应用中进行状态管理前,我们先来看一下 Redux 的工作原理。 典型的 Redux 设置由以下部分组成:

  • State : 即应用中的数据,组件中的 state 更新时也会引发组件重新渲染。
  • Reducers : 它是 Redux 存储更新的媒介, reducer 接收一个 action ,并根据 action typepayload 来操纵存储中的 state
  • Actions : 通过UI组件触发的更新存储的函数; action 接受 typepayload 两个参数,它通过 dispatcher 被触发。虽然 type 参数定义了发送到 reducer 的操作类型,但 payload 才是 reducer 在存储中更新的数据。

下面是redux.js.org上的一个图示,展示了在 Redux 状态管理库中的数据流。

image

React怎么用Redux管理状态?

我们来仔细研究一下如何使用 Redux 来管理 React 应用程序中的 state 。为了演示,我们将通过构建一个基本的计数器应用程序来简单的展示实现。

项目设置

用下面的命令创建一个 redux-demo 的项目:

Terminal window
npx create-react-app redux-demo

然后,安装 redux 包(使我们能够在应用程序中使用 Redux)和另一个名为 react-redux 的包,这个包能够简化 React 应用连接到 Redux 存储和 reducers

Terminal window
cd redux-demo
npm install redux react-redux

安装依赖后启动:

Terminal window
npm start

由于我们专注于使用 Redux,因此我们不会详细介绍构建 React 组件的细节。 在 src 文件夹中,创建一个名为 Components 的新文件夹,然后创建两个名为 Counter.jsCounter.css 的文件。将以下代码添加到 Counter.js 文件中:

const Counter = () => {
return (
<main>
<h1>Redux Counter</h1>
<div>{counter}</div>
<button>Increment Counter</button>
<button>Decrement Counter</button>
</main>
);
};
export default Counter;

代码里有两个按钮,后面将使用它们来增加或减少计数器的值。 接下来,将 Counter.js 导入 App.js。更新后的 App.js 如下所示:

import Counter from './components/Counter';
function App() {
return <Counter />;
}
export default App;

设置Redux

src 文件夹中,创建名为 store 的文件夹;在此文件夹中创建 index.js 文件。我们还没有订阅 store,因为我们只是设置计数器值;还没有准备好开始监听其状态的变化。 然后,将应用连接到 Redux store,以便应用的组件可以使用该 store;并导出 counterReducer 的实例。 src/store/index.js 文件现在应如下所示:

import { createStore } from "redux";
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
};
}
return state
};
const store = createStore(counterReducer);
export default store;

提供 store 并访问 store 里面的数据

为了让应用组件使用 store 作为中央状态存储库,我们需要从 react-redux 导入的 Provider 组件包装根 App 组件。然后,我们将通过将存储作为 prop 传递给 Provider 组件来引用该存储。 为此,请更新 src/index.js 文件,如下所示:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from './store/index'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);

为了从 Counter 组件访问存储中的数据,我们将从 React-Redux 库中导入 useSelector Hook。这个 Hook 将使能够让我们希望从 store 里获取想要的数据。useSelector 的另一个好处是它在幕后管理订阅。 我们使用 useSelector Hook 从存储中提取计数器 state 。接下来,我们将传递一个函数,该函数接收 Redux 管理的 state 以及我们想要提取的 state 部分。最后,我们将输出计数器值,代码如下:

import { useSelector } from 'react-redux';
const Counter = () => {
const counter = useSelector((state) => state.counter);
return (
<main>
<h1>Redux Counter</h1>
<div>{counter}</div>
<button>Increment Counter</button>
<button>Decrement Counter</button>
</main>
);
};
export default Counter;

到目前为止,我们已经了解了如何检索 Redux 管理的数据。现在,我们看一下如何更新数据。

调度 actions 更新数据

为了将计数器值增加或减少 1,我们使用 react-redux 包中的 useDispatch Hook。 首先,我们调用 useDispatch ,返回一个调度函数,我们可以调用该函数来针对 Redux 存储调度 action。接下来,我们将创建各个按钮的事件函数,并且在函数里,我们将传递 type。 更新后的 Counter.js 代码如下所示:

import { useSelector, useDispatch } from 'react-redux';
const Counter = () => {
const counter = useSelector((state) => state.counter);
const dispatch = useDispatch();
const incrementHandler = () => {
dispatch({ type: 'increment' });
};
const decrementHandler = () => {
dispatch({ type: 'decrement' });
};
return (
<main>
<h1>Redux Counter</h1>
<div>{counter}</div>
<button onClick={incrementHandler}>Increment Counter</button>
<button onClick={decrementHandler}>Increment Counter</button>
</main>
);
};
export default Counter;

当单击递增或递减按钮时,会调用适当的 action 将计数器值增加或减少 1。 我们仔细研究了如何使用 Redux 来管理 React 应用程序中的 state。现在,让我们看看如何使用 Vuex 来管理 Vue 应用程序中的 state

何为Vuex?

Vue 由 Evan You 创建,并由 Vue 核心团队维护。 Vuex 基于与 Redux 相同的 Flux 架构。 Vuex 是 Vue.js 应用的状态管理库。 通过这个库,应用程序 state 是集中的,应用中的每个组件都可以在任何时间点访问所需的 state。使用 Vuex,能够很容易的获取和更新 state

Vuex工作原理

Vuex states, getters, mutations, 和 actions的集合:

  • States : 就像Redux一样, state 定义了整个应用的全局数据属性;这使得定位特定数据变得容易,开发人员轻松获取当前应用程序状态的快照用于调试或其他目的。
  • Getters :store 中的计算值。在构建时,我们可能需要根据某个时间点计算派生状态,例如,过滤帖子列表并获取总量。如果该值将被多个组件使用,那么我们可以通过使用 getter 来实现这一目的。这将防止我们在不同的组件中重复实现相同作用的函数。
  • Mutations:提交 mutation 是有效更新 Vuex 存储中状态的唯一方法。Mutations 像事件一样;每个 mutation 都有一个字符串类型和一个处理函数;状态修改由 mutation 处理函数执行。
  • Actions:其工作方式与 mutation 非常相似,但是,操作不是直接改变 state,而是提交改变需要异步操作的 statemutation

下面是vuex.vuejs.org上的一个图示,展示了在 Vuex 状态管理库中的数据流。

image

Vue.js怎么用Vuex管理状态?

为了展示如何使用 Vuex 来管理 Vue 应用程序中的状态,让我们从 Redux 演示中构建一个计数器应用的副本,具有相同的结构和功能。

项目设置和创建 Vuex store

首先使用以下命令创建 Vue 项目名称 vuex-demo

Terminal window
vue create vuex-demo

在 Vue 项目设置过程中,我们会被问到一系列问题。对于本教程,我们采用以下配置:

提示选项
Please pick a presetManually select features
Check the features needed for your project默认选择 + Vuex
Choose a version of Vue.js that you want to start the project with3.x
Pick a linter / formatter configESLint with error prevention only
Pick additional lint featuresLint on save
Where would you prefer placing config for Babel, ESLint, etc.In dedicated config files
Save this as a preset for future projects?N

我们从创建项目的时候就很容易的将 Vuex 安装到应用中,可以到 src/store/index.js 找到它。 如果应用程序没有 Vuex,只需使用以下命令安装库:

Terminal window
npm install vuex@next --save

注意,在上面的代码中添加 @next 会安装最新版本的 Vuex 继续,由于我们聚焦在使用 Vuex,因此我们将保持 Counter 组件简单。 在 src/Components 文件夹,创建一个名为 Counter.vue 的文件,并添加以下代码:

<template>
<main>
<h1>Vuex Counter</h1>
<div>0</div>
<button>Increment Counter</button>
<button>Decrement Counter</button>
</main>
</template>

接下来,将 Counter.vue 导入到 App.vue 文件中。 App.vue 文件应如下所示:

<template>
<Counter />
</template>
<script>
import Counter from './components/Counter.vue';
export default {
name: 'App',
components: {
Counter
}
}
</script>
export default App;

接下来,我们需要为 demo 创建适当的 state, mutations, actionsgetters

在 Vue.js 组件中使用 Vuex 存储

state 是存储中的一揽子属性。因此,为了定义计数器值的 state,我们将其作为键值插入到存储中的 state 中。我们还给它初始值为0,如下所示:

import { createStore } from 'vuex';
export default createStore({
state: {
counter: 0,
},
mutations: {},
actions: {},
modules: {},
});

要访问 Counter 组件中的计数器值,我们首先导入 store,然后定位所需的 state(在本例中为 Counter ):

<template>
<main>
<h1>Vuex Counter</h1>
<div>{{counter}}</div>
<button>Increment Counter</button>
<button>Decrement Counter</button>
</main>
</template>
<script>
import { computed } from '@vue/reactivity';
import store from '../store';
export default {
setup() {
const counter = computed(() => {
return store.state.counter
})
return {
counter
};
},
};
</script>

为了使按钮更新计数器值,我们将在 Counter 组件中创建方法,并调度一个调用 mutation(我们传递有效 payload )的操作来更新 state 。它是从 Counter 组件传递的有效 payload,用于更新计数器 state(将其递减或递增 1)。 为了实现这一目标,我们将更新 Counter.vue,如下所示:

<template>
<main>
<h1>Vuex Counter</h1>
<div>{{ counter }}</div>
<button @click="increment">Increment Counter</button>
<button @click="decrement">Decrement Counter</button>
</main>
</template>
<script>
import { computed } from '@vue/reactivity';
import store from '../store';
export default {
setup() {
const counter = computed(() => {
return store.state.counter
})
const increment = () => {
store.commit('increment', { value: 1 })
}
const decrement = () => {
store.commit('decrement', { value: 1 })
}
return {
counter,
increment,
decrement,
};
},
};
</script>

接下来,更新 stote:

import { createStore } from 'vuex';
export default createStore({
state: {
counter: 0,
},
mutations: {
increment(state, payload) {
state.counter = state.counter + payload.value;
},
decrement(state, payload) {
state.counter = state.counter - payload.value;
},
},
actions: {},
modules: {},
});

使用 getters 读取 state

假设我们想要读取 state 的值,并可能用它来执行外部操作。我们可以使用 getter 来实现这一点。 在计数器演示中,我们将读取计数器值并在 DOM 上渲染该值乘以 2。 让我们更新 store 以使用 getter,如下所示:

import { createStore } from 'vuex';
export default createStore({
state: {
counter: 0,
},
mutations: {
increment(state, payload) {
state.counter = state.counter + payload.value;
},
decrement(state, payload) {
state.counter = state.counter - payload.value;
},
},
actions: {},
getters: {
modifiedCounterValue(state) {
return state.counter * 2;
},
},
modules: {},
});

我们现在可以从 App.vue 文件访问修改后的 CounterValue。我们使用计算属性来实现这一点,如下所示:

<template>
<Counter />
<div>Modified counter value: {{modifiedCounterValue}}</div>
</template>
<script>
import { computed } from '@vue/reactivity';
import Counter from './components/Counter.vue';
import store from './store';
export default {
name: 'App',
components: {
Counter,
},
setup() {
const modifiedCounterValue = computed(() => {
return store.getters.modifiedCounterValue
})
return {
modifiedCounterValue,
}
}
}
</script>

调度更新数据的actions

在 Vue 中,最佳实践是仅在执行异步操作时使用 actions。由于我们的演示不是异步的,因此我们可以参考 Vuex docs 来了解如何使用 actions 来分派数据,同时处理异步操作和处理 Promise,例如从 API 获取数据。

结论

Redux 和 Vuex 状态管理库在开发者生态系统中广泛使用。在本文中,我们探索了这两个库,展示了它们的工作原理,并演示了如何使用它们。我们还研究了 Redux 工具包,并展示了它如何帮助简化 Redux 设置、帮助避免常见错误,以及如何用名为 configureStore 的改进版本替换 createStore。 您可以在官方文档中阅读有关 Redux Toolkit 的更多信息。同样,您可以从其官方文档中了解有关 Vuex 的更多信息。 这些库可以充当所有应用程序状态的可预测中央存储,但最好避免将它们与不是很复杂的应用一起使用,因为这可能会让项目变得更加复杂和乏味。 与 Vuex 相比,Redux 是一个更流行、支持更好的库,但 Vuex 似乎是一个维护性更好的状态管理库。但是,选择权在您手中,具体取决于您的具体项目需求。 我希望本文能够帮助您更好地了解 Redux 和 Vuex 的工作原理,并为您提供有用的信息来确定哪种解决方案最适合您的项目。

翻译自 Comparing Redux vs. Vuex