从0到1实现react(四):实现useReducer

122 阅读4分钟

仓库地址:github.com/zhuxin0/min…

你是否好奇 React Hooks 的魔法是如何实现的?今天我们就来揭秘 Mini React 中 useReducer 的实现原理,让你一次性搞懂 Hooks 的底层机制!

🚀 引言:什么是 useReducer?

想象一下,你正在玩一个RPG游戏,你的角色有各种状态:血量、魔法值、经验值等。每当你做出不同的行动(攻击、施法、升级),这些状态都会发生相应的变化。

useReducer 就像是这个游戏的状态管理器,它接收你的"行动"(action),然后根据预定义的"规则"(reducer函数),来更新你的"状态"(state)。

// 就像游戏中的状态管理
const [state, dispatch] = useReducer(gameReducer, initialState);

// 执行行动
dispatch({ type: 'ATTACK', damage: 10 });  // 攻击
dispatch({ type: 'HEAL', amount: 20 });    // 治疗

🏗️ 整体架构:Hooks 系统的设计哲学

在深入 useReducer 之前,我们先来看看整个 Hooks 系统是如何设计的。

核心设计理念

  1. 函数式编程:Hooks 让函数组件也能拥有状态
  2. 链表结构:多个 Hook 通过链表连接
  3. Fiber 架构:与 React 的 Fiber 调度系统深度集成

系统流程图

graph TD
    A[函数组件执行] --> B[renderWithHooks]
    B --> C[初始化 Hook 链表]
    C --> D[useReducer 调用]
    D --> E[updateWorkInProgressHook]
    E --> F{是否初次渲染?}
    F -->|是| G[创建新 Hook]
    F -->|否| H[复用已有 Hook]
    G --> I[返回 state 和 dispatch]
    H --> I
    I --> J[用户调用 dispatch]
    J --> K[执行 reducer]
    K --> L[更新 Hook 状态]
    L --> M[触发重新渲染]
    M --> N[scheduleUpdateOnFiber]

🔍 核心实现:解剖 useReducer

让我们一步步分析 useReducer 的实现,就像拆解一个精密的钟表机械。

1. Hook 链表的管理

首先,我们需要了解 Hook 是如何存储和管理的:

// 全局变量,管理当前的 Fiber 和 Hook
let currentFiber = null;
let workInProgressHook = null;

这两个变量就像是"当前工作台"和"当前工具":

  • currentFiber:当前正在处理的组件 Fiber 节点
  • workInProgressHook:当前正在处理的 Hook

2. renderWithHooks:Hook 系统的启动器

function renderWithHooks(wip) {
  currentFiber = wip;           // 设置当前工作的 Fiber
  workInProgressHook = null;    // 重置 Hook 指针
  wip.memoizedState = null;     // 清空之前的状态
}

这个函数就像是每次函数组件执行前的"准备工作",确保 Hook 系统处于正确的初始状态。

3. updateWorkInProgressHook:Hook 链表的核心

这是整个 Hook 系统最精妙的部分:

function updateWorkInProgressHook() {
  let hook;

  // 🎯 初次渲染:创建新的 Hook
  if (!currentFiber.alternate) {
    hook = {
      memoizedState: null,  // 存储 Hook 的状态
      next: null,          // 指向下一个 Hook
    };

    if (!workInProgressHook) {
      // 第一个 Hook
      currentFiber.memoizedState = hook;
      workInProgressHook = hook;
    } else {
      // 后续 Hook,形成链表
      workInProgressHook.next = hook;
      workInProgressHook = hook;
    }
  } 
  // 🔄 更新渲染:复用已有的 Hook
  else {
    currentFiber.memoizedState = currentFiber.alternate.memoizedState;
    if (!workInProgressHook) {
      hook = workInProgressHook = currentFiber.alternate.memoizedState;
    } else {
      hook = workInProgressHook = workInProgressHook.next;
    }
  }
  
  return hook;
}

Hook 链表结构图

graph LR
    A[Fiber Node] --> B[memoizedState]
    B --> C[Hook 1: useReducer]
    C --> D[Hook 2: useState]
    D --> E[Hook 3: useEffect]
    E --> F[null]
    
    C --> C1[memoizedState: state值]
    C --> C2[next: 指向下一个Hook]
    
    D --> D1[memoizedState: state值]
    D --> D2[next: 指向下一个Hook]

4. useReducer:状态管理的核心

现在来看 useReducer 的具体实现:

function useReducer(reducer, initialState) {
  // 🎯 获取当前 Hook
  const hook = updateWorkInProgressHook();
  
  // 🚀 初次渲染:设置初始状态
  if (!currentFiber?.alternate) {
    hook.memoizedState = initialState;
  }
  
  // 🎮 创建 dispatch 函数
  function dispatch(action) {
    // 执行 reducer,计算新状态
    hook.memoizedState = reducer(hook.memoizedState, action);
    
    // 创建新的 Fiber 树用于比较
    currentFiber.alternate = { ...currentFiber };
    
    // 触发重新渲染
    scheduleUpdateOnFiber(currentFiber);
  }
  
  // 返回当前状态和派发函数
  return [hook.memoizedState, dispatch];
}

🎭 完整的渲染流程

