学完react源码收获有哪些方面呢?面试会被问到什么呢?这是一篇提问贴~~

88 阅读9分钟
考察方向核心面试问题关键源码概念/机制
核心架构Fiber 架构解决了什么问题?可中断的异步更新、任务优先级调度
双缓冲 Fiber 树是什么?current 树与 workInProgress 树、alternate 指针
React 的协调(Reconciliation)过程?Diff 算法策略(同级比较、类型差异、key)
渲染与更新一次更新的完整流程是怎样的?触发更新 -> 调度 -> 渲染阶段(可中断)-> 提交阶段(同步、不可中断)
Hooks 原理Hooks 为什么不能在循环或条件语句中调用?Hooks 在 Fiber 节点中以链表形式存储,调用顺序必须稳定。
useState 的原理是什么?闭包与状态存储、更新队列、函数式更新
useEffect 的执行时机与清理机制?渲染后被调度执行、清理函数在组件卸载或依赖变更前执行
特色功能React 事件机制(合成事件)如何工作?事件委托、事件池、合成事件对象(SyntheticEvent)

💡 核心概念深入解读

表格中的一些核心概念在面试中通常会要求更深入的阐述,这里为你提供一些关键的解读角度:

  • 🚀 Fiber 架构的本质
    Fiber 本质上是虚拟 DOM 的进阶版,也是一个存储在内存中的 JavaScript 对象。它的革命性在于将原本同步的、不可中断的递归更新过程(Stack Reconciler),拆解成了一个个可以异步执行的微小单元(Fiber 节点) ,每个 Fiber 节点对应一个 React 元素(组件/DOM 节点)。React 利用浏览器的空闲时间(requestIdleCallback)来执行这些单元,并且可以为不同类型的更新(如用户输入、数据请求)分配不同的优先级,从而使高优先级的更新能够打断并插队低优先度的渲染,保证了用户界面的流畅响应
//fiber数据结构
interface Fiber {
  // 标识信息
  tag: WorkTag;        // 组件类型(函数组件/类组件/DOM节点等)
  key: string | null;  // 同级节点唯一标识
  type: any;           // 组件构造函数或DOM标签名(如 'div')

  // 树结构关系
  return: Fiber | null; // 父节点
  child: Fiber | null;  // 第一个子节点
  sibling: Fiber | null;// 下一个兄弟节点
  alternate: Fiber | null; // 当前节点对应的旧Fiber(用于Diff)

  // 状态与副作用
  memoizedState: any;   // Hook链表(函数组件状态)
  stateNode: any;       // 实例(DOM节点/类组件实例)
  flags: Flags;         // 标记需要进行的操作(如Placement/Update)
  lanes: Lanes;         // 优先级(车道模型)

  // 工作进度
  pendingProps: any;    // 待处理的props
  memoizedProps: any;   // 上次渲染的props
}

  • 🔄 双缓冲技术 (Double Buffering)
    这是一种在图形学中常见的技术,React 借鉴它来保证更新的连贯性。React 在内存中同时维护两棵 Fiber 树:

    • current 树:代表当前已渲染到页面的 UI 状态。
    • workInProgress 树:正在后台构建的、用于下一次渲染的新状态。
      所有更新都在 workInProgress 树上进行。当这棵树构建完成后,通过一次快速的指针切换,workInProgress 树就变成了新的 current 树。这个机制确保了用户永远不会看到渲染到一半的中间状态,实现了原子性更新
  • ⚙️ Hooks 的底层模型
    你可以将 Hooks 的工作机制想象成一个设计精良的“磁带机” 。在组件初次渲染时,每个 Hook(如 useStateuseEffect)被依次调用,它们的信息被按顺序“录制”到 Fiber 节点对应的链表中。在后续的更新渲染中,useState 必须严格按照第一次“录制”的顺序来回放,从链表中取出属于自己的状态数据。如果在条件语句中使用 Hook,就会破坏这个确定的顺序,导致状态错乱。这也是为什么 Hook 不能被嵌套在条件或循环中的根本原因。

八股文

react18以前批处理是啥样的? react18+的批处理怎么做的,从底层讲讲

  • react18前的批处理只针对合成事件生效;18后的批处理不仅对合成事件、对微任务等所有事件都生效
  • 底层原理

    Part 1: React 18 之前的批处理实现原理

在 React 18 之前,批处理的实现依赖于一个关键的机制: “执行上下文栈”

