本文梳理 React Native 新架构(Fabric)"前端触发渲染 → 上屏"的完整链路,覆盖三个阶段:JS 侧构建影子树、ShadowTree commit、主线程 mount 上屏。涉及 JS、C++、iOS 原生三层,以 iOS 为例(Android 流程类似,mounting 层实现不同)。
一、链路总览
三个关键阶段:
- 构建影子树(JS 线程):reconciler 逐节点 createNode(新增)/ cloneNode*(更新,不可变树结构共享)/ appendChild(挂载),最后 completeRoot 整树提交。ShadowNode 活在 C++,JS 只持有不透明 handle 并发指令。
- commit(JS 线程):ShadowTree 乐观锁 CAS、Yoga 布局、产出新 revision 推给 MountingCoordinator。
- mount 上屏(主线程):从 MountingCoordinator pull 出 transaction(此时 diff),把 mutations 应用到 UIView。
二、阶段一:JS 触发 → C++ 入口(JS 线程)
2.1 Surface 与 React Reconciler
Surface 是 Fabric 的一个独立渲染单元------一块 RN 根视图区域,封装一棵组件树 + 专属 ShadowTree/Fiber 根/原生视图。一个 App 可有多个 Surface 并存,各自独立渲染、独立生命周期(start/stop)。后面链路里的 surfaceId 标识操作作用于哪个 Surface。
React Reconciler 是 React 的核心协调器,负责把组件树(JSX/函数组件)转成可渲染的内部树(Fiber 树),并算出每次更新后哪些节点变了。它分两阶段工作:
- render 阶段:遍历组件树,执行函数组件、diff props/state,算出 workInProgress 树并给变化节点打 flags(可中断、可恢复)。
- commit 阶段 :根据 flags 执行副作用------对 Fabric 而言,就是通过桥方法(
createNode/cloneNode*/appendChild/completeRoot)把变化同步成 C++ ShadowNode 的 clone,最终触发上屏。不可中断。
简单说:reconciler 决定"树变成什么样",桥方法把决定同步给 native。
运行时代码路径(原生创建一个 Surface 时触发):
csharp
原生 startSurface(surfaceId, moduleName, props)
→ JS: runApplication(appKey, params) // AppRegistryImpl.js
→ renderApplication // renderApplication.js
→ Renderer.renderElement(rootTag, useFabric) // RendererImplementation.js
→ ReactFabric.render // ReactFabric.js
→ updateContainer(element, fiberRoot) // 进入 React reconciler
reconciler 接手后走 render/commit,首次渲染通过 createNode(建节点)+ appendChild(挂父子关系)+ completeRoot(整树提交)在 C++ 建第一棵 ShadowNode 树,mount 上屏(见后续章节)。
2.2 Fiber 树
React reconciler 的工作区。每个组件对应一个 fiber 节点,用 child/sibling/return 三指针单链表组织成树。fiber 的 stateNode 是个包装对象,.node 字段指向对应的 C++ ShadowNode handle------两棵树靠它连接(见 2.3)。
reconciler 用双缓冲维护两份 fiber 树:current(已提交,只读)和 workInProgress(正在算,可写),通过 alternate 字段两两配对。算更新时只动 workInProgress,current 整个 render 阶段不动;commit 阶段才原子切换(fiberRoot.current 指针从旧树指向 workInProgress)。这让 render 可中断、可恢复、可被高优先级抢占,而 current 始终完整可读。
建 workInProgress 时按变化路径区分对待,不会弄脏 current:
- 未变分支:workInProgress 直接指向 current 的同一个 fiber 对象(只读共享,不写新值)。
- 变化路径 :调
createWorkInProgress(current),返回的是 current 的 alternate(另一个独立对象),在它上面覆写字段。current 那份对象没被碰。
Fiber 节点对象本身可变(可覆写 alternate 的字段),但 current 树语义上不可变(更新走 workInProgress 新链,不原地改 current)。
reconciler diff children 时用 key(+ type)判断元素是否复用:同 key 同 type 复用旧 fiber(clone workInProgress,state 保留),否则新建 fiber。复用走 cloneNode(tag 延续)、新建走 createNode(新 tag),这层决策对应到 Fabric 的 tag 身份(见 4.3)。
2.3 ShadowNode 树
Fabric 真实渲染用的影子树,由 C++ 侧的 UIManager 维护。ShadowNode 活在 C++ 堆上,不活在 JS;JS 侧只持有一个通过 JSI 暴露的不透明 handle(HostObject 包装),fiber 的 stateNode.node 就是指向它的 handle------两棵树靠此连接。
reconciler 在 render 阶段算 workInProgress、标记 flags,在 commit 阶段才通过桥方法把变化同步成 ShadowNode 的 clone。本质是 JS 发起指令,C++ 执行实际操作:
createNode/cloneNode*是 JS→C++ 跨界调用,C++ 侧用 ComponentDescriptor 新建/复制 ShadowNode 并返回 handle。- props 用
RawProps(UIManagerBinding.cpp:229)传过去,C++ 侧解析成强类型 Props(如ViewProps)。 - 整棵影子树构建完,native 已持有强类型树,
completeRoot只需传根引用,不用再序列化。
这是新架构相对老架构 Paper 的核心提速点:没有 JSON 序列化、没有异步 batch,全是同步 JSI 调用。
与 Fiber 树对照:ShadowNode 树 C++ 完全不可变只能 clone,Fiber 树对象可变但 current 语义不可变。两套机制不同,但"不原地改已提交的树、未变分支不重建"是共性。用户感知的"更新 node 树"通常指 ShadowNode 树,但实际算变更的是 Fiber 树。
2.4 构建 ShadowNode 树的桥方法
React Fiber reconciler 在 commit 阶段调 hostConfig 方法,直接桥到 global.nativeFabricUIManager(C++ UIManagerBinding)。
UIManagerBinding.cpp 的 createAndInstallIfNeeded 把 UIManagerBinding 作为 nativeFabricUIManager 挂到 JS global,get 按方法名返回 jsi::Function::createFromHostFunction。本渲染链路涉及 8 个树构建方法:
| JS 方法 | 说明 |
|---|---|
createNode |
新建 ShadowNode。props → RawProps → uiManager->createNode(tag, viewName, surfaceId, RawProps, instanceHandle) |
cloneNodeWithNewChildren |
clone 节点 + 全新 children list(props 不变) |
cloneNodeWithNewProps |
clone 节点 + 新 props(children 不变)。局部更新主力 |
cloneNodeWithNewChildrenAndProps |
clone 节点 + 新 props + 新 children |
appendChild |
把子 ShadowNode 挂到父 ShadowNode(走 native,改 C++ 影子树结构)。非 root 节点挂子用 |
createChildSet |
创建空 children set,攒 root 的子节点用(root 没父,不能 appendChild,改用 set + completeRoot 提交) |
appendChildToSet |
把子节点 push 进 set。纯 JS list 操作,不走 UIManager |
completeRoot |
整树提交触发点 。解析 surfaceId + shadowNodeList → uiManager->completeSurface(surfaceId, list, {.enableStateReconciliation=true, .mountSynchronously=false, .source=React}) |
2.5 completeSurface → ShadowTree::commit
这是 JS 桥方法进入 C++ 渲染引擎的衔接点。completeRoot 跨进 C++ 调 UIManager::completeSurface,它做三件事:按 surfaceId 从 ShadowTreeRegistry 找到该 Surface 专属的 ShadowTree;用传进来的 rootChildren 构造新的 RootShadowNode(基于旧 Root clone,替换 children);调 ShadowTree::commit 提交,进入第三章的 commit 流程。提交成功后更新一致性缓存。
三、阶段二:ShadowTree commit(JS 线程)
3.1 commit 流程
commit 是把新 ShadowTree 写入当前状态的过程。先说清两个关键概念:
- revision :ShadowTree 的一次提交快照,包含根 ShadowNode(指向整棵不可变树)+ 版本号 number + 遥测数据。每个 revision 有独立的树结构(从根能遍历整棵树),但未变节点靠 shared_ptr 在多个 revision 间共享,所以新建一个 revision 只需 clone 变化路径上的节点,不深拷贝整棵树。ShadowTree 用
currentRevision_持有当前已提交的状态,每次 commit 产生一个 number+1 的新 revision 替换它(旧的不存历史,靠引用计数存活------被 MountingCoordinator 等持有就活着,没人引用就析构)。 - transaction:本次提交要产生的新树。reconciler 在 commit 阶段已通过 createNode/cloneNode*/appendChild 把各子节点的 ShadowNode 改好,transaction 把这些变化汇总成一棵完整的新树交给 commit。commit 的核心就是执行它拿到新树,再写入 revision。
ShadowTree::commit 循环调 tryCommit 直到成功,tryCommit 步骤:
- 读旧 revision :共享锁读
currentRevision_,记下oldRevision(含 oldRootShadowNode 和它的 number)。 - 执行 transaction:调用回调,基于旧 Root 构建新 RootShadowNode。
- State 协调:ShadowNode 的 State(原生侧状态,如 ScrollView 的 contentOffset)可能过时,这一步把过时 State 推进到最新,复用旧树跳过未变分支。
- Commit Hooks:在布局和写入前,给 hook 们机会观察或替换新 Root(见 3.3)。
- 布局:对新 Root 跑 Yoga 布局,算出各节点 frame,收集受影响节点。
- CAS 写入 (乐观锁):独占锁内检查
currentRevision_.number是否还等于oldRevision.number------若相等说明期间没人插队,把新 Root 封装成新 revision 写入currentRevision_;若不等说明被别的 commit 抢先了,返回失败,外层循环重试。封印树(seal)使其不可再改。 - mount:把新 revision 推给 MountingCoordinator,通知 delegate 进入 mounting 阶段。
乐观锁的意义:commit 耗时主要在 clone/布局,这部分各算各的不需互斥;只有最后写入
currentRevision_时需要保证一致性------CAS 校验版本号没变才写入,冲突者重试,避免对整个 commit 持长锁。
3.2 事务传递与跨线程桥梁
commit 产出的新 revision 要从 JS 线程传到主线程去 mount,两线程不能直接互调,靠 MountingCoordinator 做桥梁------它挂在 ShadowTree 上,专门在两条线程间传递 revision 并解耦双方节奏。JS 线程只管推(push),不等主线程消费;主线程按帧节奏取(pull),不催 JS 线程。
下图用 rev2、rev3 代指同一帧内两次 commit 产出的两个 revision(number 递增):
MountingCoordinator 内部用互斥锁 + 条件变量做同步,起到三个作用:
- 传递 revision:push 存、pull 取,都加锁避免并发读写。
- 合并一帧内的多次 commit:push 只保留最新 revision,旧的丢弃。一帧内 commit 多次,主线程只需 mount 最后一次,中间过渡状态不上屏。
- 节奏解耦:JS 线程 commit 完即返回;主线程按帧 pull,来晚了新 revision 直接覆盖旧值,想等可阻塞到有新 revision。
diff 不在 commit(JS 线程)里做,而是延迟到主线程 pull 时才算(见第四章)。这是「延迟 diff」------只在真正要 mount 时才算,commit 产出的 revision 可以攒着等主线程一起处理。
3.3 CommitHooks
commit 流程里留的扩展点:在布局和写入之前,给注册的 hook 们一个机会观察或修改即将提交的新树,可用于布局动画、遥测、自动回滚等。
四、阶段三:Mounting 上屏(主线程)
4.1 从 Scheduler 到主线程 mount
commit 完成后,通知沿链路传到主线程执行 mount,大致分三步:
- Scheduler 接收并节流:Scheduler 收到 commit 完成通知,默认不立即 mount,而是把 mount 请求挂到 RuntimeScheduler 的渲染队列,等 JS event loop 的"渲染步骤"统一执行(对应 Web event loop 的 render step)。这是一帧内多次 commit 的 mount 请求合并成一次的地方。
- 桥接到 ObjC:Scheduler 在 C++ 侧,iOS 视图操作在 ObjC,靠 SchedulerDelegateProxy 把回调转成 ObjC 调用,由 RCTSurfacePresenter 交给 RCTMountingManager。
- 派发主线程 + pull 事务 :RCTMountingManager 把事务派发到主线程(mount 涉及 UIView,必须在主线程),并用串行化保证 mount 不重入。实际处理时从 MountingCoordinator pull 出一个 MountingTransaction(其中含 diff 出的 mutations,diff 在此发生),分三段执行:准备 → 应用 mutations 到视图 → 收尾通知。
少数场景(如首屏、需要立即可见)会用同步模式直接 mount,不进队列。
4.2 tag:节点的稳定身份
每个 ShadowNode 有个 tag(整数 ID),是节点的稳定身份标识。它由 reconciler 在 createNode 时分配,贯穿节点整个生命周期------clone 出新对象时 tag 不变,节点删除时 tag 才消失。所以同一个逻辑节点在新旧两个 revision 里是不同的 C++ 对象(指针不同),但 tag 相同。
tag 派生自 React 的 key 体系(见 2.2):React 复用 fiber 时走 cloneNode,tag 延续;新建 fiber 时走 createNode,分配新 tag。所以 tag 相同 = 同一个逻辑节点,tag 不同 = 不同节点。
diff 配对就靠 tag 判断身份,另有更强的剪枝条件:指针相同(&old == &new,连 clone 都没发生)直接跳过整棵子树。三者优先级:指针相同 > tag 相同 > tag 不同。
4.3 ShadowView:diff 的对比对象
diff 不直接对比 ShadowNode,而是先构造 ShadowView 再对比。ShadowView 是 ShadowNode 的轻量投影,保留 diff/mount 需要的字段(tag、props、layoutMetrics、state、children),去掉内部元数据。好处是更轻、缓存友好,且 mounting 层不依赖 ShadowNode 内部结构。
ShadowView 依然保留 children,diff 靠递归对比 children 判断子树变化,靠指针相等(&old == &new,未变子树 shared_ptr 复用同一对象)跳过整棵未变子树。
flattening:不渲染的节点会被压平
ShadowNode 原始树里,有些节点只是用来参与布局(撑尺寸、定位),自身不产生任何可见渲染------比如一个只设了 flex 布局、无背景/边框/内容的 <View> 容器。这类节点对最终视图无贡献,却占一个 ShadowNode + 对应一个 UIView,是浪费。
flattening 把这类"只布局不渲染"的节点压平:它的 children 直接提升到父节点,自身从视图树里消失。这样 ShadowView 树比 ShadowNode 原始树节点更少------既少 diff、也少 mount 的 UIView。
压平是有条件的:节点的 ViewProps 标记为可 flatten(常见于纯布局 View),且它的 props 不会影响渲染(无背景色、无边框、无透明度等)。一旦节点带上了会影响渲染的属性(如背景色),就不能被压平,必须保留为独立视图。
diff 阶段遇到 flattening/unflattening 变化(某节点这次该压平、或上次压平了这次不该压平),会生成对应的 reparent mutation:把 children 从原父摘下、挂到新父。
4.4 diff 算法(pullTransaction 内,主线程)
关键:diff 不在 commit(JS 线程)里做 ,而是延迟到主线程 pullTransaction 时按需调用 calculateShadowViewMutations(baseRevision.rootShadowNode, lastRevision.rootShadowNode)------只在真正要 mount 时才算。
入口把两棵 Root 各构造一个 ShadowView,然后同步遍历这两棵树 ------从根开始,每个节点配成一对 (旧, 新) 对比,再递归往下走 children 配对。和单树遍历骨架一样,只是同时走两棵、对应位置比较。
css
旧树 (base) 新树 (last)
Root 对比 Root'
/ | \ ↓ / | \
A B C 配 pair A X B'
| | | ↓ | | |
... 递归每个子 pair ...
遍历时的两个关键优化:
- 指针相等剪枝 :若某对
(旧, 新)是同一对象(未变子树 shared_ptr 复用),整棵子树跳过,不再往下走。 - 配对处理:children 不总是位置一一对应(子节点顺序变、数量变),要分段配对------这就是下面三个 Stage 的事。
配对靠 tag 判断身份(tag 相同=同一节点、tag 不同=不同节点,见 4.2),指针相同则更强------整棵子树跳过。
核心三阶段
- Stage 1 对齐前缀 :从前往后扫,指针相等的子树直接跳过(
&old == &new,第一道筛子);否则同位置同 tag 同 concreteness/flattening 的 pair 生成Updatemutation 并递归子树;遇 tag 不同即 break。 - Stage 2 一方到末尾 :剩余旧节点全
Remove+Delete+递归销毁;剩余新节点全Create+Insert+递归建子树。 - Stage 3 乱序 :用
TinyMap<Tag>索引剩余新节点,双指针扫描 old/new:- tag 匹配 →
updateMatchedPair+ 递归子树。 - 旧 tag 已被插入 → 视为 reorder,生成
Remove。 - 旧 tag 不在新列表 →
Remove,最后批量Delete+销毁子树。 - 新节点无匹配 →
Insert,最后批量Create+建子树。
- tag 匹配 →
(Un)flattening:处理 ViewProps flattening 导致的树塌缩/展开 reparent,按 Flatten/Unflatten 分别发 Remove/Insert。
mutation 应用顺序 :destructiveDownward(后序拆要删的子树,叶子先)→ Update → Remove(逆序,防索引错位) → Delete → Create → downward(先序建新子树,父先)→ Insert,保证 mount 端按「先拆后建」安全顺序应用。
4.5 mutations → UIView 操作
diff 的产出是一个有序的 mutation 列表 ShadowViewMutationList,每条 mutation 装一次视图操作,按"先拆后建"顺序排列。单条 mutation 的主要字段:
type:mutation 类型(Create/Insert/Update/Remove/Delete)。parentShadowView:父节点(Insert/Remove 时要知道挂到哪个父、从哪个父摘)。childShadowView:操作的目标节点。index:位置(Insert/Remove 时在父的 children 中的下标)。oldChildShadowView/newChildShadowView:Update 时的旧/新 child,用于对比 props 变化。
mount 端遍历这个列表逐条执行,按类型分发到对应的 UIView 操作:
| Mutation | 操作 |
|---|---|
| Create | 从 RCTComponentViewRegistry dequeue 一个 ComponentView |
| Insert | updateProps/updateState/updateLayoutMetrics/finalizeUpdates 后 mountChildComponentView:index:(加 subview) |
| Update | 对比 old/new props/state/layoutMetrics,按位掩码 RNComponentViewUpdateMask 只更新变化项,最后 finalizeUpdates:mask |
| Remove | unmountChildComponentView: |
| Delete | 回收进 RCTComponentViewRegistry 的 recycle pool |
dequeueComponentViewWithComponentHandle:tag: 优先从 recycle pool 取,否则 RCTComponentViewFactory 新建。所有操作 RCTAssertMainQueue()。
4.6 ComponentView 复用池(recycle pool)
Create/Delete 操作的 UIView 不是每次都新建/销毁,而是走 RCTComponentViewRegistry 的复用池------Delete 时回收进池,Create 时优先从池取,避免频繁 alloc/dealloc UIView。
进池条件(Delete 时 enqueue):
shouldBeRecycled为 true:ComponentView 自己声明可复用。不支持复用的组件类型(状态复杂、复用会出问题)会置 false,直接销毁不进池。- 池未满:每种组件类型上限 1024 个,超过则当前视图直接销毁,不再入池(有界缓存,满了丢弃新的、保已有的)。
- 已从父视图摘下(
superview == nil):还挂着就回收是非法的。
满足条件时先 prepareForRecycle 清状态、tag 置 0,再 push 进池。池按 ComponentHandle(组件类型)分桶------View 回 View 桶、Text 回 Text 桶,复用时只从对应类型取,保证类型匹配。
出池 (Create 时 dequeue):按 componentHandle 从对应桶 LIFO 取最后一个;桶空才调 RCTComponentViewFactory 新建。取出后设新 tag、注册进 registry。
直接销毁不进池的情况 :组件不支持复用、池满、内存警告(UIApplicationDidReceiveMemoryWarning 时整个池清空,所有回收视图释放)。
进出都重设 tag:同一个 UIView 对象会被复用给不同节点(不同 tag),所以进池前置 0、出池后设新 tag,不保留旧身份。
4.7 上屏的最终一步
这一步不再执行 mutations(Create/Insert/Update 等在 4.5 已应用完,UIView 层级和属性都改好了),而是 mount 事务的收尾:
- 发收尾通知:mount 完成后通知 observers(让上层知道这次 mount 结束),并上报本次 mount 的遥测数据。
- 交给系统上屏:Fabric 改的只是 UIView/CALayer 的数据(layer tree),真正显示到屏幕是系统的事------主 runloop 休眠后,Core Animation 在下一个 vsync 把 layer tree 提交给 render server 渲染,这才上屏。Fabric 不自己驱动上屏。
所以从 mutations 应用完到真正上屏,中间还隔一个 runloop 周期(通常一帧内),由 Core Animation 完成。
五、端到端示例:初始化与更新
用一个列表页把前面三个阶段串起来走一遍。初始 2 项,更新后变 3 项(中间插入 + 末尾一项文本变化):
jsx
function List() {
const [items, setItems] = useState([{id:'a',text:'A'}, {id:'b',text:'B'}]);
// 更新后: [{id:'a',text:'A'}, {id:'x',text:'X'}, {id:'b',text:'B2'}]
return (
<View> {/* tag 10, 容器 */}
{items.map(it => (
<View key={it.id}> {/* tag 11(a) / 12(b) / 13(x 新) */}
<Text>{it.text}</Text> {/* tag 21 / 22 / 23 */}
</View>
))}
</View>
);
}
场景一:首次创建(mount)
reconciler 首次渲染,commit 阶段逐节点 createInstance + 挂载:
scss
1. createNode(tag=10, "View", ...) → ShadowNode[10] (容器)
2. createNode(tag=11, "View", ...) → ShadowNode[11] (a 项)
3. createNode(tag=21, "Text", {text:"A"}, ...) → ShadowNode[21]
4. appendChild(parent=[11], child=[21]) → 21 挂到 11 (走 native)
5. createNode(tag=12, "View", ...) → ShadowNode[12] (b 项)
6. createNode(tag=22, "Text", {text:"B"}, ...) → ShadowNode[22]
7. appendChild(parent=[12], child=[22]) → 22 挂到 12
8. completeRoot(surfaceId, rootChildren=[[10]]) ← 整树提交触发
非根节点首次挂载用
appendChild(走 native)逐个挂子;root 用completeRoot提交。
commit 后进入 diff + mount(主线程 pullTransaction):old 空、new 有 → 对 10/11/12/21/22 各产出 Create + Insert,mount 端 dequeue 5 个 ComponentView 并 addSubview。5 次 createNode + 2 次 appendChild + 1 次 completeRoot。
场景二:局部更新(中间插入 x 项 + b 项文本变 B2)
setItems 后新树 children = a, x, b,其中:
- a 项(tag 11/21):完全没变 → shared_ptr 复用。
- x 项(tag 13/23):新增。
- b 项(tag 12/22):tag 12 的 props 没变,tag 22 的 text 变了。
JS 侧 clone 链(只 clone 变化路径,a 整棵子树复用):
ini
1. createNode(tag=13, "View", ...) → ShadowNode[13] (x 项, 新建)
2. createNode(tag=23, "Text", {text:"X"}, ...) → ShadowNode[23]
3. appendChild(parent=[13], child=[23]) → 23 挂到 13 (走 native)
4. cloneNodeWithNewProps(node=[22], newProps={text:"B2"})
→ ShadowNode[22'] (只 b 项文本变了, clone 22 带 newProps; children 复用)
5. 容器 10 的 children 变了 → cloneNodeWithNewChildren(node=[10])
→ ShadowNode[10'] (容器 children 变, props 不变; 新 children 由 C++ 从原节点+变化推导)
6. root 的 children 变了 → 用 createChildSet + appendChildToSet 攒新 set = [10']
→ completeRoot(surfaceId, set) (root 没父, 用 set 提交)
注意 set 只用于 root 这层。非根节点(如 10)更新 children 走 clone 变体,children 由 C++ 侧从原节点和本次变化推导,不在 JS 攒 set。
关键观察:
- a 项整棵子树(tag 11/21)零操作------既不 createNode 也不 clone,靠 shared_ptr 复用。
- clone 链 =
22' → 10' → root,长度 ≈ 树深。 - 新增节点 x 用
createNode+appendChild(和首次挂载一样)。 - 文本变化用
cloneNodeWithNewProps(只带新 props)。 - 容器 children 变化用
cloneNodeWithNewChildren。
commit 后进入 diff(主线程 pullTransaction 内):
- Root:
&old == &new? 否 → 进 children。 - 10 vs 10':同 tag →
Updatemutation(props 没变,children 变)→ 进 children。 - 位置 0:
&11 == &11? 是 → 整棵 a 子树跳过(指针相等剪枝)。 - 位置 1:old 是 12、new 是 13 → tag 不同 → 进乱序分支:
- 13 是新 tag →
Insertmutation,最后批量Create。 - 12 在新树后位找到 → 视为 reorder →
Remove(从原位置摘)+ 后续Insert(插到新位置)。
- 13 是新 tag →
- 22 vs 22':同 tag →
Update(text:"B"→"B2")。
主线程 mount 端按"先拆后建"顺序应用 mutation(Remove(逆序) → Delete → Create → Insert):
Createtag 13/23 → dequeue ComponentView(优先复用池)。Insert13 到 10(位置 1)→[view10 addSubview:view13]。Remove12 从原位置 1 → 摘下(不销毁,准备重插)。Insert12 到 10(位置 2)→ 重挂。Update22' →view22.text = "B2"(位级 props 对比,只改 text 字段)。- a 项(11/21):完全不动 UIView。
两场景对比
| 维度 | 首次创建 | 局部更新(插 1 项 + 改 1 项文本) |
|---|---|---|
| createNode | 5 次 | 2 次(仅新增 x 项) |
| cloneNode* | 0 次 | 2 次(22'、10') |
| appendChild(native) | 2 次 | 1 次(23→13) |
| completeRoot | 1 次 | 1 次 |
| diff 产出 mutation | 5×(Create+Insert) | Create+Insert(x)、Remove+Insert(b 重排)、Update(b 文本) |
| 主线程 UIView 操作 | 5 新建 + 5 挂载 | 2 新建 + 1 挂载 + 1 摘重挂 + 1 setText |
| 未变子树(a 项) | --- | 零操作(指针相等剪枝) |
核心机制小结
- 不可变树 + 结构共享:任何修改都 clone,clone 时未变 children 用 shared_ptr 复用。clone 链长度 = 树深,与节点总数无关。
- 三个 clone 变体 :只改 props 用
cloneNodeWithNewProps、只改 children 用cloneNodeWithNewChildren、都改用cloneNodeWithNewChildrenAndProps。 - appendChild vs set :非根节点有父,用
appendChild(走 native)挂子;root 没父,用createChildSet+appendChildToSet攒 set 后completeRoot提交。 - 删除无显式 native 调用 :JS 侧新树不再包含该节点,
completeRoot提交后 diff 自然产出 Remove+Delete mutation。 - diff 靠指针相等剪枝:未变子树整棵跳过。
- props 位级更新:mount 端按位掩码只改变化字段。
六、线程模型与同步总结
| 阶段 | 线程 | 同步机制 |
|---|---|---|
| 构建 ShadowNode(createNode/clone/append) | JS 线程 | 无锁,reconciler 单线程 |
| ShadowTree::commit(含布局、state 协调、hooks、CAS) | JS 线程 | shared_mutex 乐观锁 CAS,revision 号变化即重试 |
| MountingCoordinator::push | JS 线程(commit 末尾) | std::mutex + condition_variable,只保留最新 revision 号 |
| diff(calculateShadowViewMutations) | 主线程(pullTransaction 内) | 调用方线程,加 MountingCoordinator mutex |
| mount(mutations → UIView) | 主线程 | RCTAssertMainQueue(),_transactionInFlight 串行化 |
| 上屏 | 系统主线程 | Core Animation vsync 提交 layer tree |
两层帧节流:
- JS 侧:
RuntimeScheduler::scheduleRenderingUpdate+updateRendering------一帧内多次 commit 合并到 event loop render step 一次性 mount。 - UI 侧(两道关):
MountingCoordinator::push一帧内多次 commit 只保留最新 revision,中间过渡态被丢弃、主线程只 mount 最终态;RCTMountingManager串行化保证一个 mount 事务处理完才开始下一个,避免事务重入交叉搞乱视图层级。
七、关键文件索引
| 层 | 文件 |
|---|---|
| JS 入口 | Libraries/ReactNative/AppRegistry.js、Libraries/Renderer/implementations/ReactFabric-prod.js、Libraries/ReactNative/FabricUIManager.js |
| JSI 绑定 | ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp |
| UIManager | ReactCommon/react/renderer/uimanager/UIManager.cpp |
| ShadowTree | ReactCommon/react/renderer/mounting/ShadowTree.cpp |
| diff | ReactCommon/react/renderer/mounting/Differentiator.cpp |
| MountingCoordinator | ReactCommon/react/renderer/mounting/MountingCoordinator.cpp |
| Scheduler | ReactCommon/react/renderer/scheduler/Scheduler.cpp |
| RuntimeScheduler | ReactCommon/react/runtimescheduler/RuntimeScheduler_Modern.cpp |
| iOS MountingManager | React/Fabric/Mounting/RCTMountingManager.mm |
| iOS Scheduler 桥 | React/Fabric/RCTScheduler.mm、React/Fabric/RCTSurfacePresenter.mm |
| ComponentView | React/Fabric/Mounting/RCTComponentViewProtocol.h、React/Fabric/Mounting/RCTComponentViewRegistry.mm、React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm |