跳转至

Redux

写在之前

纯函数与副作用

在学习 ReactRedux 过程中,常常看到 纯函数 (Pure Function)副作用 (Side Effect),理解这两个概念可以更好的理解官方文档和他们代码的实现逻辑。

纯函数必须满足两个条件:

  1. 输出只取决于输入参数, 同样的输入永远得到同样的输出;
  2. 没有副作用(No Side Effects),不改变函数外部任何状态

例如:

1
2
3
4
5
function add(a, b) {
    console.log("......");

    return a + b;
}

满足第 1 个条件(参数 a,b 确定,返回值确定),但不满足第 2 个条件(输出了 log,改变了函数外面(side)的状态;

不是纯函数示例:

  • 依赖外部变量,不可预测

    1
    2
    3
    4
    5
    let x = 10;
    
    function add(a) {
        return a + x;       // x 外部变量
    }
    
  • 改变全局变量,产生副作用

    1
    2
    3
    4
    5
    let count = 0;
    
    function inc() {
        count++;
    }
    
  • 修改参数

    1
    2
    3
    function pushItem(arr, item) {
        arr.push(item); 
    }
    
  • 产生随机性

    1
    2
    3
    function getRandom() {
        return Math.random();
    }
    
  • 打印日志、访问IO、网络请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function foo() {
        console.log("hello"); // 副作用
    }
    
    /* ----------------------------- */
    
    function loadData() {
        return fetch('/api'); // 副作用
    }
    

Note

redux reducer 一定要是纯函数, 因为 state 的更新需要:

  • 可预测;
  • 可回滚;
  • 可重放;
  • 可比较;
  • 可记录每步状态历史

Redux DevTools 能"回放时间线",就是因为 reducer 是纯函数:

  • 给定相同的旧 state 和相同的 action, reducer 永远返回同样的 nextState

副作用(Side Effects)

任何在函数之外,对系统状态产生影响的行为,都叫副作用。

副作用的本质

函数不仅仅返回一段值,而是对外界造成某种影响。所有影响"外部世界"的行为,都算副作用

常见副作用行为列表:

  • 改变外部变量(包括参数);
  • 修改传入对象(数组、对象 mutable);
  • 打印日志;
  • 网络请求 / IO;
  • 存取 LocalStorage / Cookie;
  • DOM 操作;
  • 访问系统时间 / 随机数;
  • 数据库读写 / 文件操作 / Socket;

Note

Redux 官方明确:

  • reducer 禁止产生任何副作用
  • 函数只能做一件事:计算新的 state

React 与 Redux

  1. React State 是局部状态

    React 的哲学:状态只属于产生它的 UI 区域
    React 的设计目标:组件驱动 UI, State 是和组件绑定的:

    • 挂在组件实例上;
    • 生命周期由组件决定;
    • 组件销毁, state 消失;
    • 这是一种“局部封装”

    React 只负责渲染层:

    • React 的工作是把 state 映射到 DOM;
    • 不关心业务逻辑、全局数据、缓存、跨页面状态;
    • 所以它的状态模型是:组件 → 局部 → 生命周期绑定 → 瞬时状态

    Note

    React State 的边界 == 组件边界。 它不是应用状态,它是 UI 层状态。

  2. Redux State 是全局状态

    Redux 不是 UI 框架,它是应用状态容器。它解决的问题:

    • 多页面共享数据
    • 跨组件状态同步
    • 消息/异步请求状态
    • 持久化 / Debug / 时光回溯

    Redux 核心理念来自 状态机 + 事件溯源:(prevState, action) => nextState

    Note

    在 Redux 开来,组件应该是状态消费者,不是状态拥有者

初识状态管理

  • 所谓"状态"就是数据在某一时刻的
  • "状态变化"就是数据从一个值变成另一个值;
  • 状态管理就是记录这些变化,使它们变得可控可预测可调试可追溯,并能驱动界面渲染
  • 为了实现这些目标,我们不能直接修改原始数据,而是先复制一份,在复制后的数据上进行修改。这个不修改原数据,而修改副本的做法,称为不可变性(immutability), 参考可变更新与不可变更新
  • Redux 就是一种实现这种状态管理的工具,它提供了一套方法来管理、记录和控制状态变化。

Note

Redux 的核心理念: Redux 把状态放到一个统一的地方,用一套严格规则管理变化.

  • Redux 通过一个全局 Store 管理状态,并约束状态如何更新;
  • 状态只允许通过 action 去变更;
  • 变更逻辑由 用户定义的 reducer 函数 决定

Redux 的三个基本工具

  1. Store 状态仓库

    • 存放所有共享数据(即:状态)
    • 存放改变数据的成员方法(即:成员函数)
    • 间接存放自定义的 reducer 函数(说间接,是因为自定义的数据处理函数被成员函数 dispatch() 调用来实现数据变化)
  2. Action 意图描述

    • 告诉 reducer 要做什么
    • 不更新状态
    • 只是一个说明对象 { type: "...", payload: ... }
  3. Reducer 纯逻辑

    • reducer 函数是用户自定义的函数,其中处理的 action 与 2 中用户自定义的 action 对应;
    • 根据 action 把数据"复制" 一份,并在新的数据上修改数据,即:完成了状态更新;
    • 纯函数:对输入做处理、输出新状态(即:输出一个新的数据,不仅数值是更新后的,而是整个数据是新的一份),不修改原状态

使用 Redux 的一个最小例子

Redux 的最小闭环:

  • Store 存状态;
  • Action 描述意图
  • 自定义的 Reducer 函数counterReducer()处理变化

counter.js 代码:

/**
 * @file counter.js
 *
 * @description
 * A simple counter implementation using Redux-like state management.
 * Demonstrates how to set up a store, define a reducer,
 * and dispatch actions to update the state.
 */

// 1. Initial state
const initialState = {
    count: 0,
    winner: null
}

 // 2. reducer function
function counterReducer(state = initialState, action) {
    switch (action.type) {
        case 'INCREMENT':
            return {
                ...state,                   // Preserve other state properties
                count: state.count + 1
            }
        case 'SET_WINNER':
            return {
                ...state,
                winner: action.payload      // Set winner
            }
        default:
            return state
    }
}

/**
 * 3. Create store
 * Using Redux's createStore to create a store with
 * the reducer and initial state
 * It is the entry point of the state management system
 * and holds the entire state tree of the application.
 * All state transitions happen through this store.
 */
import { createStore } from 'redux'
const store = createStore(counterReducer)

console.log(store)

// 4. Dispatch actions
store.dispatch({ type: 'INCREMENT' })
console.log(store.getState())           // { count: 1, winner: null }

store.dispatch({ type: 'SET_WINNER', payload: 'Alice' })
console.log(store.getState())           // { count: 1, winner: 'Alice' }

store.dispatch({ type: 'INCREMENT' })
console.log(store.getState())           // { count: 2, winner: 'Alice' }

运行结果:

PS > node .\counter.js
{
  dispatch: [Function: dispatch],
  subscribe: [Function: subscribe],
  getState: [Function: getState],
  replaceReducer: [Function: replaceReducer],
  '@@observable': [Function: observable]
}
{ count: 1, winner: null }
{ count: 1, winner: 'Alice' }
{ count: 2, winner: 'Alice' }
PS > 

必要的解释:

用 JS 写一个原生的类状态管理

代码:

/**
 * @file timeTravel.js
 *
 * @description A simple time-traveling counter implementation in JavaScript to
 * simulate Redux State Management as a closure.
 *
 * @returns { increment, decrement, undo, redo, get, debug }
 */
function createTimeTravelCounter() {
    /**
     * Local variables to hold the history of states and the current index
     * history:
     *  - An array to store the sequence of counter values
     *  - which can not be changed directly from outside as Redux state
     *  - but only through the provided methods as Redux actions
     *
     * index:
     *  - A pointer to the current position in the history array
     */
    let history = [0];
    let index = 0;

    return {
        increment() {
            const newValue = history[index] + 1;

            /**
             * array.slice(0, index + 1) means we take the history array
             * from the start up to the current index (inclusive)
             *
             * When a new action is taken, we need to discard any "future" states
             *
             * For example, if history = [0, 1, 2] and index = 1 (pointing to value 1),
             * and we call increment(), we need to remove the value 2 from history
             * before adding the new value 2 (1 + 1).
             */
            history = history.slice(0, index + 1);

            /**
             * array.push(newValue) adds the new value to the end of the history array
             *
             * Now we can safely add the new value to the history
             * and move the index forward
             */
            history.push(newValue);
            index++;

            return this.get();
        },

        decrement() {
            const newValue = history[index] - 1;

            history = history.slice(0, index + 1);

            history.push(newValue);
            index++;

            return this.get();
        },

        undo() {
            if (index > 0) {
                index--;
            }

            return this.get();
        },

        redo() {
            if (index < history.length - 1) {
                index++;
            }

            return this.get();
        },

        get() {
            return history[index];
        },

        debug() {
            return { history, index };
        }
    };
}

const counter = createTimeTravelCounter();

console.log("\ncounter:\n", counter);

console.log("\nType of counter:", typeof counter);

console.log("\nMethods:", Object.keys(counter));

console.log("\nInitial value:", counter.get());
console.log("Increment to:", counter.increment());
console.log("Increment to:", counter.increment());
console.log("Undo to:", counter.undo());
console.log("Redo to:", counter.redo());
console.log("Decrement to:", counter.decrement());

console.log("\nDebug info:", counter.debug());

运行结果:

PS > node .\timeTravel.js

counter:
{
  increment: [Function: increment],
  decrement: [Function: decrement],
  undo: [Function: undo],
  redo: [Function: redo],
  get: [Function: get],
  debug: [Function: debug]
}

Type of counter: object

Methods: [ 'increment', 'decrement', 'undo', 'redo', 'get', 'debug' ]

Initial value: 0
Increment to: 1
Increment to: 2
Undo to: 1
Redo to: 2
Decrement to: 1

Debug info: { history: [ 0, 1, 2, 1 ], index: 3 }
PS > 
箭头函数的简写形式
fn => fn()

等价于

1
2
3
(fn) => {
    return fn();
}
1. 单个参数 --> 可省略括号
2. 函数体只有一行表达式 → 可省略 {} 和 return

代码:

/**
 * @file miniRedux.js
 *
 * @description A minimal Redux-like state management implementation in JS
 * with time-traveling capabilities.
 *
 * @param {function} reducer, a function that determines how
 * the state changes in response to actions *
 * @param {object} initialState, the initial state of the store
 *
 * @returns { dispatch, getState, subscribe, undo, redo }
 */
function createStore(reducer, initialState) {
    let currentState = initialState;

    /**
     * listeners: an array to hold subscriber functions
     * that will be called whenever the state changes
     */
    let listeners = [];
    let history = [initialState];
    let index = 0;

    function getState() {
        return history[index];
    }

    /**
     *
     * @param {object}  action, an object describing the change to be made
     *
     * @return {} no return value
     *
     * The dispatch function applies an action to the current state
     * using the reducer function to produce a new state.
     *
     * It also manages the history array to enable time-traveling
     * by storing each new state and updating the index accordingly.
     */
    function dispatch(action) {
        const prev = getState();

        /**
         * Call the reducer with the previous state and the action
         * to get the next state
         * @param {object} prev, the previous state before applying the action
         * @param {object} action, the action to be applied
         *
         * @return {object} next, the new state after applying the action
         *
         * The reducer is a pure function that takes the previous state
         * and an action, and returns the next state without mutating
         * the previous state.
         *
         * dispatch() does not modify prev directly; instead, it relies on
         * the reducer to produce a new state object.
         */
        const next = reducer(prev, action);

        index++;
        history = history.slice(0, index);
        history.push(next);

        currentState = next;

        /**
         * Important: Notify all subscribers about the state change
         * by calling each listener function in the listeners array.
         *
         * This mimics Redux's subscribe mechanism.
         */
        listeners.forEach(fn => fn());
    }

    /**
     *
     * @param {*} listener, a function to be called when the state changes
     * @returns { function } a function to unsubscribe the listener
     */
    function subscribe(listener) {
        listeners.push(listener);

        return () => {
            listeners = listeners.filter(fn => fn !== listener);
        }
    }

    function undo() {
        if (index > 0) index--;
    }

    function redo() {
        if (index < history.length - 1) index++;
    }

    return {
        dispatch,
        getState,
        subscribe,
        undo,
        redo,
    };
}

/**
 * @param {*} state, current state
 * @param {*} action, an action object describing the change
 *
 * @returns { object } the new state after applying the action
 */
function counterReducer(state, action) {
    switch (action.type) {
        case "INC":
            return { ...state, count: state.count + 1 };
        case "SET_WINNER":
            return { ...state, winner: action.payload };
        default:
            return state;
    }
}

/**
 * Create the store with the reducer and initial state
 * It is the entry point of the state management system
 * and holds the entire state tree of the application.
 * All state transitions happen through this store.
 */
const store = createStore(
    counterReducer,                 // reducer
    { count: 0, winner: null }      // initial state
);

console.log("store:\n", store)

console.log(store.getState());          // { count: 0, winner: null }

store.dispatch({ type: "INC" });
console.log(store.getState());          // { count: 1, winner: null }

store.dispatch({ type: "SET_WINNER", payload: "Alice" });
console.log(store.getState());          // { count: 1, winner: "Alice" }

store.dispatch({ type: "INC" });
store.dispatch({ type: "INC" });
store.dispatch({ type: "INC" });

console.log(store.getState());          // { count: 4, winner: "Alice" }

store.undo();
console.log(store.getState());          // { count: 3, winner: "Alice" }

store.undo();
console.log(store.getState());          // { count: 2, winner: "Alice" }

store.redo();
console.log(store.getState());          // { count: 3, winner: "Alice" }

/**
 * @description Example of subscribing to state changes
 * When the state changes, the subscribed function will
 * be called and log the new state.
 *
 * @returns { function } unsubscribe function to stop listening to state changes
 */
const unsubscribe = store.subscribe(() => {
    console.log("State changed:", store.getState());
});

// Logs: State changed: { count: 4, winner: "Alice" }
store.dispatch({ type: "INC" });

// Logs: State changed: { count: 5, winner: "Alice" }
store.dispatch({ type: "INC" });

// Logs: State changed: { count: 5, winner: "Bob" }
store.dispatch({ type: "SET_WINNER", payload: "Bob" });

unsubscribe();

// No log this time
store.dispatch({ type: "INC" });

运行结果:

PS > node .\miniRedux.js
store:
 {
  dispatch: [Function: dispatch],
  getState: [Function: getState],
  subscribe: [Function: subscribe],
  undo: [Function: undo],
  redo: [Function: redo]
}
{ count: 0, winner: null }
{ count: 1, winner: null }
{ count: 1, winner: 'Alice' }
{ count: 4, winner: 'Alice' }
{ count: 3, winner: 'Alice' }
{ count: 2, winner: 'Alice' }
{ count: 3, winner: 'Alice' }
State changed: { count: 4, winner: 'Alice' }
State changed: { count: 5, winner: 'Alice' }
State changed: { count: 5, winner: 'Bob' }
PS > 

Note

listeners.forEach(fn => fn());

等价于

1
2
3
listeners.forEach((fn) => {
    return fn();
});

Redux

Redux reducer

Note

reduce 在这里不是"减少"的意思,请参考下面 Merriam-Webster 的部分意思:

  • to draw together or cause to converge: CONSOLIDATE
    // reduce all the questions to one
  • to bring to a specified state or condition
    // the impact of the movie reduced them to tears
  • to change the denominations or form of without changing the value
  • to add one or more electrons to (an atom or ion or molecule)

Redux reducer 名称借用了数组的 array.reduce() 方法的名称,因为状态是"持续累积"的:

  • UI 用户操作
  • 网络请求返回
  • 定时任务触发
  • WebSocket 推送

这些都是"事件序列",一个一个发生,驱动 state 演变。

Redux 并没有像 array.reduce() 一样"把数组 reduce 一遍",它借用了 reduce 最核心的抽象:把一个序列逐步累积为一个最终值。区别只是:

  • Array.reduce 的序列是数组元素
  • Redux.reduce 的序列是 Action

Note

状态不是被完整覆盖,而是不断更新累积

Redux reducer 必须是:

  • 纯函数
  • 不可变更新
  • 无副作用

Redux 的四层次架构

责任 作用 是否官方强制
store 状态容器 集成 reducer/middleware
slices 状态域模型 定义每块状态与变更规则 ✔ 推荐(RTK)
selectors 状态访问 API 只读抽象 + 性能 ✖ 但强烈提倡
hooks UI 适配入口 统一 dispatch/select 类型 ✖ 业界共识
  1. Store 层:管理状态生命周期 (系统集成器)

    管理状态系统 —— 根节点与配置中心

  2. Slice 层:描述状态与业务变更(状态域模型)

    定义状态空间 —— 状态是什么 + 怎样变化

  3. Selector 层:抽象数据访问(状态访问 API)

    读状态的唯一方式 —— 稳定、高性能、无副作用

  4. Hook 层:提供 UI 入口(状态访问适配器(React 入口))

    • UI 访问状态的接口 —— 弱耦合、类型安全、业务方便
    • React-Redux 官方提供的“React 接口”, React-Redux 提供:

      • useDispatch()
      • useSelector()

Note

Redux 官方“强制”的只有两件事:

  • Store
  • Reducer / Slice(Redux Toolkit)

selectors、hooks 的拆分是社区最佳实践,不是强制标准