【React Fiber架构+React18知识点+浏览器原生帧流程和React阶段流程相串】

Fiber 架构:React 的革命性升级

为什么需要 Fiber?

React 15 的问题:

javascript 复制代码
// 假设有 10000 个组件需要更新
function App() {
  return (
    <div>
      {Array.from({ length: 10000 }, (_, i) => <Item key={i} />)}
    </div>
  );
}

// React 15:同步渲染,一旦开始就不能停
// 主线程被占用 → 用户点击按钮没反应 → 页面卡顿

Fiber 的解决方案:可中断的渲染

javascript 复制代码
传统:渲染 10000 个组件(阻塞 500ms)→ 用户操作无响应

Fiber:渲染 100 个 → 检查是否有高优先级任务 → 有则暂停 → 处理用户点击 → 继续渲染

Fiber 是什么?

Fiber = 虚拟栈帧 = 工作单元

javascript 复制代码
// 简化的 Fiber 节点结构
const fiber = {
  type: 'div',              // 组件类型
  props: { className: 'box' }, // 属性
  stateNode: domElement,    // 真实 DOM 节点
  
  // 链表结构(用于遍历)
  child: childFiber,        // 第一个子节点
  sibling: siblingFiber,    // 下一个兄弟节点
  return: parentFiber,      // 父节点
  
  // 副作用
  effectTag: 'UPDATE',      // 'PLACEMENT' | 'UPDATE' | 'DELETION'
  alternate: oldFiber,      // 上一次的 Fiber(用于 Diff)
};

Fiber 树的遍历:

javascript 复制代码
// JSX
<App>
  <Header>
    <Title />
  </Header>
  <Content />
</App>

// Fiber 树(链表结构)
App├─ child → Header
 │├─ child → Title
 │           └─ return → Header
 └─ sibling → Content

// 遍历顺序(深度优先)
App → Header → Title → Content

时间切片(Time Slicing)

javascript 复制代码
// React 内部的调度逻辑(简化版)
function workLoop(deadline) {
  let shouldYield = false;
  
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 处理一个 Fiber
    shouldYield = deadline.timeRemaining() < 1; // 剩余时间不足 1ms
  }
  
  if (nextUnitOfWork) {
    // 还有工作未完成,下一帧继续
    requestIdleCallback(workLoop);
  } else {
    // 完成,提交更新到 DOM
    commitRoot();
  }
}

requestIdleCallback(workLoop);

代码解释:

