从0到1实现react(四):useState的实现&&节点删除

38 阅读5分钟

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

🌟 开篇:React Hook 的魔法世界

想象一下,React 组件就像是一个有记忆的魔法师🧙‍♂️。每次表演(渲染)时,它都能记住之前的状态,并在合适的时候改变这些状态。而 useState 就是这个魔法师最重要的法术之一。

让我们先看看这个魔法师是如何记住和管理状态的:

🎯 useState:状态管理的艺术

🧠 Hook 的记忆系统

在 Mini React 中,每个组件的状态就像是一串珍珠项链,每个 Hook 都是项链上的一颗珍珠:

// hooks.js 中的核心数据结构
let currentFiber = null;        // 当前正在工作的 Fiber 节点
let workInProgressHook = null;  // 当前正在处理的 Hook

// 每个 Hook 的结构就像一颗珍珠
const hook = {
  memoizedState: null,  // 存储状态值
  next: null           // 指向下一个 Hook
};

🎪 Hook 链表的建立过程

让我用一个生动的比喻来解释:想象你是一个串珍珠的工匠,每次调用 useState 就是在项链上添加一颗新珍珠。

graph TD
    A["第一次渲染<br/>currentFiber.alternate = null"] --> B["调用 useState(0)"]
    B --> C["创建第一个 Hook<br/>{memoizedState: 0, next: null}"]
    C --> D["调用 useState('hello')"]
    D --> E["创建第二个 Hook<br/>{memoizedState: 'hello', next: null}"]
    E --> F["形成 Hook 链表<br/>Hook1 -> Hook2 -> null"]
    
    style A fill:#e1f5fe
    style C fill:#c8e6c9
    style E fill:#c8e6c9
    style F fill:#fff3e0

🔄 Hook 更新的秘密

当组件更新时,就像魔法师重新表演一样,他需要按照相同的顺序重新拿起每颗珍珠:

function updateWorkInProgressHook() {
  let hook;
  
  // 🎭 初次渲染:创建全新的珍珠项链
  if (!currentFiber.alternate) {
    hook = {
      memoizedState: null,
      next: null,
    };
    // 将珍珠串在项链上
    if (!workInProgressHook) {
      currentFiber.memoizedState = hook;
      workInProgressHook = hook;
    } else {
      workInProgressHook.next = hook;
      workInProgressHook = hook;
    }
  } 
  // 🔄 更新时:复用之前的珍珠项链
  else {
    currentFiber.memoizedState = currentFiber.alternate.memoizedState;
    if (!workInProgressHook) {
      hook = workInProgressHook = currentFiber.alternate.memoizedState;
    } else {
      hook = workInProgressHook = workInProgressHook.next;
    }
  }
  return hook;
}

🎯 useState 的本质:化繁为简的设计

你可能惊讶地发现,useState 其实只是 useReducer 的一个特殊版本!

function useState(initialState) {
  return useReducer(null, initialState);
}

function useReducer(reducer, initialState) {
  const hook = updateWorkInProgressHook();
  
  // 🌱 初次渲染:播下种子
  if (!currentFiber?.alternate) {
    hook.memoizedState = initialState;
  }

  // 🎭 创建状态更新的魔法函数
  function dispatchClosure(currentFiber, hook, reducer, action) {
    // 📝 更新状态
    hook.memoizedState = reducer ? reducer(hook.memoizedState) : action;
    
    // 🔄 触发重新渲染
    currentFiber.alternate = { ...currentFiber };
    currentFiber.sibling = null;
    scheduleUpdateOnFiber(currentFiber);
  }
  
  const dispatch = dispatchClosure.bind(null, currentFiber, hook, reducer);
  return [hook.memoizedState, dispatch];
}

🌊 状态更新的连锁反应

当你调用 setState 时,就像在平静的湖面投下一颗石子,会引起一系列连锁反应:

sequenceDiagram
    participant User as 用户点击
    participant Dispatch as dispatch函数
    participant Hook as Hook对象
    participant Scheduler as 调度器
    participant WorkLoop as 工作循环
    participant DOM as DOM更新

    User->>Dispatch: setState(newValue)
    Dispatch->>Hook: 更新 memoizedState
    Dispatch->>Scheduler: scheduleUpdateOnFiber()
    Scheduler->>WorkLoop: 启动工作循环
    WorkLoop->>WorkLoop: 重新渲染组件
    WorkLoop->>DOM: 提交更改到DOM

🗑️ 节点删除:优雅的告别艺术

🎬 删除的时机:Reconciliation 过程

在 React 的世界里,删除节点就像是一场精心编排的舞蹈。当新的虚拟 DOM 树与旧的进行对比时,有些节点需要优雅地退场:

function reconcileChildren(wip, children) {
  // 🎭 将 children 转换为统一的数组格式
  let arr = Array.isArray(children) ? children : [children];
  let oldFiber = wip.alternate?.child;  // 旧的孩子节点
  
  for (let i = 0; i < arr.length; i++) {
    newFiber = createFiber(arr[i], wip);
    const isSame = sameNode(oldFiber, newFiber);
    
    // 🎭 如果节点不同且旧节点存在,标记删除
    if (!isSame && oldFiber?.stateNode) {
      deleteChild(wip, oldFiber);
    }
    
    oldFiber = oldFiber.sibling;
  }
  
  // 🧹 清理剩余的旧节点
  if (oldFiber) {
    deleteRemainingChildren(wip, oldFiber);
  }
}

