Redux

1 Redux 概述与核心概念

Redux 是一个用于 JavaScript 应用程序的可预测状态容器,它以集中式 Store 的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。虽然常与 React 搭配使用,但 Redux 本身是一个独立的库,可以与其他视图库一起使用。

1.1 Redux 三大设计原则

  1. 单一数据源 :整个应用的 state 被存储在单个 store 的对象树中。这使得应用程序的状态更加 predictable 且易于调试。
  2. State 是只读的 :改变 state 的唯一方法 是触发 action(一个描述发生了什么的对象)。这确保了视图和网络请求都不能直接修改 state。
  3. 使用纯函数进行更改 :为了指定 state 如何被 action 转换,你需要编写 reducers(纯函数),它接收先前的 state 和 action,并返回新的 state。

1.2 核心术语

术语 描述 示例
Action 具有 type 字段的普通对象,描述应用中发生的事件 { type: 'INCREMENT', payload: 2 }
Action Creator 创建并返回 action 对象的函数 const increment = (amount) => ({ type: 'INCREMENT', payload: amount })
Reducer 纯函数,接收当前 state 和 action,返回新 state (state, action) => newState
Store 集中管理状态的对象 createStore(reducer) 创建
Dispatch Store 的方法,用于更新 state 的唯一方式,接收一个 action 对象 store.dispatch(action)
Selector 函数,从 store 状态树中提取指定的片段 const selectCounter = state => state.value

2 Redux 源码结构与核心实现

Redux 源码非常简洁(约 2kB),主要集中在 src 目录下的几个文件:

bash 复制代码
src
├── applyMiddleware.js
├── bindActionCreators.js
├── combineReducers.js
├── compose.js
├── createStore.js
└── index.js              # 入口文件

2.1 createStore 源码解析

createStore 是 Redux 的核心,用于创建一个状态容器 store。

javascript 复制代码
// 简化后的 createStore 核心结构
export default function createStore(reducer, preloadedState, enhancer) {
  // 参数处理:支持省略 preloadedState
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState;
    preloadedState = undefined;
  }
  
  // 如果传入了 enhancer(如 applyMiddleware),则用 enhancer 增强 createStore
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.');
    }
    // 注意:这里返回的是增强后的 store,不再继续执行下面的逻辑
    return enhancer(createStore)(reducer, preloadedState);
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.');
  }

  let currentReducer = reducer;     // 当前 reducer 函数
  let currentState = preloadedState; // 当前 state
  let currentListeners = [];        // 当前监听器列表
  let nextListeners = currentListeners; 
  let isDispatching = false;        // 是否正在 dispatch 的标记

  // 确保 nextListeners 是可修改的副本
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice();
    }
  }

  // 获取当前 state
  function getState() {
    if (isDispatching) {
      throw new Error('You may not call store.getState() while the reducer is executing...');
    }
    return currentState;
  }

  // 订阅 state 变化
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.');
    }
    if (isDispatching) {
      throw new Error('You may not call store.subscribe() while the reducer is executing...');
    }
    
    let isSubscribed = true;
    ensureCanMutateNextListeners();
    nextListeners.push(listener); // 将监听器加入列表
    
    // 返回取消订阅的函数
    return function unsubscribe() {
      if (!isSubscribed) return;
      if (isDispatching) {
        throw new Error('You may not unsubscribe from a store listener while the reducer is executing...');
      }
      
      isSubscribed = false;
      ensureCanMutateNextListeners();
      const index = nextListeners.indexOf(listener);
      nextListeners.splice(index, 1); // 从监听器列表中移除
    };
  }

  // 分发 action,触发 state 更新的唯一方式
  function dispatch(action) {
    if (!isPlainObject(action)) { // 检查 action 是否为普通对象
      throw new Error('Actions must be plain objects. Use custom middleware for async actions.');
    }
    if (typeof action.type === 'undefined') {
      throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');
    }
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.');
    }
    
    try {
      isDispatching = true;
      // 关键:将当前 state 和 action 传给 reducer,计算新的 state
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }
    
    // 通知所有订阅者 state 已更改
    const listeners = (currentListeners = nextListeners);
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener(); // 调用监听器(通常用于触发视图更新)
    }
    
    return action; // 返回 action
  }

  // 替换当前 reducer
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.');
    }
    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.INIT }); // 触发 INIT action 初始化新 state
  }

  // 初始化:分发一个初始 action 来设置初始 state
  dispatch({ type: ActionTypes.INIT });

  // 返回 store 的公共 API
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable // 支持 Observable 的符号
  };
}

