react 状态管理之 redux 的使用与实现原理
本文主要介绍 redux 在 react 中的最佳实践, 并通过分析源码实现 redux 的核心逻辑, 以掌握他们的原理。
什么是 redux
Redux 是一个使用叫做“action”的事件来管理和更新应用状态的模式和工具库 它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。
redux 设计原则
- 整个应用的 state 被保存在同一个 store 上, 并且 store 和 view 分离, 这就是集中式 store 的具体体现
- 单向数据流
- 用 state 来描述应用程序在特定时间点的状况
- 基于 state 来渲染出 View
- 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新,生成新的 state
- 基于新的 state 重新渲染 View
- 基于不可变数据, redux 的所有状态更新都是使用不可变的方式
redux 术语
action
action
是一个具有 type
字段的普通 JavaScript 对象。你可以将 action
视为描述应用程序中发生了什么的事件.
const addTodoAction = {
type: 'INCREMENT',
payload: 2
}
Action Creator
action creator
是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:
const incrementByAmount = amount => {
return {
type: 'INCREMENT',
payload: amount
}
}
Reducer
reducer
是一个函数,接收当前的 state
和一个 action
对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState
。 你可以将 reducer
视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。
Reducer 必需符合以下规则:
- 仅使用 state 和 action 参数计算新的状态值
- 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)。
- 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码
也就是说 reducer 必须是一个纯函数
Store
当前 Redux 应用的状态存在于一个名为 store 的对象中。
store 是通过传入一个 reducer 来创建的,并且有一个名为 getState 的方法,它返回当前状态值:
dispatch
Redux store 有一个方法叫 dispatch。更新 state 的唯一方法是调用 store.dispatch() 并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState() 可以获取新 state。
Selector
Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑:
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
Selector
函数接收根状态对象state
作为参数,并返回你需要获取的状态计算值。
redux 使用方法与原理
介绍了redux
的主要概念之后,让我们来看看在代码中如何使用redux
,以及如何来实现redux
的核心逻辑。
安装:
npm i redux
基础使用
以下代码只使用 redux
和 react
实现一个简单的计数器, 效果如下图:
react 版本使用的是 17
代码如下:
index.tsx:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore } from 'redux'
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
interface CounterState {
value: number
}
const initState: CounterState = {
value: 0,
}
const reducer = (state = initState, action: any): CounterState => {
switch (action.type) {
case INCREMENT:
return { value: state.value + 1 };
case DECREMENT:
return { value: state.value - 1 };
default:
return state;
}
}
export const store = createStore(reducer);
store.subscribe(render);
function render() {
ReactDOM.render(
(<App />),
document.getElementById('root')
);
}
render();
App.tsx:
import React from 'react';
import logo from './logo.svg';
import styles from './App.module.css';
import { DECREMENT, INCREMENT, store } from './index'
function App() {
const handleDecrement = () => {
store.dispatch({
type: DECREMENT
})
}
const handleIncrement = () => {
store.dispatch({
type: INCREMENT
})
}
return (
<div className={styles.App}>
<header className={styles['App-header']}>
<img src={logo} className={styles['App-logo']} alt="logo" />
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={handleDecrement}
>
-
</button>
<span className={styles.value}>{store.getState().value}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={handleIncrement}
>
+
</button>
</div>
</div>
</header>
</div>
);
}
export default App;
上面的代码我们我们主要做了这么几件事:
- 通过导入 redux 中的
createStore
方法创建 store,createStore
方法接收一个reducer
函数作为参数, 通过调用store.subscribe
来监听 store 改变从而重新 render reducer
是一个函数, 参数为state
和action
, 方法的返回值是新的 statereducer
中通过不同的action.type
操作数据返回新的 statecreateStore
的返回值 store, 具有几个方法:- getState(), 返回整个状态树
- dispatch(action), 派发状态, 返回值是这个 action
- subscribe(callback), 监听 state 改变的函数, 返回值是一个取消监听的函数
可以看到在 redux
中我们通过 createStore
来创建 store
, 接下来我们用代码来实现这个 createStore
函数:
export default function createStore(reducer: Function) {
let state: any;
const listeners: Function[] = [];
function getState() {
return state;
}
function dispatch(action: { type: string; payload?: any }) {
state = reducer(state, action);
listeners.forEach((listener) => listener());
}
function subscribe(listener: Function) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
// 派发一个 action, type 是一个 前缀加随机字符串, 目的是为了初始化 state
dispatch({ type: '@@redux/INIT' });
return {
getState,
dispatch,
subscribe,
};
}
分析 redux
的核心函数 createStore
,可以看到实际上 redux
使用了 单例模式
内部创建了一个 state 变量用于存储 store, 并通过必包持久化这个 state 变量, 使得用户在任意地方都可以通过 getState
方法来获取 store
, 通过 dispatch
方法来派发动作从而改变 state
。
createStore
内部同时也使用了 发布订阅
模式, 让外部能添加监听函数来监听 state
的数据改变。
bindActionCreators
重复写诸如 { type: 'INCREMENT' }
这样的 action 势必会带来很多麻烦, 也容易出错, 所以可以使用 bindActionCreators
来优化代码
使用方式:
index.tsx:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore, bindActionCreators } from 'redux'
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
const increment = () => ({
type: INCREMENT
})
const decrement = () => ({
type: DECREMENT
})
const actionCreators = { increment, decrement }
interface CounterState {
value: number
}
const initState: CounterState = {
value: 0,
}
const reducer = (state = initState, action: any): CounterState => {
switch (action.type) {
case INCREMENT:
return { value: state.value + 1 };
case DECREMENT:
return { value: state.value - 1 };
default:
return state;
}
}
export const store = createStore(reducer);
export const boundActions = bindActionCreators(actionCreators, store.dispatch);
store.subscribe(render);
function render() {
ReactDOM.render(
(<App />),
document.getElementById('root')
);
}
render();
App.tsx:
import React from 'react';
import logo from './logo.svg';
import styles from './App.module.css';
import { store, boundActions } from './index'
function App() {
const handleDecrement = () => {
boundActions.decrement()
}
const handleIncrement = () => {
boundActions.increment()
}
return (
<div className={styles.App}>
<header className={styles['App-header']}>
<img src={logo} className={styles['App-logo']} alt="logo" />
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={handleDecrement}
>
-
</button>
<span className={styles.value}>{store.getState().value}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={handleIncrement}
>
+
</button>
</div>
</div>
</header>
</div>
);
}
export default App;
实现:
- bindActionCreators 接收一个 actionCreators 对象作为第一个参数, 该对象是所有 actionCreator 的集合; 接收一个 dispatch 作为第二个参数, 该参数实际上就是 store.dispatch
- 通过遍历 bindActionCreators 的属性, 给 bindActionCreators 的返回值赋值对应的属性, 值为一个函数, 该函数接收 actionCreator 的参数, 函数体中会派发对应的动作
function bindActionCreator(actionCreator: Function, dispatch: Function) {
return function (...rest: any[]) {
return dispatch(actionCreator(...rest));
}
}
export default function bindActionCreators(actionCreators: Record<string, Function>, dispatch: Function) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch);
}
const boundActionCreators: any = {};
for (let key in actionCreators) {
boundActionCreators[key] = bindActionCreator(actionCreators[key], dispatch);
}
return boundActionCreators;
}
combineReducers
随着项目的庞大, state 中的数据会越来越大, 如果每个模块的 state 都写在一起通过同一个 reducer 来处理的话那代码将变得难以维护。有没有可以将 reducer 或者 state 分割开来的机制?答案是有的, 可以通过 combineReducers
将 reducer 函数拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。
使用方式:
index.tsx:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore, bindActionCreators, combineReducers } from 'redux'
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
const increment = () => ({
type: INCREMENT
})
const decrement = () => ({
type: DECREMENT
})
const actionCreators = { increment, decrement }
interface CounterState {
value: number
}
const counterInitState: CounterState = {
value: 0,
}
const counterReducer = (state = counterInitState, action: any): CounterState => {
switch (action.type) {
case INCREMENT:
return { value: state.value + 1 };
case DECREMENT:
return { value: state.value - 1 };
default:
return state;
}
}
interface TodoState {
todos: string[]
}
const todoInitState: TodoState = {
todos: []
}
const ADD_TODO = 'ADD_TODO'
const addTodo = () => ({ type: ADD_TODO });
const todoActionsCreators = { addTodo }
const todoReducer = (state = todoInitState, action: any): TodoState => {
switch (action.type) {
case ADD_TODO:
return { todos: ['Use Redux'] };
default:
return state;
}
}
const rootReducer = combineReducers({
counter: counterReducer,
todo: todoReducer
})
export const store = createStore(rootReducer);
export const boundActions = bindActionCreators(actionCreators, store.dispatch);
export const todoActions = bindActionCreators(todoActionsCreators, store.dispatch);
store.subscribe(render);
function render() {
ReactDOM.render(
(<App />),
document.getElementById('root')
);
}
render();
App.tsx:
import React from 'react';
import logo from './logo.svg';
import styles from './App.module.css';
import { store, boundActions, todoActions } from './index'
function App() {
const handleDecrement = () => {
boundActions.decrement()
}
const handleIncrement = () => {
boundActions.increment()
}
const handleAddTodo = () => {
todoActions.addTodo()
}
return (
<div className={styles.App}>
<header className={styles['App-header']}>
<img src={logo} className={styles['App-logo']} alt="logo" />
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={handleDecrement}
>
-
</button>
<span className={styles.value}>{store.getState().counter.value}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={handleIncrement}
>
+
</button>
</div>
<div className={styles.row}>
<ul>
todos:
{
store.getState().todo.todos.map((item) => (<li key={item}>{item}</li>))
}
</ul>
<button
className={styles.button}
aria-label="Add todo"
onClick={handleAddTodo}
>
add todo
</button>
</div>
</div>
</header>
</div>
);
}
export default App;
实现:
- combineReducers 接收一个对象作为参数, 该对象的 key 是分 reducer 的key, 值是各个分割的 reducer
- combineReducers 的返回值是一个函数 combination, 这个 combination 函数实际上就是总的 reducer
- combination 函数内部会根据 reducers 的 key 聚合成新的 state, 并且在每次派发对象时会调用分 reducer 计算最终的 state
export default function combineReducers(reducers: Record<string, Function>) {
return function combination(state = {} as any, actions: { type: string; payload: any }) {
const nextState: any = {};
for (let key in reducers) {
nextState[key] = reducers[key](state[key], actions);
}
return nextState;
};
}
以上就是 redux 核心方法的使用和原理, redux
的核心逻辑还是比较简单的, 所以 redux
这个库还是比较可靠不容易出bug。总结redux
的核心概念,我们发现redux
实际上是集中维护了一个state
作为整个应用的状态, 并且只能通过dispatch
action
来改变状态,并且这个状态是一个不可变数据,也就是说每次状态改变时新旧两个state
都是不同的对象。因此redux
具有如下特点:
- 状态变更可追踪,利于调试也利于定位问题
- 状态可持久化,基于不可变数据可以很方便的将某一个状态作为快照保存起来
- 可以很方便地实现撤销重做,无论是基于命令的重做还是基于快照的重做在redux中都能很容易实现
- 数据与视图分离,通过暴露监听函数来驱动视图更新,所以无论是那种ui库其实都能用
- 生态完善,强大的中间件支持能力
本文介绍了
redux
的基本使用方法,但是在react
中使用仍然不是很方便需要手动地监听state
数据来render
整个UI树, 并且在组件中获取store
中的数据仍然不是很方便, 下一篇文章react-redux 使用与实现原理将介绍使用react-redux
来订阅 store、检查更新数据和触发重新渲染并介绍其原理。