学习脉络:了解历史,了解Flux,了解Redux、Vuex,了解基于hooks的状态管理方案。
历史
前端的发展历程可以追溯到最早的静态HTML页面,那时任何微小的改动都需要整个页面的重绘。随后,iframe和XMLHttpRequest(XHR)的出现让开发者能够实现异步局部加载,进一步丰富了网页的动态性。然后,jQuery成为了一个里程碑,它提供了一套简单易用的API来操纵DOM和实现Ajax操作。
进入到框架时代,Angular、React和Vue等前端框架成为主流。这一时代的两大特点是组件化 和数据驱动。组件化意味着将用户界面拆分为多个可重用和独立的小块,而数据驱动则意味着所有的界面渲染都是由底层数据模型控制的。
对于这两个特点,一方面,组件化让我们面临一个问题:在复杂应用中,不同组件如何进行有效的通信? 另一方面,数据驱动方式提出了另一个问题:如何高效地管理多个组件依赖的公共数据?
这个时候就有聪明的Facebook攻城狮提出了Flux架构思想,打开了前端状态管理的大门。
Flux
Flux 是一种架构思想,专门解决软件的结构问题。它跟MVC 架构是同一类东西,但是更加简单和清晰。
flux四部分(View Action Dispatcher Store)
Flux将一个应用分成四部分:
View 视图层
Action 动作 (视图层发来的消息)
Dispacher 派发器(用来接收
Store 数据层(存储应用状态
单向流动
Flux最大特点是数据单向流动,举个「点击按钮数字+1」的例子:
用户点击按钮 (View发出用户的Action
Action 将时间传递给Dispacher
Dispacher改变Store里的状态(Store里计数器的值+1
Store状态改变出发View渲染(视图上的计数器+1
Flux 是前端状态管理的基础架构模式,而 Redux、Vuex 等库可以被视为 Flux 思想的具体实现和优化。这些库采用了 Flux 的核心概念,并加入了各自独特的特性和优化,以提供更高效、可扩展的状态管理解决方案。
Redux
三大原则(单一、只读、纯函数)
- 单一数据源(一个Redux应用只有一个Store),也是单向的数据流;
- state只读(state只可整体替换,不可直接修改);
- 使用纯函数(Reducer)来修改state。
Redux代码示例
js
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
// reducer.js
const initialState = { count: 0 };
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
};
const store = createStore(reducer);
// App.js
const App = ({ count, dispatch }) => (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
</div>
);
const mapStateToProps = (state) => ({
count: state.count,
});
const ConnectedApp = connect(mapStateToProps)(App);
ReactDOM.render(
<Provider store={store}>
<ConnectedApp />
</Provider>,
document.getElementById('root')
);
纯函数
纯函数有以下特点:
- 相同的输入总是返回相同的输出。
- 不产生副作用。
- 不依赖于外部状态。
纯函数示例
js
// 纯函数,接受一个参数,返回一个新的对象,原参数没有被改变
function addOneToEachElement(arr) {
return arr.map(element => element + 1);
}
// 非纯函数,改变了输入参数
function addOneToEachElementImpure(arr) {
for(let i = 0; i < arr.length; i++) {
arr[i] += 1;
}
return arr;
}
// 非纯函数,使用了系统 I/O
function readFileAndDoSomething(filename) {
const data = fs.readFileSync(filename); // 假设 fs 是文件系统模块
// do something with data
}
// 非纯函数,使用了 Math.random()
function getRandomNumber() {
return Math.random();
}
在Redux中,reducer 必须是纯函数,主要有以下几个原因:
- 确保可预测性:纯函数的输出完全取决于输入,不会有副作用,从而使得State的变化可预测。
- 确保可测试性:纯函数更容易测试,因为给定相同的输入,输出总是相同的。
- 确保可维护性:纯函数由于没有副作用,更容易维护和理解。
- 时间回溯:纯函数使得开发者工具能实现时间旅行等高级功能,这些功能能帮助你更好地调试应用。
举个简单例子 假设我们有一个简单的计数器应用,其中的reducer可能是这样的(纯函数):
js
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
如果我们使用非纯函数,可能会是这样:
js
function nonPureCounterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
state.count = state.count + 1; // 直接修改了state
return state;
case 'DECREMENT':
state.count = state.count - 1; // 直接修改了state
return state;
default:
return state;
}
}
在这个非纯函数nonPureCounterReducer
的例子中,我们直接修改了传入的state
。这会导致以下问题:
- 这样的reducer可能会引起组件无法正确重新渲染,因为React 的 PureComponent / React.memo 是浅比较,它们可能不会意识到 state 已经改变。
- 当你尝试回溯或者查看历史state时,你会发现所有的历史state都是相同的,因为他们都是对同一个对象的引用。
使用非纯函数会让你的应用变得难以维护和调试,因此建议总是使用纯函数作为你的 reducer。
React-Redux
React-Redux 是一个专为 React 设计的库,旨在简化 Redux 在 React 中的使用。它并没有改变 Redux 的基础原理,如单一状态树、Action 和 Reducer,而是提供了一系列 React 组件和 Hooks,让你能更轻松地在 React 应用中实现状态管理。
通过使用 React-Redux 提供的 Provider 组件和 connect 高阶组件或者新引入的 Hooks(useSelector 和 useDispatch 等),可以避免编写大量样板代码,同时也可以更方便地将 Redux store 的状态和 dispatch 方法传递给 React 组件。
Vuex(单一、mutation、action)
三大原则
- 应用层级的状态应该集中到单个 Store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
js
// 导入 Vue 和 Vuex
import Vue from 'vue';
import Vuex from 'vuex';
// 使用 Vuex
Vue.use(Vuex);
// 创建一个新的 store 实例
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
}
});
// 创建一个新的 Vue 实例
new Vue({
el: '#app',
store,
computed: {
count() {
return this.$store.state.count;
}
},
methods: {
incrementAsync() {
this.$store.dispatch('incrementAsync');
}
},
template: `
<div>
<p>{{ count }}</p>
<button @click="incrementAsync">Increment After 1 Second</button>
</div>
`
});
Vuex与Redux的区别
二者核心区别在于响应式 。Vuex有getter/setter,知道组件何时重新渲染,所以Vuex可以直接修改State内容,而React需要替换整个State。
Hooks时代的react状态管理
使用React原生的Hooks(如useState
、useReducer
等)或者基于Hooks的库(如zustand
、hox
等)通常更加简洁和直观。这些方案通常没有Redux和Vuex那样的"仪式感",例如不需要定义action types、actions和reducers,因此对新手更友好。
常见的方案有unstated-next
、hox
、zustand
等,详见EllieSummer大佬的文章:React hooks 状态管理方案