2.2 combineReducers 源码解析

随着应用变大,通常会将根 reducer 拆分成多个独立的 reducers(每个负责状态的不同部分)。combineReducers 辅助函数将这些 reducers 合并成一个单一的 reducer 函数。

javascript 复制代码
// 简化版的 combineReducers
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);
  const finalReducers = {};
  
  // 过滤出有效的 reducer(函数类型)
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i];
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key];
    }
  }
  
  const finalReducerKeys = Object.keys(finalReducers);

  // 返回合并后的 reducer
  return function combination(state = {}, action) {
    let hasChanged = false; // 标记 state 是否改变
    const nextState = {};
    
    // 遍历每个 reducer
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i];
      const reducer = finalReducers[key];
      const previousStateForKey = state[key]; // 当前 key 的旧 state
      const nextStateForKey = reducer(previousStateForKey, action); // 执行 reducer 计算新 state
      
      if (typeof nextStateForKey === 'undefined') {
        throw new Error(`Reducer "${key}" returned undefined when handling action of type "${action.type}".`);
      }
      
      nextState[key] = nextStateForKey;
      // 检查此部分 state 是否改变(引用比较)
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    
    hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length;
    
    // 如果 state 改变则返回新 state,否则返回旧 state
    return hasChanged ? nextState : state;
  };
}

2.3 applyMiddleware 源码解析

Middleware(中间件)是扩展 Redux 功能的主要方式,它提供了一个第三方扩展点在 dispatch action 和 action 到达 reducer 之间。常用于日志记录、异步操作等。

javascript 复制代码
// 简化版的 applyMiddleware
export default function applyMiddleware(...middlewares) {
  // 返回一个 enhancer,它接收 createStore 并返回增强后的 createStore
  return (createStore) => (reducer, preloadedState, enhancer) => {
    // 先创建 store
    const store = createStore(reducer, preloadedState, enhancer);
    
    let dispatch = () => {
      throw new Error('Dispatching while constructing your middleware is not allowed. ' + 
                    'Other middleware would not be applied to this dispatch.');
    };
    
    // 提供给中间件的 store API
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    };
    
    // 将每个中间件传入 middlewareAPI 执行,得到一串链式函数
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    // 组合中间件:例如 [A, B, C] 变成 A(B(C(dispatch)))
    dispatch = compose(...chain)(store.dispatch);
    
    // 返回增强了 dispatch 的 store
    return {
      ...store,
      dispatch
    };
  };
}

