React Native Fabric 渲染链路

本文梳理 React Native 新架构(Fabric)"前端触发渲染 → 上屏"的完整链路,覆盖三个阶段:JS 侧构建影子树、ShadowTree commit、主线程 mount 上屏。涉及 JS、C++、iOS 原生三层,以 iOS 为例(Android 流程类似,mounting 层实现不同)。

一、链路总览

sequenceDiagram participant JS as JS 线程 participant MC as MountingCoordinator participant Main as 主线程 JS->>JS: setState → Fiber reconcile(render 阶段) JS->>JS: commit 阶段:createNode / cloneNode* / appendChild JS->>JS: completeRoot → UIManagerBinding::completeSurface JS->>JS: UIManager::completeSurface → ShadowTree::commit JS->>JS: tryCommit(布局 + CAS 写入 currentRevision_) JS->>MC: push(revision)(只留最新,合并一帧多次 commit) JS-->>JS: 返回,不等消费 Note over MC: 跨线程桥梁(mutex + condition_variable) Main->>MC: pull(按帧节奏) MC-->>Main: 返回最新 revision Main->>Main: pullTransaction → diff(对比 base vs latest) Main->>Main: 执行 mutations → UIView / CALayer Main-->>Main: Core Animation 下一个 vsync 上屏

三个关键阶段:

  1. 构建影子树(JS 线程):reconciler 逐节点 createNode(新增)/ cloneNode*(更新,不可变树结构共享)/ appendChild(挂载),最后 completeRoot 整树提交。ShadowNode 活在 C++,JS 只持有不透明 handle 并发指令。
  2. commit(JS 线程):ShadowTree 乐观锁 CAS、Yoga 布局、产出新 revision 推给 MountingCoordinator。
  3. 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 步骤:

  1. 读旧 revision :共享锁读 currentRevision_,记下 oldRevision(含 oldRootShadowNode 和它的 number)。
  2. 执行 transaction:调用回调,基于旧 Root 构建新 RootShadowNode。
  3. State 协调:ShadowNode 的 State(原生侧状态,如 ScrollView 的 contentOffset)可能过时,这一步把过时 State 推进到最新,复用旧树跳过未变分支。
  4. Commit Hooks:在布局和写入前,给 hook 们机会观察或替换新 Root(见 3.3)。
  5. 布局:对新 Root 跑 Yoga 布局,算出各节点 frame,收集受影响节点。
  6. CAS 写入 (乐观锁):独占锁内检查 currentRevision_.number 是否还等于 oldRevision.number------若相等说明期间没人插队,把新 Root 封装成新 revision 写入 currentRevision_;若不等说明被别的 commit 抢先了,返回失败,外层循环重试。封印树(seal)使其不可再改。
  7. 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 递增):

sequenceDiagram participant JS as JS 线程 participant MC as MountingCoordinator participant Main as 主线程 JS->>MC: push(rev2) 存 lastRevision_(加锁,只留最新) JS-->>JS: 返回,不等消费 Note over MC: 一帧内再 push(rev3)<br/>rev2 被丢弃,只留 rev3 Main->>MC: pull(按帧节奏) MC-->>Main: 返回 rev3 Main->>Main: diff(rev3 vs base) + mount

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,大致分三步:

  1. Scheduler 接收并节流:Scheduler 收到 commit 完成通知,默认不立即 mount,而是把 mount 请求挂到 RuntimeScheduler 的渲染队列,等 JS event loop 的"渲染步骤"统一执行(对应 Web event loop 的 render step)。这是一帧内多次 commit 的 mount 请求合并成一次的地方。
  2. 桥接到 ObjC:Scheduler 在 C++ 侧,iOS 视图操作在 ObjC,靠 SchedulerDelegateProxy 把回调转成 ObjC 调用,由 RCTSurfacePresenter 交给 RCTMountingManager。
  3. 派发主线程 + 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 生成 Update mutation 并递归子树;遇 tag 不同即 break。
  • Stage 2 一方到末尾 :剩余旧节点全 Remove+Delete+递归销毁;剩余新节点全 Create+Insert+递归建子树。
  • Stage 3 乱序 :用 TinyMap<Tag> 索引剩余新节点,双指针扫描 old/new:
    • tag 匹配 → updateMatchedPair + 递归子树。
    • 旧 tag 已被插入 → 视为 reorder,生成 Remove。
    • 旧 tag 不在新列表 → Remove,最后批量 Delete+销毁子树。
    • 新节点无匹配 → Insert,最后批量 Create+建子树。

