💡 核心概念深入解读
表格中的一些核心概念在面试中通常会要求更深入的阐述,这里为你提供一些关键的解读角度:
- 🚀 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 树: -
⚙️ Hooks 的底层模型
你可以将 Hooks 的工作机制想象成一个设计精良的“磁带机” 。在组件初次渲染时,每个 Hook(如useState、useEffect)被依次调用,它们的信息被按顺序“录制”到 Fiber 节点对应的链表中。在后续的更新渲染中,useState必须严格按照第一次“录制”的顺序来回放,从链表中取出属于自己的状态数据。如果在条件语句中使用 Hook,就会破坏这个确定的顺序,导致状态错乱。这也是为什么 Hook 不能被嵌套在条件或循环中的根本原因。
八股文
react18以前批处理是啥样的? react18+的批处理怎么做的,从底层讲讲
- react18前的批处理只针对合成事件生效;18后的批处理不仅对合成事件、对微任务等所有事件都生效
- 底层原理
Part 1: React 18 之前的批处理实现原理
在 React 18 之前,批处理的实现依赖于一个关键的机制: “执行上下文栈” 。
底层实现机制:
-
executionContext(执行上下文)变量:
React 内部维护了一个全局变量executionContext,它是一个二进制掩码,表示 React 当前正处于什么样的“工作阶段”。常见的上下文有:NoContext: 无上下文,表示 React 不在任何控制的执行流程中。BatchedContext: 批处理上下文。RenderContext: 正在渲染。CommitContext: 正在提交更新到 DOM。
-
批处理的启动与关闭:
-
启动:当 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) 和新的
createRootAPI。批处理的改进是构建在这个新架构之上的。底层实现机制:
-
废弃旧的
executionContext批处理逻辑:
在新的代码路径中(特别是使用createRoot时),React 团队逐渐弃用了基于executionContext的批处理逻辑。 -
状态更新的统一调度:
无论更新从哪里来(事件处理函数、setTimeout、Promise、原生事件),当调用setState时,React 18 都会将其包装成一个更新对象,并将其排入一个统一的更新队列。关键点在于,React 不再急于知道当前处于什么“上下文”。它只是将更新收集起来。
-
利用 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); };过程如下:
setTimeout回调(一个宏任务)开始执行。- 执行
setCount(1),React 将其更新加入队列,并调度一个微任务(如果还没调度的话)。 - 执行
setFlag(true),React 将其另一个更新加入同一个队列。 setTimeout宏任务执行结束。- JavaScript 引擎执行微任务队列。
- React 的微任务被执行,它从队列中取出所有等待的更新(
setCount和setFlag),并启动一次统一的重新渲染。
- 当第一个
-
总结对比
| 特性 | React 17 及以前 | React 18 (使用 createRoot) |
|---|---|---|
| 批处理范围 | React 事件合成系统内部 | 所有场景(事件处理函数、timeouts、promises、原生事件等) |
| 实现机制 | executionContext (BatchedContext) | 基于调度器的微任务 |
| 核心逻辑 | “如果处于批处理上下文中,则延迟更新” | “将更新排队,并在当前事件循环的微任务中统一处理” |
| 渲染次数 | 在 setTimeout 等异步代码中可能触发多次 | 在几乎所有场景下都只触发一次 |
| 关键API | ReactDOM.render | ReactDOM.createRoot |
注意:为了启用 React 18 的全新特性(包括自动批处理),你必须使用 ReactDOM.createRoot 而不是旧的 ReactDOM.render。如果你在 React 18 中继续使用 ReactDOM.render,它会退回到 React 17 的批处理行为以保持兼容。
useEffect和useLayoutEffect的差别是什么,如何实现的
- useEffect是在页面绘制后异步执行,不会阻塞主线程; useLayoutEffect是在页面绘制前同步执行,会阻塞主线程,在beforeMutation时执行useEffect的return销毁函数;useLayoutEffect是在mutationLayout时执行
react可执行中断渲染从底层怎么做到的
1.关键机制:时间切片
- 核心思想:将渲染任务拆分成小单元(Fiber),在浏览器的空闲时间执行。
- 关键步骤:处理单元 -> 检查时间 -> 有时间继续 -> 没时间暂停并重新调度。这个逻辑是完全正确的。