底层实现机制:

  1. executionContext (执行上下文)变量
    React 内部维护了一个全局变量 executionContext,它是一个二进制掩码,表示 React 当前正处于什么样的“工作阶段”。常见的上下文有:

    • NoContext: 无上下文,表示 React 不在任何控制的执行流程中。
    • BatchedContext: 批处理上下文。
    • RenderContext: 正在渲染。
    • CommitContext: 正在提交更新到 DOM。
  2. 批处理的启动与关闭

    • 启动:当 React 的事件处理函数被触发时(例如 onClick),React 会在调用你的处理函数之前,通过一个叫做 batchedUpdates 的函数,将 executionContext 设置为 BatchedContext

      javascript

      // 伪代码,简化版
      function batchedUpdates(fn, a) {
        const prevExecutionContext = executionContext;
        executionContext |= BatchedContext; // 添加批处理上下文
        try {
          return fn(a); // 这里执行你的 onClick 回调函数
        } finally {
          executionContext = prevExecutionContext; // 恢复之前的上下文
          // 注意:在退出时,如果上下文不再是 BatchedContext,可能会触发刷新
        }
      }
      
    • 状态更新时的检查:当你调用 setState 时,React 会创建一个更新对象,并将其放入对应 Fiber 节点的更新队列中。然后,它会调用一个名为 ensureRootIsScheduled 的函数来调度更新。
      在调度之前,有一个关键判断:

      javascript

      // 伪代码,简化版
      if (executionContext === NoContext) {
        // 如果不在任何上下文中(比如在 setTimeout 里),立即同步刷新更新
        flushSyncCallbackQueue();
      }
      
      **重点**:如果当前 `executionContext` 包含 `BatchedContext`,React 就不会立即调度更新,而是将其“收集”起来,等待当前执行栈结束。
      
      • 关闭与刷新:当 batchedUpdates 函数执行完毕,退出 try...finally 块时,它会将 executionContext 恢复原状。如果恢复后变成了 NoContext,React 就会在这个时候一次性刷新所有积累的更新,进行重新渲染。

    为什么在 setTimeout 或原生事件中不批处理?

    javascript

    // 例子:在 React 17 中
    handleClick = () => {
      setTimeout(() => {
        setCount(1); // 第一次更新
        setFlag(true); // 第二次更新
      }, 0);
    };
    

    当 setTimeout 回调执行时,它已经脱离了 React 事件合成系统的封装。此时 executionContext 为 NoContext。所以每次 setState 都会触发那个 if (executionContext === NoContext) 条件,导致立即、同步的刷新,从而产生两次渲染。


    Part 2: React 18 的自动批处理实现原理

    React 18 引入了 并发特性(Concurrent Features)  和新的 createRoot API。批处理的改进是构建在这个新架构之上的。

    底层实现机制:

    1. 废弃旧的 executionContext 批处理逻辑
      在新的代码路径中(特别是使用 createRoot 时),React 团队逐渐弃用了基于 executionContext 的批处理逻辑。

    2. 状态更新的统一调度
      无论更新从哪里来(事件处理函数、setTimeoutPromise、原生事件),当调用 setState 时,React 18 都会将其包装成一个更新对象,并将其排入一个统一的更新队列

      关键点在于,React 不再急于知道当前处于什么“上下文”。它只是将更新收集起来。

    3. 利用 JavaScript 事件循环与微任务
      这是实现自动批处理的核心。React 18 的调度器(Scheduler)利用了浏览器的事件循环机制。

      • 当第一个 setState 被调用时,React 会调度一个微任务(或者类似微任务的机制,具体实现可能使用 queueMicrotask 或 Promise.resolve().then())来在当前事件循环的末尾处理所有这些更新。
      • 在同一个事件循环中,后续的任何 setState 调用都会简单地将其更新添加到同一个队列中。
      • 当前宏任务(如一次 click 事件、一个 setTimeout 回调)执行完毕后,JavaScript 引擎会开始执行微任务队列。这时,React 调度的那个微任务会执行,从而一次性处理所有队列中的更新。

      为什么现在 setTimeout 也能批处理了?

      让我们再看那个例子,但在 React 18 中:

      javascript

      // 例子:在 React 18 中 (使用 createRoot)
      handleClick = () => {
        setTimeout(() => {
          setCount(1); // 将更新加入队列
          setFlag(true); // 将另一个更新加入同一个队列
          // setTimeout 宏任务执行完毕
          // 此时,微任务开始执行,React 在这里批量处理 setCount 和 setFlag,只进行一次渲染
        }, 0);
      };
      

      过程如下:

      1. setTimeout 回调(一个宏任务)开始执行。
      2. 执行 setCount(1),React 将其更新加入队列,并调度一个微任务(如果还没调度的话)。
      3. 执行 setFlag(true),React 将其另一个更新加入同一个队列
      4. setTimeout 宏任务执行结束。
      5. JavaScript 引擎执行微任务队列
      6. React 的微任务被执行,它从队列中取出所有等待的更新(setCount 和 setFlag),并启动一次统一的重新渲染

总结对比

特性React 17 及以前React 18 (使用 createRoot)
批处理范围React 事件合成系统内部所有场景(事件处理函数、timeouts、promises、原生事件等)
实现机制executionContext (BatchedContext)基于调度器的微任务
核心逻辑“如果处于批处理上下文中,则延迟更新”“将更新排队,并在当前事件循环的微任务中统一处理”
渲染次数在 setTimeout 等异步代码中可能触发多次在几乎所有场景下都只触发一次
关键APIReactDOM.renderReactDOM.createRoot

注意:为了启用 React 18 的全新特性(包括自动批处理),你必须使用 ReactDOM.createRoot 而不是旧的 ReactDOM.render。如果你在 React 18 中继续使用 ReactDOM.render,它会退回到 React 17 的批处理行为以保持兼容。

useEffect和useLayoutEffect的差别是什么,如何实现的

  • useEffect是在页面绘制后异步执行,不会阻塞主线程; useLayoutEffect是在页面绘制前同步执行,会阻塞主线程,在beforeMutation时执行useEffect的return销毁函数;useLayoutEffect是在mutationLayout时执行

image.png

react可执行中断渲染从底层怎么做到的

1.关键机制:时间切片

  1. 核心思想:将渲染任务拆分成小单元(Fiber),在浏览器的空闲时间执行。
  2. 关键步骤:处理单元 -> 检查时间 -> 有时间继续 -> 没时间暂停并重新调度。这个逻辑是完全正确的。

image.png