javascript 复制代码
// React 内部的调度逻辑(简化版)
function workLoop(deadline) {
  // 1. 标记:是否需要【让出主线程】给浏览器
  let shouldYield = false;
  
  // 2. 核心循环:只要有任务 + 不需要让出,就一直执行
  // nextUnitOfWork = 下一个要处理的 Fiber 节点(最小工作单元)
  while (nextUnitOfWork && !shouldYield) {
    // 3. 处理【单个 Fiber 任务】(diff、标记更新、渲染组件)
    // 执行完一个,返回下一个待处理的 Fiber
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    
    // 4. 检查浏览器【剩余空闲时间】
    // 小于 1ms → 时间不够了,必须暂停!
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  // 5. 如果还有未完成的任务
  if (nextUnitOfWork) {
    // 【主动暂停】,等浏览器下一帧空闲时,继续执行
    requestIdleCallback(workLoop);
  } else {
    // 6. 所有任务全部完成!一次性更新到真实 DOM
    commitRoot();
  }
}

// 启动工作循环(浏览器空闲时执行)
requestIdleCallback(workLoop);

关键变量 / 函数解释

  1. workLoop React 的工作循环,所有渲染任务都在这里排队执行。
  2. deadline 浏览器 requestIdleCallback 自动传入的对象,告诉你当前帧还剩多少空闲时间
  3. **nextUnitOfWork**最小工作单元 = 一个 Fiber 节点(对应一个组件 / 一个 DOM)。
  4. **performUnitOfWork**干实事的函数:处理单个 Fiber(执行组件、diff 对比、标记更新)。
  5. requestIdleCallback 浏览器 API:在帧空闲时执行回调,不阻塞渲染 / 交互。
  6. commitRoot() 最终把内存里的更新,一次性同步渲染到真实 DOM

实际效果:

javascript 复制代码
帧 1 (16ms):渲染 100 个组件 (5ms) → 空闲 (11ms) → 处理用户输入
帧 2 (16ms):渲染 100 个组件 (5ms) → 空闲 (11ms) → 动画流畅
帧 3 (16ms):渲染 100 个组件 (5ms) → ...

补充:React 真实实现的小细节

React 源码没有直接用 requestIdleCallback ,而是用 MessageChannel 替代:

  1. 原因:requestIdleCallback 兼容性差、触发不稳定(最低 20ms 间隔)
  2. 原理:MessageChannel 可以实现微任务级精确调度,切分 5ms 任务片
  3. 结果:效果和这段伪代码完全一致,只是精度更高。

调和过程(Reconciliation):Render 和 Commit

两阶段架构:
javascript 复制代码
Render 阶段(可中断)          Commit 阶段(不可中断)
├─ 构建 Fiber 树              ├─ 操作真实 DOM
├─ Diff 算法                  ├─ 执行副作用
├─ 标记副作用                 └─ 调用生命周期
└─ 可以被高优先级任务打断
Render 阶段详解
  1. React 创建更新,计算优先级(Lanes)
  2. 进入 workLoop 时间切片
    • 在浏览器帧空闲时执行
    • 每次执行一小段 Fiber,检查时间
    • 时间不够 → 让出主线程 → 下帧继续
  3. 直到:整棵 Fiber 树 diff 完成 → 生成副作用列表→ Render 阶段结束

Render 阶段可能跨 1 帧~N 帧,但不阻塞页面。

javascript 复制代码
function reconcileChildren(fiber, children) {
  let oldFiber = fiber.alternate?.child; // 旧 Fiber
  let prevSibling = null;
  
  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      return: fiber,};
    
    // Diff 算法
    if (oldFiber && oldFiber.type === child.type) {
      // 类型相同 → 更新
      newFiber.alternate = oldFiber;
      newFiber.effectTag = 'UPDATE';
      newFiber.stateNode = oldFiber.stateNode;} else {
      // 类型不同 → 替换
      newFiber.effectTag = 'PLACEMENT';
    }
    
    // 构建链表
    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }
    
    prevSibling = newFiber;
    oldFiber = oldFiber?.sibling;
  });
}
Commit 阶段详解
  1. Before Mutation(DOM变更前): 执行类组件 getSnapshotBeforeUpdate
  2. Mutation 阶段:真实 DOM 更新→ DOM 变了,但浏览器还没渲染
  3. Layout 阶段:同步执行 useLayoutEffect → 你可以在这里读取最新布局、强制修改样式、避免闪烁, 执行类组件 componentDidMount / DidUpdate
  4. 同步执行 useLayoutEffect 创建函数
  5. 此时:DOM 已变,但屏幕还没显示新画面
javascript 复制代码
function commitRoot() {
  // 阶段 1:before mutation(DOM 变更前)
  commitBeforeMutationEffects(); // 调用 getSnapshotBeforeUpdate
  
  // 阶段 2:mutation(DOM 变更)
  commitMutationEffects(); // 插入、更新、删除 DOM
  
  // 阶段 3:layout(DOM 变更后)
  commitLayoutEffects(); // 调用 componentDidMount/Update、useLayoutEffect
}

function commitMutationEffects(fiber) {
  if (fiber.effectTag === 'PLACEMENT') {
    // 插入 DOM
    fiber.return.stateNode.appendChild(fiber.stateNode);
  } else if (fiber.effectTag === 'UPDATE') {
    // 更新属性
    updateDOMProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === 'DELETION') {
    // 删除 DOM
    fiber.return.stateNode.removeChild(fiber.stateNode);
  }
}

