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 用 RawPropsUIManagerBinding.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.cppcreateAndInstallIfNeededUIManagerBinding 作为 nativeFabricUIManager 挂到 JS global,get 按方法名返回 jsi::Function::createFromHostFunction。本渲染链路涉及 8 个树构建方法:

JS 方法 说明
createNode 新建 ShadowNode。props → RawPropsuiManager->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/finalizeUpdatesmountChildComponentView: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.jsLibraries/Renderer/implementations/ReactFabric-prod.jsLibraries/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.mmReact/Fabric/RCTSurfacePresenter.mm
ComponentView React/Fabric/Mounting/RCTComponentViewProtocol.hReact/Fabric/Mounting/RCTComponentViewRegistry.mmReact/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
相关推荐
光影少年8 小时前
RN 原生动画 Animated 用法、动画性能优化
react native·react.js·掘金·金石计划
任磊abc1 天前
react native项目配置eslint+prettier
react native·eslint·prettier
sTone873751 天前
类RN框架通过Service暴露卡片渲染能力给AI Chat
android·react native
想你依然心痛2 天前
【共创季稿事节】HarmonyOS 7.0 应用框架(ArkUI-X)跨平台能力深度探索
flutter·react native·arkui-x·跨平台渲染·harmonyos 7.0·arkrender·组件兼容性
大龄秃头程序员2 天前
RN 新架构 Bridgeless 模式下原生向 RN 发事件:一次踩坑与复盘
react native
大龄秃头程序员2 天前
React Native 0.86 新架构实战:Fabric 原生组件开发的 4 个致命坑
react native
光影少年3 天前
RN适配方案:屏幕适配、分辨率适配、刘海屏/全面屏适配
react native·react.js·掘金·金石计划
墨狂之逸才3 天前
React Native 设备唯一 ID:MediaDrm + Keychain 实战
react native
光影少年3 天前
RN中的StyleSheet.create 好处、内联样式弊端
react native·react.js·掘金·金石计划