// compose 工具函数:将多个函数组合起来,例如 compose(f, g, h) 将生成 (...args) => f(g(h(...args)))
export default function compose(...funcs) {
  if (funcs.length === 0) return arg => arg;
  if (funcs.length === 1) return funcs[0];
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

中间件的模板代码通常遵循以下结构:

javascript 复制代码
const middleware = store => next => action => {
  // 在 dispatch 前可以做些什么
  console.log('dispatching', action);
  
  // 调用下一个中间件(或最终的 store.dispatch),传递 action
  let result = next(action);
  
  // 在 dispatch 后可以做些什么
  console.log('next state', store.getState());
  
  return result; // 通常返回 dispatch 的结果
};

3 React-Redux 原理与源码分析

React-Redux 是连接 React 和 Redux 的官方绑定库,它使得 React 组件能够方便地从 Redux store 中读取数据,并 dispatch actions。

3.1 Provider 组件

Provider 是一个 React 组件,它使用 React 的 Context API 将 Redux store 传递给所有子孙组件。

jsx 复制代码
// Provider 简化源码
function Provider({ store, context, children }) {
  // 利用 useMemo 根据 store 变化创建 contextValue
  const contextValue = useMemo(() => {
    // 创建一个根 Subscription(订阅)实例
    const subscription = new Subscription(store);
    subscription.onStateChange = subscription.notifyNestedSubs;
    return { store, subscription };
  }, [store]);

  const previousState = useMemo(() => store.getState(), [store]);

  useEffect(() => {
    const { subscription } = contextValue;
    subscription.trySubscribe(); // 发起订阅

    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs(); // 如果 state 改变,立即通知订阅者
    }
    
    // 清理函数:组件卸载时取消订阅
    return () => {
      subscription.tryUnsubscribe();
      subscription.onStateChange = null;
    };
  }, [contextValue, previousState]);

  const Context = context || ReactReduxContext;

  // 通过 Context.Provider 传递 contextValue
  return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

3.2 connect 高阶组件 (HOC) 与 Hooks

connect 是一个高阶组件,它负责连接 React 组件和 Redux store。

传统 connect 函数简化概念

javascript 复制代码
// connect 函数声明示例
connect(
  mapStateToProps?, 
  mapDispatchToProps?, 
  mergeProps?, 
  options?
)(MyComponent);

现代 React-Redux Hooks: 在现代函数式组件中,更推荐使用 React-Redux 提供的 Hooks:

  • useSelector : 从 store 的 state 中提取所需的数据。

    javascript 复制代码
    const counter = useSelector(state => state.counter);
  • useDispatch : 返回 store 的 dispatch 方法,用于分发 actions。

    javascript 复制代码
    const dispatch = useDispatch();
    // ...
    dispatch({ type: 'INCREMENT' });

3.3 Subscription

Subscription 是 React-Redux 实现嵌套订阅高效更新的核心。它管理着一组监听器(listeners),并在 state 变化时通知它们。

javascript 复制代码
// Subscription 类简化源码
export default class Subscription {
  constructor(store, parentSub) {
    this.store = store;
    this.parentSub = parentSub; // 父级订阅器
    this.unsubscribe = null;
    this.listeners = nullListeners; // 监听器集合
    this.handleChangeWrapper = this.handleChangeWrapper.bind(this);
  }

  // 添加嵌套订阅
  addNestedSub(listener) {
    this.trySubscribe();
    return this.listeners.subscribe(listener); // 将监听器加入列表
  }

  // 通知所有监听器
  notifyNestedSubs() {
    this.listeners.notify();
  }

  // 处理变化的包装器
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange(); // 通常触发组件的更新检查
    }
  }

  // 尝试订阅:如果有父订阅器,则添加到父订阅器,否则直接订阅 store
  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper) // 嵌套订阅
        : this.store.subscribe(this.handleChangeWrapper); // 直接订阅 store
      this.listeners = createListenerCollection(); // 创建监听器集合
    }
  }

  // 取消订阅
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe();
      this.unsubscribe = null;
      this.listeners.clear();
      this.listeners = nullListeners;
    }
  }
}

React-Redux 通过 Provider 创建一个根级的 Subscription,并为每个 connect 后的组件或使用 Hooks 的组件创建嵌套的 Subscription。这样就形成了一个订阅树,当 state 变化时,能够精确地通知到相关的组件,避免不必要的渲染,优化了性能。

4 Redux 中间件与异步操作

默认情况下,Redux 的 dispatch 是同步的。为了处理异步操作(如 API 调用),需要使用中间件。

4.1 redux-thunk 中间件

redux-thunk 是最常用的异步中间件之一,它允许 action creator 返回一个函数(而不仅仅是一个对象)。

源码及其原理

javascript 复制代码
// redux-thunk 核心源码
function createThunkMiddleware(extraArgument) {
  // 中间件标准格式:store => next => action => {}
  return ({ dispatch, getState }) => (next) => (action) => {
    // 如果 action 是一个函数,则执行它,并传入 dispatch 和 getState
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    // 否则,将 action 传递给下一个中间件或最终的 reducer
    return next(action);
  };
}