实战案例:为什么 useLayoutEffect 比 useEffect 先执行?

javascript 复制代码
function Component() {
  useEffect(() => {
    console.log('3. useEffect'); // 异步执行(宏任务)
  });
  
  useLayoutEffect(() => {
    console.log('2. useLayoutEffect'); // 同步执行(Commit 阶段)
  });
  
  console.log('1. render');
  return <div>Hello</div>;
}

// 输出顺序:
// 1. render
// 2. useLayoutEffect(DOM 更新后立即执行,阻塞浏览器绘制)
// 3. useEffect(浏览器绘制后执行)

使用场景:

javascript 复制代码
// ✅ useLayoutEffect:需要同步读取 DOM 布局
function Tooltip() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const ref = useRef(null);
  
  useLayoutEffect(() => {
    const rect = ref.current.getBoundingClientRect();
    setPosition({ x: rect.left, y: rect.top }); // 避免闪烁
  }, []);
  
  return <div ref={ref} style={{ left: position.x, top: position.y }}>提示</div>;
}

// ✅ useEffect:异步操作(请求、订阅)
function DataFetcher() {
  useEffect(() => {
    fetch('/api/data').then(setData); // 不阻塞渲染
  }, []);
}

React Fiber + Concurrent 完整流程:

React Render+Commit+浏览器原生帧流程梳理:

浏览器原生:一帧完整生命周期:

浏览器一帧 ≈ 16ms(60fps),永远按下面顺序执行

第 1 步:执行 JS(宏任务 → 清空微任务):

  1. 从宏任务队列取一个任务执行(如:click、setTimeout、script、requestIdleCallback)

  2. 执行过程中产生的所有微任务 (Promise.then、queueMicrotask、React 内部调度)→ 必须在本帧同步清空

  3. 直到 JS 调用栈空 + 微任务空 → 才会进入渲染流程

第 2 步:浏览器渲染流水线(不可中断):

  1. Style Recalculate(样式重计算)

  2. Layout / Reflow(布局:几何位置、宽高)

  3. Paint(绘制:颜色、图片、文字)

  4. Composite(合成:图层合并)

第 3 步:进入空闲 / 等待下一帧:

  • requestIdleCallback → 此时执行

  • 时间到 → 进入下一帧,重复上述流程

串联:

用户触发更新:

用户触发更新(setState /props change / 父组件重渲染)→ React Render 阶段 (diff Fiber、可中断、时间切片)→ React Commit 阶段真实 DOM 更新同步执行 useLayoutEffect → 回到浏览器原生渲染流水线(浏览器渲染):浏览器 重排 (Layout) + 重绘 (Paint) (屏幕才真正变)→ 异步执行 useEffect :渲染完全结束,React 将 useEffect 放入宏任务 ,在下一次空闲时执行→ 不阻塞渲染,不影响页面流畅度。


网页第一次渲染(挂载阶段):

同步执行入口 JS(createRoot /root.render)→ React Render 阶段 (首次构建 Fiber 树,无 diff)→ React Commit 阶段DOM 插入(第一次挂到页面)同步执行 useLayoutEffect → 浏览器 Layout + Paint (首次显示页面)→ 异步执行 useEffect


不管是首次渲染 还是更新

  1. 浏览器执行 JS、微任务
  2. Render 阶段:可中断、不操作 DOM、时间切片、跨帧
  3. Commit 阶段:同步不可中断、更 DOM、执行 useLayoutEffect
  4. 先 Commit,再浏览器绘制: Layout + Paint(屏幕显示)
  5. useEffect 永远在绘制完才异步执行

易混淆:

只有 React Commit 阶段是:同步、阻塞、占用当前帧;React 调和阶段(Render 阶段):异步、可中断、非阻塞、和浏览器帧交替执行。

浏览器一帧(16ms):JS 执行、样式计算、Layout、Paint、Composite。

