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

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

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

🚀 引言:什么是 useReducer?

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

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

javascript 复制代码
// 就像游戏中的状态管理
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 是如何存储和管理的:

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

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

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

2. renderWithHooks:Hook 系统的启动器

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

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

3. updateWorkInProgressHook:Hook 链表的核心

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

javascript 复制代码
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 的具体实现:

javascript 复制代码
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];
}

🎭 完整的渲染流程

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

示例代码

javascript 复制代码
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:调度的入口

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

工作循环:workLoop

javascript 复制代码
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 状态:

javascript 复制代码
// 巧妙的状态复用
currentFiber.memoizedState = currentFiber.alternate.memoizedState;

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

3. 闭包的妙用

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

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

🎯 实际应用场景

1. 状态管理器模式

javascript 复制代码
// 购物车状态管理
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. 复杂表单状态

javascript 复制代码
// 表单状态管理
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 调用顺序问题

javascript 复制代码
// ❌ 错误:条件调用 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 函数的纯净性

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

// ✅ 正确:纯函数 reducer
const goodReducer = (state, action) => {
  // 返回新的对象
  return { ...state, count: state.count + 1 };
};
相关推荐
龙在天2 分钟前
你只会console.log就Out了
前端
用户681722457213 分钟前
h5实现点击电话进入拨打电话功能
前端
青红光硫化黑1 小时前
React-native之组件
javascript·react native·react.js
菠萝+冰1 小时前
在 React 中,父子组件之间的通信(传参和传方法)
前端·javascript·react.js
庚云1 小时前
一套代码如何同时适配移动端和pc端
前端
Jinuss1 小时前
Vue3源码reactivity响应式篇Reflect和Proxy详解
前端·vue3
海天胜景1 小时前
vue3 el-select 默认选中第一个
前端·javascript·vue.js
小小怪下士_---_1 小时前
uniapp开发微信小程序自定义导航栏
前端·vue.js·微信小程序·小程序·uni-app
前端W1 小时前
腾讯地图组件使用说明文档
前端
页面魔术2 小时前
无虚拟dom怎么又流行起来了?
前端·javascript·vue.js