const thunk = createThunkMiddleware();
export default thunk;

使用示例

javascript 复制代码
// 同步 Action Creator
const incrementSync = (amount) => ({ type: 'INCREMENT', payload: amount });

// 异步 Action Creator (thunk)
const incrementAsync = (amount) => {
  return (dispatch, getState) => { // 返回一个函数(thunk)
    setTimeout(() => {
      dispatch({ type: 'INCREMENT', payload: amount }); // 异步完成后 dispatch
      // 可以在这里访问当前的 state: const currentState = getState();
    }, 1000);
  };
};

// 在组件中分发
dispatch(incrementAsync(5));

4.2 其他异步中间件

除了 redux-thunk,还有其他流行的异步中间件,如 redux-saga(使用 Generator 函数管理复杂的异步流程)和 redux-observable(基于 RxJS 响应式编程)。

5 总结与最佳实践

5.1 Redux 数据流

Redux 遵循严格的单向数据流

  1. 状态存储在唯一的 Store 中。
  2. 视图基于 Store 中的 State 进行渲染。
  3. 当用户交互发生时,视图会 dispatch 一个 Action。
  4. Store 调用 Reducer,并传入当前 State 和 Action。Reducer 返回一个新的 State。
  5. Store 更新为新的 State,并通知所有订阅了 state 变化的视图。
  6. 视图根据新的 State 重新渲染。
graph LR A[View UI] -- dispatches --> B[Action] B -- is handled by --> C[Reducer] C -- updates --> D[Store State] D -- notifies --> A

5.2 不可变更新(Immutability)

Reducer 必须是纯函数 ,且禁止直接修改 state 。你必须使用不可变更新模式。

错误示例

javascript 复制代码
function todoReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      state.push(action.text); // ❌ 错误:直接修改了原 state!
      return state;
    default:
      return state;
  }
}

正确示例(使用展开运算符)

javascript 复制代码
function todoReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.text]; // ✅ 正确:返回新数组
    case 'TOGGLE_TODO':
      return state.map((todo, index) => 
        index === action.index 
          ? { ...todo, completed: !todo.completed } // ✅ 正确:返回新对象
          : todo
      );
    default:
      return state;
  }
}

对于更复杂的不可变更新,可以使用 immer 等库来简化操作。

5.3 开发者工具

Redux DevTools Extension 是一个强大的调试工具,可以记录 action 日志、检查 state 快照、进行"时间旅行"调试等。可通过中间件启用:

javascript 复制代码
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';

const store = createStore(
  reducer,
  composeWithDevTools(applyMiddleware(thunk, logger)) // 使用 composeWithDevTools 包装
);

Redux 的架构和约束虽然在一定程度上增加了代码量,但其带来的可预测性、可调试性和可维护性使其在复杂的大型应用中非常有价值。

相关推荐
艾小码8 小时前
React 渲染流程深度解析(结合 react-reconciler)
前端·javascript·react.js
知识分享小能手9 小时前
React学习教程,从入门到精通, React 入门指南:创建 React 应用程序的语法知识点(7)
前端·javascript·vue.js·学习·react.js·前端框架·anti-design-vue
水冗水孚9 小时前
面对业务需求,多思考一下如何更好实现,不要成为麻木的前端npm调包侠
react.js·npm
qczg_wxg9 小时前
React Native系统组件(二)
javascript·react native·react.js
ZZHow102410 小时前
React前端开发_Day12_极客园移动端项目
前端·笔记·react.js·前端框架·web
前端岳大宝10 小时前
Module Federation
react.js·前端框架·状态模式
安心不心安12 小时前
React Router 6 获取路由参数
前端·javascript·react.js
1024小神1 天前
vue/react项目如何跳转到一个已经写好的html页面
vue.js·react.js·html
Maschera961 天前
扣子同款半固定输入模板的简单解决方案
前端·react.js