Fiber架构下真实流程:Render 阶段 :跑在帧与帧之间的空闲时间 执行 5ms → 暂停 → 浏览器渲染 → 再执行 5ms→ 完全不阻塞帧流程;Commit 阶段 :插到某一帧的 JS 执行期 同步跑完 → 然后浏览器走 Layout/Paint→ 短暂阻塞,但极快。


易混淆:DOM 更新不是浏览器绘制:

  • DOM 更新 :JS 操作 DOM 树,发生在 Commit 阶段
  • 浏览器绘制 :把 DOM 画到屏幕,发生在 Commit 之后

Q:useLayoutEffect 为什么能 "防闪烁"?

A:因为它在:DOM 已更新 → 浏览器绘制前 同步执行,你可以在这里改样式,浏览器只画最终结果

Q:useEffect 为什么不卡?

A:因为它在:浏览器绘制完成后 才异步执行不参与帧内同步工作,绝不阻塞渲染。


React18 Q&A:

1. React18 最核心的更新是什么?

:默认开启并发渲染(Concurrent Rendering) ,提供可中断、可优先级、可插队 的更新机制,同时带来自动批处理、useTransitionSuspense 增强、流式 SSR 等核心能力。

2. React18 如何开启并发特性?

:用新的根节点 API 替换旧 API:

javascript 复制代码
// 旧
ReactDOM.render(<App />, el)
// React18
const root = ReactDOM.createRoot(el)
root.render(<App />)

只有用 createRoot,并发、自动批处理、新 Hooks 才会生效。

3. 什么是自动批处理(Automatic Batching)?

  • 批处理:把多次 setState 合并成一次渲染,提升性能。
  • React18 之前:只在合成事件、生命周期里批处理,Promise、setTimeout、原生事件里不批。
  • React18:所有场景自动批处理,不管是异步、Promise、定时器、原生事件,都会合并更新。

4. React18 移除 / 废弃了哪些 API?

  • 淘汰 ReactDOM.render,改用 createRoot
  • 淘汰 unmountComponentAtNode,改用 root.unmount()
  • 淘汰 ReactDOM.hydrate,改用 hydrateRoot
  • 废弃 useEffect 依赖为 undefined 等旧写法

并发核心:

5. 什么是 Concurrent Mode(并发模式)?

:不是功能,是底层渲染机制

  • 渲染过程可中断、可恢复、可插队、可废弃
  • 高优先级任务(输入、点击)可以打断低优先级任务(列表渲染)
  • 保证主线程不阻塞,页面永远流畅

6. useTransition 是什么?有什么用?

  • 把更新标记为非紧急、可中断的过渡更新
  • 不会阻塞用户交互、输入
  • 返回 [isPending, startTransition]
  • 适用:大数据列表、搜索过滤、图表渲染等非紧急大计算

7. useDeferredValue 是什么?和 useTransition 区别?

  • 延迟一个值的更新,等紧急渲染完再更新
  • 相当于给值做并发防抖 区别
  • useTransition:控制更新动作
  • useDeferredValue:控制值本身
  • 无法控制更新动作时(如父组件传值)用 useDeferredValue

8. React18 的 Suspense 有哪些能力?

  • 服务端:流式 SSR + Suspense 渐进式加载
  • 客户端:支持组件懒加载、数据获取的等待状态
  • 配合并发渲染,实现不卡顿、可中断的 Loading 切换

Fiber & 架构原理:

9. Fiber 是什么?解决了什么问题?

  • Fiber 是一个工作单元 + 虚拟栈帧,是 React16+ 的协调器
  • 链表 + 循环替代递归遍历
  • 解决旧版 Stack 协调器同步递归、不可中断、阻塞主线程的卡顿问题
  • 实现:可中断、时间切片、优先级调度、双缓存树

10. Render 阶段 和 Commit 阶段 的区别?

  • Render 阶段 :遍历 Fiber、diff、标记更新→ 可中断、可异步、可重复执行
  • Commit 阶段 :操作 DOM、执行生命周期 / Effect→ 同步、不可中断、必须一次性完成,保证 DOM 稳定。