让我们通过一个完整的例子来看看整个流程:

示例代码

function Counter() {
  const [count, setCount] = useReducer((state, action) => {
    switch (action.type) {
      case 'increment':
        return state + 1;
      case 'decrement':
        return state - 1;
      default:
        return state;
    }
  }, 0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount({ type: 'increment' })}>+</button>
    </div>
  );
}

完整流程图

sequenceDiagram
    participant User as 用户
    participant Component as Counter组件
    participant Hooks as Hooks系统
    participant Fiber as Fiber调度器
    participant DOM as DOM

    User->>Component: 点击按钮
    Component->>Hooks: dispatch({type: 'increment'})
    Hooks->>Hooks: 执行 reducer 函数
    Hooks->>Hooks: 更新 hook.memoizedState
    Hooks->>Fiber: scheduleUpdateOnFiber(currentFiber)
    Fiber->>Fiber: 启动工作循环
    Fiber->>Component: 重新执行组件函数
    Component->>Hooks: 再次调用 useReducer
    Hooks->>Hooks: 复用已有 Hook,返回新状态
    Component->>Fiber: 返回新的虚拟DOM
    Fiber->>DOM: 更新真实DOM
    DOM->>User: 显示新的计数值

🔧 Fiber 调度系统的配合

useReducer 的重新渲染是如何触发的呢?这就涉及到 Fiber 调度系统:

scheduleUpdateOnFiber:调度的入口

export function scheduleUpdateOnFiber(fiber) {
  wip = fiber;        // 设置工作中的 Fiber
  wipRoot = fiber;    // 设置根 Fiber
  
  scheduleCallback(wookloop);  // 调度工作循环
}

工作循环:workLoop

function wookloop() {
  while (wip) {
    performUnitOfWork();  // 执行单元工作
  }
  if (!wip && wipRoot) {
    commitRoot(wipRoot);  // 提交更改到 DOM
  }
}

Fiber 工作流程图

graph TD
    A[dispatch 调用] --> B[scheduleUpdateOnFiber]
    B --> C[设置 wip 和 wipRoot]
    C --> D[scheduleCallback]
    D --> E[workLoop 执行]
    E --> F[performUnitOfWork]
    F --> G{还有子节点?}
    G -->|是| H[处理子节点]
    G -->|否| I{还有兄弟节点?}
    I -->|是| J[处理兄弟节点]
    I -->|否| K[回到父节点]
    H --> F
    J --> F
    K --> L{是否完成?}
    L -->|否| F
    L -->|是| M[commitRoot]
    M --> N[更新 DOM]

🎨 设计亮点与思考

1. 链表设计的巧思

为什么使用链表而不是数组来存储 Hooks?

链表的优势:

  • 🔗 动态扩展:可以根据 Hook 数量动态添加节点
  • 🚀 高效插入:在链表中间插入新 Hook 成本很低
  • 💾 内存友好:只分配需要的内存空间

2. 状态复用的智慧

在更新渲染时,系统会复用之前的 Hook 状态:

// 巧妙的状态复用
currentFiber.memoizedState = currentFiber.alternate.memoizedState;

这就像是"站在巨人的肩膀上",新的渲染可以基于之前的状态继续工作。

3. 闭包的妙用

dispatch 函数是一个闭包,它"记住"了:

  • 对应的 hook 对象
  • 传入的 reducer 函数
  • 当前的 currentFiber
function dispatch(action) {
  // 这里的 hook、reducer、currentFiber 都来自外层作用域
  hook.memoizedState = reducer(hook.memoizedState, action);
  currentFiber.alternate = { ...currentFiber };
  scheduleUpdateOnFiber(currentFiber);
}

🎯 实际应用场景

1. 状态管理器模式

// 购物车状态管理
const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      return [...state, action.item];
    case 'REMOVE_ITEM':
      return state.filter(item => item.id !== action.id);
    case 'CLEAR_CART':
      return [];
    default:
      return state;
  }
};

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, []);
  
  return (
    <div>
      {/* 购物车UI */}
    </div>
  );
}

2. 复杂表单状态

// 表单状态管理
const formReducer = (state, action) => {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, [action.field]: action.value };
    case 'RESET_FORM':
      return {};
    case 'SET_ERRORS':
      return { ...state, errors: action.errors };
    default:
      return state;
  }
};

🐛 常见问题与注意事项

1. Hook 调用顺序问题

// ❌ 错误:条件调用 Hook
function MyComponent({ showCounter }) {
  if (showCounter) {
    const [count, setCount] = useReducer(counterReducer, 0);
  }
  
  // 这会打破 Hook 链表的顺序!
}

// ✅ 正确:始终按同样顺序调用
function MyComponent({ showCounter }) {
  const [count, setCount] = useReducer(counterReducer, 0);
  
  if (showCounter) {
    return <div>{count}</div>;
  }
  return null;
}

2. reducer 函数的纯净性

// ❌ 错误:不纯的 reducer
const badReducer = (state, action) => {
  // 直接修改 state
  state.count++;
  return state;
};

// ✅ 正确:纯函数 reducer
const goodReducer = (state, action) => {
  // 返回新的对象
  return { ...state, count: state.count + 1 };
};