(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 → Update mutation(props 没变,children 变)→ 进 children。
  • 位置 0:&11 == &11? 是 → 整棵 a 子树跳过(指针相等剪枝)。
  • 位置 1:old 是 12、new 是 13 → tag 不同 → 进乱序分支:
    • 13 是新 tag → Insert mutation,最后批量 Create。
    • 12 在新树后位找到 → 视为 reorder → Remove(从原位置摘)+ 后续 Insert(插到新位置)。
  • 22 vs 22':同 tag → Update(text:"B"→"B2")。

主线程 mount 端按"先拆后建"顺序应用 mutation(Remove(逆序) → Delete → Create → Insert):

  • Create tag 13/23 → dequeue ComponentView(优先复用池)。
  • Insert 13 到 10(位置 1)→ [view10 addSubview:view13]。
  • Remove 12 从原位置 1 → 摘下(不销毁,准备重插)。
  • Insert 12 到 10(位置 2)→ 重挂。
  • Update 22' → 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 项) --- 零操作(指针相等剪枝)

核心机制小结

  1. 不可变树 + 结构共享:任何修改都 clone,clone 时未变 children 用 shared_ptr 复用。clone 链长度 = 树深,与节点总数无关。
  2. 三个 clone 变体 :只改 props 用 cloneNodeWithNewProps、只改 children 用 cloneNodeWithNewChildren、都改用 cloneNodeWithNewChildrenAndProps。
  3. appendChild vs set :非根节点有父,用 appendChild(走 native)挂子;root 没父,用 createChildSet+appendChildToSet 攒 set 后 completeRoot 提交。
  4. 删除无显式 native 调用 :JS 侧新树不再包含该节点,completeRoot 提交后 diff 自然产出 Remove+Delete mutation。
  5. diff 靠指针相等剪枝:未变子树整棵跳过。
  6. 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

两层帧节流:

  1. JS 侧:RuntimeScheduler::scheduleRenderingUpdate + updateRendering------一帧内多次 commit 合并到 event loop render step 一次性 mount。
  2. 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
相关推荐
enjoywindstorm2 天前
移动端原生框架对比评测
react native
传奇开心果编程2 天前
【声明式UI开发实用技术学与练】第8课 状态提升与下放
学习·flutter·react native·ui·swiftui·composer
传奇开心果编程3 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程4 天前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
500844 天前
React Native for OpenHarmony 实战:三方库 react-native-url-polyfill 的鸿蒙化适配指南
javascript·react native·react.js·性能优化·electron·harmonyos
传奇开心果编程4 天前
【现代声明式UI学与练】第9课 性能优化——渲染优化、列表优化、内存优化、启动优化
学习·flutter·react native·ui·性能优化·swiftui·android jetpack
500844 天前
React Native for OpenHarmony 实战:三方库 react-native-volume-control 的鸿蒙化适配指南
javascript·react native·react.js·electron·harmonyos
500849 天前
React Native for OpenHarmony 实战:三方库 react-native-crypto-js 的鸿蒙化适配指南
javascript·react native·性能优化·electron·harmonyos
500849 天前
React Native for OpenHarmony 实战:三方库 react-native-device-name 的鸿蒙化适配指南
深度学习·react native·react.js·机器学习·harmonyos
500849 天前
React Native for OpenHarmony 实战:三方库 react-native-torch 的鸿蒙化适配指南
javascript·react native·react.js·harmonyos