主题
09 · RN Fabric:三线程模型与渲染流水线
目标:说清 Fabric 下 JS / 布局后台 / UI 三线程如何协作;掌握 Render → Commit → Mount;对照旧渲染器痛点能口述「为什么要上 Fabric」。
前置:07 · RN 入门 · 08 · 原生模块与打包
下一篇:10 · Fabric C++ 核心
本篇讲 React Native Fabric 的三线程并发模型,以及一帧 UI 如何走完 Render → Commit → Mount。
一句话核心:JS 负责算内容,后台线程负责算尺寸和位置,UI 线程只负责绘制——C++ 侧用不可变数据,三线程可并发而不互相踩数据。
一、 为什么 C++ 的“不可变性 (Immutability)”能带来线程安全?
文中提到:
“通过在框架内部使用不可变的数据结构……这意味着 React 中的每次更新都会创建或克隆新对象,而不是更新数据结构。”
在传统多线程开发中,最可怕的问题是“数据竞争(Race Condition)”——如果 UI 线程正在读取节点 $A$ 的宽度,而 JS 线程同时在修改节点 $A$ 的宽度,程序就会直接崩溃(Crash)或出现画面错乱。
Fabric 的解法:
- 不管谁想修改数据,严禁直接修改原对象。
- 比如要改变节点 $A$ 的颜色,引擎会直接克隆(Clone)出一个新的节点 $A'$,把属性改好,然后用指针切换。
- 这样一来,UI 线程在读旧的 $A$,JS 线程在造新的 $A'$,两个线程互不打扰,完全不需要加锁,这就是“线程安全”。
二、 三个线程具体分工(The Three Threads)
这里定义的 3 个线程,构成了 Fabric 渲染流水线(Pipeline)的黄金三角:
1. UI 线程(Main Thread / 主线程)
角色:“唯一的建筑工人”。
分工:这是 iOS / Android 原生操作系统规定的唯一可以操作真实视图(
UIView/android.view.View)的线程。职责:
处理手势触摸事件。
拿着后台线程算好的尺寸坐标,去创建/挂载/绘制真正的原生控件(Mount 阶段)。
铁律:这个线程绝对不能卡顿,一旦阻塞超过 16ms(60帧)或 8ms(120帧),用户就会明显感觉到界面掉帧或卡死。
2. JavaScript 线程(JS Thread)
- 角色:“设计师 / 业务逻辑脑”。
- 分工:专门运行你的 React / JS 业务代码的地方。
- 职责:
- 执行组件函数,算你的
useState、useEffect。 - 生成/计算虚拟 DOM(React Element),决定屏幕上“应该显示哪些组件”(Render 阶段)。
- 算完后,把结果丢给 C++ 核心。
3. 后台线程(Background / Layout Thread)
角色:“精算的测量师”(专门跑 C++ 的线程)。
分工:这是 Fabric 新架构中专门分离出来计算布局(Flexbox / Yoga)的线程。
职责:
拿到 JS 线程给的 C++ Shadow Tree 后,专门在这个线程里运行 Yoga 布局引擎。
计算每一个 View 到底长宽是多少、$X/Y$ 坐标在哪里(Commit 阶段)。
为什么要把布局拆到后台线程? 在旧架构里,布局计算有时候会占用 UI 线程或 JS 线程,导致卡顿。Fabric 把它独立成后台线程后,就算界面布局极其复杂(算几千个节点的 Flexbox),也不会阻塞 UI 线程的滑动响应,也不会影响 JS 线程的业务逻辑响应。
三、 完整协同工作示例(一个更新是如何在 3 个线程间流转的)
以我们前面聊到的“数据请求成功后 setData 刷新界面”为例,看看这三个线程是如何像流水线一样配合的:
text
[ JS 线程 ] [ 后台线程 (C++) ] [ UI 线程 (Native) ]
1. 收到数据,执行 React 代码
2. 算完 Virtual DOM (Render)
3. JSI 创建 C++ Shadow Tree ──►
4. Yoga 引擎在此线程算布局
5. 计算每个节点的 X/Y/Width/Height (Commit)
6. 把计算好的指令打包通知 ──►
7. 拿着坐标在屏幕上真实绘制 (Mount)
8. 用户眼睛看到最终界面1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
关键的“并发”突破:
如果在第 4 步(后台线程正在算布局)时,用户手指突然滑动了屏幕,UI 线程可以毫无影响地继续高帧率处理手势动画;如果用户同时又点了别处,JS 线程也可以并行处理新的点击。
总结
这段话揭示了 Fabric 新架构能达到原生般流畅的终极密码:
- 不可变数据结构:奠定了零锁、高并发的基石,克隆对象代替直接修改。
- 职责分离:JS 线程只管逻辑与 DOM 树生成,后台线程专门用 C++ 高速算 Flexbox 布局,UI 线程专心做轻量级的原生控件挂载与手势响应。三个线程各司其职,互不抢占资源。
在 React / React Native(特别是 Fabric 架构)中,渲染(Render)、提交(Commit)和挂载(Mount) 描述了一段 UI 代码从“JSX 文本”最终变成屏幕上“真实像素点”的三个核心阶段。
你可以把这个过程想象成建造一座大楼的完整流水线:
text
[ 1. 渲染 (Render) ] ──► [ 2. 提交 (Commit) ] ──► [ 3. 挂载 (Mount) ]
设计师画蓝图 (JS) 测量/建筑评估 (C++) 工人现场盖楼 (Native UI)1
2
2
下面我们结合代码和之前的 Fabric 架构,把这三个阶段拆透:
1. 渲染阶段(Render Phase)—— “画蓝图”
- 发生在哪:JavaScript 线程
- 谁在干活:React 引擎
- 干了什么:执行你的 React 组件代码,算出 UI 应该长什么样。
当你写了 <Text>Hello</Text> 或调用了 setData() 时,React 会执行组件函数,生成一棵 虚拟 DOM 树(Virtual DOM Tree / React Element Tree)。
- 比喻:设计师在纸上(内存里)画建筑蓝图。这时候屏幕上没有任何变化,甚至原生系统(iOS/Android)都完全不知道这件事。
Fabric 的突破:在 React 18+ 的 Fabric 架构下,渲染阶段是**可被中断(Interruptible)**的。如果设计师正在画列表第 100 行的蓝图,突然用户点了一下屏幕(高优先级),React 可以暂停画蓝图,先去响应用户的点击。
2. 提交阶段(Commit Phase)—— “计算结构与布局”
- 发生在哪:C++ 线程(Fabric 核心)
- 谁在干活:Fabric 引擎 + Yoga 布局引擎
- 干了什么:把 JS 的蓝图翻译成原生的数据结构,并计算出确切的位置和大小。
当 JS 线程画好蓝图后,它会通过 JSI 把这棵树传给 C++ 层,在 C++ 内存中创建/更新 C++ Shadow Tree(阴影树)。
在这个阶段,Yoga 引擎会干一件极度繁重的事——算布局(Layout Calculation):
- 这个
<View>的flex: 1到底是多少像素? - 这个
<Text>在当前屏幕上有多宽多高? - 它的 $X$ 和 $Y$ 坐标分别在哪里?
- 比喻:工程监理拿到了设计图,在电脑里做 3D 建模,精确计算出每块砖头的尺寸、重力支撑和具体放置坐标。
3. 挂载阶段(Mount Phase)—— “现场施工与上屏”
- 发生在哪:UI 主线程(Native OS 线程)
- 谁在干活:iOS (UIKit) / Android (View 系统)
- 干了什么:拿到算好的坐标和尺寸,创建真实的原生控件,画到手机屏幕上。
C++ 层把计算好的 C++ Shadow Tree 变更打包成一条条原生指令(例如:“在 $(x:10, y:20)$ 处创建一个长 $100$ 高 $50$ 的 UIButton”)。
原生 UI 线程收到指令后:
- 创建 View:在内存中实例化真实的
UIView(iOS) 或android.view.View(Android)。 - 挂载到屏幕(Mount):把这个 View 加到手机当前页面的视图树(View Hierarchy)里。
- GPU 绘制:手机 GPU 把 View 渲染成光栅化像素,你的眼睛终于看到了界面变化。
- 比喻:施工队拿到精准的施工图纸,在真实的土地上用砖头水泥(原生 View)把大楼盖起来,并涂上颜料。
三个阶段的对比总结
我们用刚才点击“删除 Item”的例子来串联这三个阶段:
tsx
// 用户点击删除,触发 setState
const handleDelete = () => {
setList(list.filter(item => item.id !== 3));
};1
2
3
4
2
3
4
- Render 阶段 (JS):React 执行组件,Diff 算出:“新的 Virtual DOM 树里少了一个 Item”。
- Commit 阶段 (C++):Fabric 在 C++ 层更新 C++ Shadow Tree,Yoga 重新计算:“第 3 个 Item 移除,第 4 个 Item 的 Y 坐标向上平移 50px”。
- Mount 阶段 (Native):Native UI 线程执行:“销毁第 3 个原生 View,并把后续 View 执行平移动画,呈现给用户”。
核心区别一览表
| 阶段 | 执行环境 | 产物 | 耗时主要在 | 能否被中断? |
|---|---|---|---|---|
| 1. 渲染 (Render) | JS 线程 | 虚拟 DOM 树 (React Element) | JS 逻辑执行、Diff 计算 | 可以(React 18 并发模式) |
| 2. 提交 (Commit) | C++ 线程 | C++ Shadow Tree + 布局坐标 | Flexbox 布局计算 (Yoga) | 否(必须一次性算完布局) |
| 3. 挂载 (Mount) | Native UI 线程 | 真实的 iOS/Android View | 原生 View 实例化与绘制 | 否(必须在当前帧快速完成) |
如果把 RN 的新架构比作一辆跑车,JSI 是引擎,TurboModules 是传动系统,而 Fabric 就是全新的底盘与悬挂系统。它的核心使命是:彻底解决旧渲染器在复杂页面、长列表、手势动画中的卡顿、白屏和异步不同步问题。
一、 为什么需要 Fabric?(旧渲染器的三大痛点)
在旧架构(Paper 渲染器)中,UI 的渲染需要跨越三层(JS 线程 $\rightarrow$ Bridge 桥 $\rightarrow$ Shadow 线程 $\rightarrow$ UI 线程):
text
[ 旧渲染器流程 ]
JS 代码计算 UI 结构 ──(JSON 字符串化)──> [ Bridge 异步通道 ] ──> Shadow 线程计算 Flexbox 布局 ──> UI 线程创建原生 View1
2
2
这套旧机制存在 3 个致命缺陷:
- 异步 Bridge 瓶颈(Asynchronous Bridge) 所有 UI 变更必须序列化为 JSON 字符串,通过 Bridge 异步传给原生层。异步意味着 JS 线程和 UI 线程无法同步。
- 典型痛点:在快速滑动
FlatList时,UI 线程已经滑到了新位置,但 JS 线程还没把新渲染的 JSON 传过来,导致用户看到大片白屏。
- 重复的 Shadow 树(多重内存开销) 旧架构需要在 C++ 层(Yoga 布局引擎)、JS 层、以及原生 iOS/Android 层各维护一份 UI 树结构,内存占用大且同步困难。
- 无法配合 React 18 的 Concurrent(并发)特性 由于旧渲染流程是纯异步且单线程阻塞的,无法实现“高优先级渲染(如用户输入)打断低优先级渲染(如后台列表加载)”的能力。
二、 Fabric 的核心架构与工作原理
Fabric 基于 JSI (JavaScript Interface) 重新设计,使得 JS 线程可以直接持有 C++ 对象的引用,彻底抛弃了 JSON 序列化和异步 Bridge。
1. Fabric 的三层渲染流
在 Fabric 中,UI 的计算与渲染流程简化为:
text
[ JS 线程 ] [ C++ 线程 (Fabric 核心) ] [ UI 线程 (Native) ]
React 组件 (JSX) ──(JSI)──► 1. 创建/更新 C++ Shadow Tree ──► 2. 映射为原生 Views (ComponentDescriptor)
(Yoga 直接在此计算布局) (主线程同步绘制)1
2
3
2
3
- Render Phase (渲染阶段):JS 执行 JSX 代码,通过 JSI 在 C++ 层直接创建 C++ Shadow Tree(相比以前,C++ 成了唯一的中间状态,不再有多重树拷贝)。
- Commit Phase (提交阶段):Yoga 布局引擎在 C++ 层直接计算节点的位置和宽高(Layout calculation)。
- Mount Phase (挂载阶段):C++ Shadow Tree 被转化为原生的 View Tree,最终渲染到屏幕上。
三、 Fabric 带来的四大突破性优势
1. 同步测量与渲染 (Synchronous Execution)
- 以前:你想在 JS 里测量一个 View 的真实宽高(
measure),必须异步发送请求给 Native,Native 算好再异步传回 JS,会导致画面闪烁。 - Fabric:因为有 JSI,JS 可以同步(Synchronously)调用 C++ 布局引擎的方法,直接在当帧拿到测量数据并更新 UI,彻底解决微调布局时的闪烁问题。
2. 彻底解决快速滑动“白屏”问题
Fabric 允许原生 UI 线程在需要时,同步拉取 JS 线程的渲染结果。在快速滑动列表时,UI 线程不再被动等待 Bridge 的 JSON 消息,而是能在同一帧内直接拿到 C++ 节点的渲染数据,极大缓解了长列表滑动的白屏现象。
3. 完全拥抱 React 18 / 19 并发特性 (Concurrent Features)
Fabric 支持 React 的并发渲染模型:
- 优先中断:如果用户正在输入(高优先级),Fabric 可以暂停正在后台渲染的大列表(低优先级),优先响应输入,保证用户交互永远是 60/120 帧流畅的。
- Transition API:支持
startTransition、useDeferredValue等并发 Hook。
4. 跨平台 Consistency(C++ 统一底层)
旧架构中,iOS 和 Android 的很多渲染逻辑是各自用 Objective-C 和 Java 分开写的,容易导致两端样式细节不一致。 Fabric 将大部分核心逻辑(布局计算、事件分发、Shadow Tree 管理)全部移到了 C++ 层,iOS 和 Android 只是作为最外层的“显示外壳”。这保证了两端渲染行为的高度一致性,也大幅减少了跨平台 Bug。
四、 开发者如何应对?(迁移与开发变化)
对于普通业务开发者来说,Fabric 的底层变化大部分是无感(Transparent)的,你依然写正常的 JSX 和 React 代码。但对于组件库开发者或原生开发者,需要注意以下变化:
- 自定义 Native UI 组件写法变了
- 旧方式:继承
SimpleViewManager(Android) 或RCTViewManager(iOS)。 - Fabric 方式:必须使用 TypeScript 编写 Codegen Schema,然后通过 Codegen 自动生成 C++ 头文件和 Native 模版代码,以确保类型安全与 JSI 绑定。
- Codegen (代码生成器) Fabric 引入了 Codegen 工具。你在 TS 里定义的组件 Props 接口,会在编译期自动转化为 C++ 和原生端的强类型接口。如果类型写错,编译直接报错,彻底告别了以前“运行时类型不匹配导致 Native 崩溃”的问题。
总结
| 维度 | 旧渲染器 (Paper) | 新渲染器 (Fabric) |
|---|---|---|
| 通信媒介 | 异步 JSON Bridge | JSI (C++ 直接调用) |
| 布局计算 | 跨线程异步传参计算 | C++ 层同步/高效计算 |
| 渲染模式 | 纯异步,容易产生白屏 | 支持同步挂载与并发渲染 (React Concurrent) |
| UI 状态树 | JS / C++ / Native 多份拷贝 | C++ 统一管理 Shadow Tree |
| 类型安全 | 依赖运行时校验 | Codegen 编译期强类型绑定 |
一句话总结:Fabric 通过 C++ 统一底层 和 JSI 直连,把 React Native 的渲染能力推到了接近纯原生的性能高度,是 RN 摆脱“性能差、卡顿”标签的决定性一步。