Fiber 把更新彻底拆成两个阶段:

① Render 阶段(可中断、可异步)

  • 执行组件函数 /render
  • diff Fiber 树
  • 标记增删改
  • 可中断、可重启、可重复执行

② Commit 阶段(同步、不可中断)

  • 把 WIP 树一次性更新到 DOM
  • 执行 useEffect、useLayoutEffect、生命周期
  • 必须一次性跑完,保证 DOM 一致性

11. 什么是双缓存树(Current / WIP Tree)?

  • Current Tree:当前页面渲染的真实 Fiber 树
  • WorkInProgress Tree :内存中计算的新 Fiber 树所有 diff、中断、恢复都在 WIP 树执行,计算完成后一次性替换,保证页面无抖动、稳定渲染。

12. 什么是时间切片(Time Slicing)?

:把大渲染任务拆成约 5ms 的小任务片,每执行一片就检查主线程是否有空,没空就主动让出,不阻塞浏览器渲染、交互,保证 60fps 流畅。

13. Lanes 优先级模型是什么?

:React18 用位运算 Lanes替代旧优先级,更精细:

  • Immediate(同步最高)
  • UserBlocking(用户交互)
  • Normal(普通更新)
  • Low、Idle(低优先级)高优先级可以插队、中断、废弃低优先级任务。

SSR / 服务端:

14. React18 对 SSR 做了什么优化?

  • 流式渲染(Streaming SSR):HTML 边渲染边发送,不用等全部数据
  • Selective Hydration:选择性注水,只先激活交互组件
  • 配合 Suspense,非关键模块延迟加载,首屏速度大幅提升

场景实战题:

15. 输入框搜索大列表卡顿,怎么用 React18 优化?

:用 useTransition 把搜索 / 列表渲染标记为非紧急更新:

javascript 复制代码
const [isPending, startTransition] = useTransition()
const onChange = (e) => {
  setInput(e.target.value)
  startTransition(() => {
    setList(filterBigList(e.target.value))
  })
}

输入是高优先级,永远流畅;列表是低优先级,可中断。

16. 自动批处理如何关闭?(少数深挖题)

:用 ReactDOM.flushSync() 包裹,强制同步更新:

javascript 复制代码
flushSync(() => {
  setState()
})

总结:

  1. React18 核心:并发渲染 + createRoot
  2. 自动批处理:所有场景合并 setState
  3. 并发三剑客:可中断、优先级、时间切片
  4. useTransition:管动作useDeferredValue:管
  5. Fiber:链表循环替代递归,实现可中断
  6. 双缓存:Current/WIP,保证 DOM 稳定
  7. 两阶段:Render 可中断,Commit 不可中断
相关推荐
reasonsummer2 小时前
【白板类-01-01】20260326水果连连看01(html+希沃白板)
前端·html
HelloReader2 小时前
Qt Quick 视觉元素、交互与自定义组件(七)
前端
We་ct2 小时前
LeetCode 153. 旋转排序数组找最小值:二分最优思路
前端·算法·leetcode·typescript·二分·数组
程序员阿峰2 小时前
前端3D·Three.js一学就会系列:第二 画线
前端·three.js
HelloReader2 小时前
Qt Quick Controls 控件库、样式与布局(八)
前端
兔司基2 小时前
Node.js/Express 实现 AI 流式输出 (SSE) 踩的坑:为什么客户端会“瞬间断开连接”?
前端
一水鉴天2 小时前
智能代理体系 20260325(腾讯元宝)
人工智能·架构
yuki_uix2 小时前
一次 CR 引发的思考:我的 rules.ts 构想,究竟属于哪种开发哲学?
前端·ai编程
LONGZETECH2 小时前
新能源汽车动力蓄电池仿真教学软件技术解析——职教数字化解决方案
架构·汽车·汽车仿真教学软件·汽车教学软件·新能源汽车仿真教学软件