跳转至

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 > 
箭头函数的简写形式

单个参数时可以省略括号;函数体只有一行表达式时,可以省略 {}return

fn => fn()

等价于

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

代码:

/**
 * @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 的拆分是社区最佳实践,不是强制标准