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-flow.png

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

基础使用

以下代码只使用 reduxreact 实现一个简单的计数器, 效果如下图:

redux.gif

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;

上面的代码我们我们主要做了这么几件事:

  1. 通过导入 redux 中的 createStore 方法创建 store, createStore 方法接收一个 reducer 函数作为参数, 通过调用 store.subscribe 来监听 store 改变从而重新 render
  2. reducer 是一个函数, 参数为 stateaction, 方法的返回值是新的 state
  3. reducer 中通过不同的 action.type 操作数据返回新的 state
  4. createStore 的返回值 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、检查更新数据和触发重新渲染并介绍其原理。

全部评论

相关推荐

02-17 20:43
西北大学 Java
点赞 评论 收藏
分享
01-23 14:54
同济大学 Java
热爱敲代码的程序媛:给你提几点【专业技能】这个模块里面可优化的地方:1.【具备JVM调优经验】可以去b站上搜一下JVM调优的视频,估计一两个小时凭你的学习能力就能掌握JVM调优的实践方面的技能。2.【MySql优化】MySql这一栏,你去b站或者找个博客看看MySql优化,学一下,如果你本身比较熟悉MySql语句的话,那基本半天时间凭你的学习能力MySql语句优化方面的技能你也能掌握个差不多。以上1,2两点主要是因为我看你专业技能大部分都说的是偏理论,没有写应用。再就是最后,你结合你的项目,想一想你的项目中哪些sql语句是可以用MySql优化的,到时候你面试的时候也好结合着说一下。
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务