🏷️ 删除标记:给节点贴上"待删除"标签

当发现节点需要删除时,React 不会立即删除,而是给它贴上一个"待删除"的标签:

function deleteChild(returnFiber, childToDelete) {
  // 🏷️ 在父节点上维护一个删除列表
  if (returnFiber?.deletions) {
    returnFiber.deletions.push(childToDelete);
  } else {
    returnFiber.deletions = [childToDelete];
  }
}

这就像是在演出结束后,给需要退场的演员发放"退场券",但他们还要等到合适的时机才能真正离开舞台。

🎭 多节点删除:批量处理的智慧

当有多个连续的节点需要删除时,React 会进行批量处理:

function deleteRemainingChildren(wip, oldFiber) {
  let childToDelete = oldFiber;
  // 🔄 遍历兄弟节点,全部标记删除
  while (childToDelete) {
    deleteChild(wip, childToDelete);
    childToDelete = childToDelete.sibling;
  }
}

🎪 提交阶段:真正的删除表演

所有的删除操作都会在提交阶段统一执行,这就像是演出的最后谢幕环节:

graph TD
    A["Render阶段<br/>标记删除"] --> B["收集deletions数组"]
    B --> C["Commit阶段<br/>执行删除"]
    C --> D["遍历deletions"]
    D --> E["找到真实DOM节点"]
    E --> F["从父节点移除"]
    
    style A fill:#ffcdd2
    style C fill:#fff3e0
    style F fill:#c8e6c9
function commitWork(fiber) {
  // 🎭 处理各种操作:新增、更新、删除
  const { flags, stateNode } = fiber;
  let parentNode = getParentNode(fiber);
  
  // ➕ 新增节点
  if (flags & Placement && stateNode) {
    parentNode.appendChild(stateNode);
  }
  
  // 🔄 更新节点
  if (flags & Update && stateNode) {
    updateNode(stateNode, fiber.alternate.props, fiber.props);
  }
  
  // 🗑️ 删除节点
  if (fiber?.deletions) {
    commitDeletions(fiber, fiber.stateNode ?? parentNode);
  }
  
  // 🔄 递归处理子节点和兄弟节点
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function commitDeletions(fiber, parentNode) {
  fiber?.deletions.forEach((child) => {
    // 🎯 找到实际的DOM节点并删除
    parentNode.removeChild(getStateNode(child));
  });
}

🔍 寻找真实节点:穿越虚拟的迷雾

有时候,被删除的 Fiber 节点可能没有对应的真实 DOM 节点(比如 Fragment),这时需要深入寻找:

function getStateNode(fiber) {
  // 🔍 如果当前节点没有真实DOM,就找它的子节点
  while (!fiber.stateNode) {
    fiber = fiber.child;
  }
  return fiber?.stateNode;
}

🎨 完整的生命周期:从状态更新到节点删除

让我们用一个完整的例子来看看这两个机制是如何协作的:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习React' },
    { id: 2, text: '写技术博客' },
    { id: 3, text: '分享知识' }
  ]);

  const removeTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => removeTodo(todo.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

当用户点击删除按钮时,会发生以下魔法:

flowchart TD
    A["用户点击删除按钮"] --> B["调用 removeTodo"]
    B --> C["调用 setTodos"]
    C --> D["触发 dispatch"]
    D --> E["更新 Hook.memoizedState"]
    E --> F["scheduleUpdateOnFiber"]
    F --> G["启动工作循环"]
    G --> H["重新渲染组件"]
    H --> I["reconcileChildren 对比"]
    I --> J["发现缺少的 li 节点"]
    J --> K["调用 deleteChild 标记"]
    K --> L["commitWork 执行删除"]
    L --> M["真实DOM节点被移除"]
    
    style A fill:#e3f2fd
    style E fill:#f3e5f5
    style J fill:#fff3e0
    style M fill:#e8f5e8

🎯 核心设计思想总结

🧠 useState 的设计智慧

  1. 链表结构:用简单的链表保存多个 Hook 状态
  2. 双缓冲机制:current 和 alternate 树的切换
  3. 闭包魔法:dispatch 函数携带上下文信息
  4. 统一抽象:useState 基于 useReducer 实现

🗑️ 节点删除的设计哲学

  1. 延迟删除:先标记,后执行
  2. 批量处理:统一在提交阶段处理
  3. 深度搜索:智能寻找真实DOM节点
  4. 优雅降级:处理各种边界情况

🚀 写在最后

通过这次深入源码的探索,我们看到了 React 设计的精巧之处:

  • useState 用最简单的链表结构,实现了强大的状态管理
  • 节点删除 通过标记-清除的模式,确保了操作的安全性和效率

这些看似复杂的机制,背后都蕴含着简单而优雅的设计思想。正如老子所说:"大道至简",最好的技术往往都是简单而有效的。

希望这篇文章能帮你更好地理解 React 的内部工作原理。记住,理解源码不是为了炫技,而是为了写出更好的代码!


如果这篇文章对你有帮助,别忘了点个赞哦!🌟