ArkUI 条件渲染组件完整指南

文档版本:v1.0 更新时间:2026-02-05 源码版本:OpenHarmony ace_engine (master 分支) 作者:基于 CLAUDE.md 规范自动生成


目录

  1. 概述
  2. IfElse 组件详解
  3. 分支管理机制
  4. 状态保留与复用
  5. 使用示例
  6. 性能优化
  7. 常见问题
  8. 与 if-else 语法对比
  9. 附录

概述

组件定位

IfElse 组件是 OpenHarmony ArkUI 框架中实现条件渲染的核心语法节点,基于 if-elseif-else 构建声明式 UI 的条件分支切换能力。

核心特性

  • 分支标识管理:使用 branchId_ 唯一标识 if-elseif-else 构造中的各个分支
  • 状态保留:通过 TryRetake() 机制支持分支节点的复用和状态保留
  • 动画支持:在分支切换时支持过渡动画(通过 allowTransition 参数)
  • 几何过渡:支持 geometryTransition 动画效果,实现平滑的组件过渡

技术栈

  • 前端:ArkTS/TypeScript(声明式前端)、Cangjie(CJ 前端)
  • 桥接层:JSViewAbstract + Model Layer
  • 核心层:NG Syntax Architecture(IfElseNode)
  • 渲染层:Rosen + Skia

代码规模

  • 核心文件:约 5 个文件
  • 核心代码:约 500+ 行 C++ 代码
  • 涉及 2 个架构层次(Core、Bridge)

架构位置

┌─────────────────────────────────────────────────────────┐
│  前端 ArkTS / Cangjie                                   │
│                                                          │
│  if (this.condition) {                                  │
│    Text("Branch A")                                      │
│  } else if (this.otherCondition) {                      │
│    Text("Branch B")                                      │
│  } else {                                                │
│    Text("Branch C")                                      │
│  }                                                       │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  Bridge Layer (桥接层)                                   │
│                                                          │
│  声明式前端:frameworks/bridge/declarative_frontend/    │
│              jsview/js_if_else.cpp                       │
│  CJ 前端:  frameworks/bridge/cj_frontend/interfaces/   │
│              cj_ffi/cj_if_else_ffi.cpp                   │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  Core Layer (IfElseNode)                                 │
│                                                          │
│  class IfElseNode : public UINode                       │
│    源码:frameworks/core/components_ng/syntax/          │
│          if_else_node.h:27                               │
│                                                          │
│  核心方法:                                                │
│  - SetBranchId(): 设置分支ID,触发分支切换                │
│  - TryRetake(): 尝试复用之前分支的节点                    │
│  - FlushUpdateAndMarkDirty(): 刷新并标记脏状态            │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  UINode 基类方法                                          │
│                                                          │
│  - Clean(): 清理子节点                                    │
│  - GetDisappearingChildById(): 获取消失的子节点           │
│  - CollectCleanedChildren(): 收集已清理的子节点           │
│  - MarkNeedFrameFlushDirty(): 标记需要刷新                │
└─────────────────────────────────────────────────────────┘

IfElse 组件详解

组件定位

IfElseNode 是 ArkUI 条件渲染的核心实现类,继承自 UINode,负责管理 if-elseif-else 构造中的多个分支。

继承关系

UINode (基类)
    ↓
IfElseNode (语法节点)
    ↓
管理多个分支的子节点

关键特性

  • IsSyntaxNode() 返回 true:标识这是一个语法节点(非组件节点)
  • IsAtomicNode() 返回 false:不是原子节点

源码位置

  • 头文件:frameworks/core/components_ng/syntax/if_else_node.h:27
  • 实现:frameworks/core/components_ng/syntax/if_else_node.cpp:21-132

核心数据结构

1. branchId_ - 分支标识符

// 源码:if_else_node.h:63-66
// 唯一标识 if-elseif-else 构造中的分支
// -1 表示未初始化,首次渲染前
int32_t branchId_ = -1;

作用

  • 标识当前激活的分支(if 分支、elseif 分支或 else 分支)
  • 值为 -1 表示未初始化(首次渲染前)
  • 每个分支由编译器生成唯一的常量 ID

分支 ID 生成规则(推测):

  • if 分支:0
  • elseif 分支:1, 2, 3, ...
  • else 分支:最后一个 ID

2. branchIdChanged_ - 分支变更标志

// 源码:if_else_node.h:68-70
// 由 SetBranchId 设置,FlushUpdateAndMarkDirty 清除
bool branchIdChanged_ = false;

作用

  • 标识分支是否发生变更
  • SetBranchId() 设置为 true(如果分支确实变化)
  • FlushUpdateAndMarkDirty() 清除为 false

3. retakenElmtIds_ - 重新获取的节点 ID 列表

// 源码:if_else_node.h:72
// 重新获取的节点ID列表
std::list<int32_t> retakenElmtIds_;

作用

  • 存储从 TryRetake() 成功复用的节点 ID
  • 用于通知前端哪些节点被复用
  • 避免重复创建和销毁节点

SetBranchId 流程

完整流程图

┌─────────────────────────────────────────────────────────┐
│  1. SetBranchId 被调用                                   │
│     源码:if_else_node.cpp:65-78                        │
└─────────────────────────────────────────────────────────┘
                          ↓
          ┌─────────────────────────────┐
          │ 比较 branchId_ 与 value      │
          │ branchIdChanged_ =          │
          │ (branchId_ != value)        │
          └─────────────────────────────┘
                          ↓
              ┌───────────────────────┐
              │ branchIdChanged_?     │
              └───────────────────────┘
                 ↓              ↓
              false          true
                 ↓              ↓
          ┌─────────┐    ┌─────────────────────────────┐
          │ 直接返回 │    │ 2. 清理旧分支子节点           │
          └─────────┘    │ Clean(false, true, branchId_)│
                         │ 源码:if_else_node.cpp:74    │
                         └─────────────────────────────┘
                                    ↓
                         ┌─────────────────────────────┐
                         │ 3. 收集已清理的子节点 ID       │
                         │ CollectCleanedChildren()    │
                         │ - removedElmtId: 被移除的ID  │
                         │ - reservedElmtId: 被保留的ID │
                         └─────────────────────────────┘
                                    ↓
                         ┌─────────────────────────────┐
                         │ 4. 更新分支 ID               │
                         │ branchId_ = value            │
                         └─────────────────────────────┘
                                    ↓
┌─────────────────────────────────────────────────────────┐
│  5. FlushUpdateAndMarkDirty 被调用                      │
│     源码:if_else_node.cpp:80-96                        │
└─────────────────────────────────────────────────────────┘
                          ↓
              ┌───────────────────────┐
              │ branchIdChanged_?     │
              └───────────────────────┘
                 ↓              ↓
              false          true
                 ↓              ↓
          ┌─────────┐    ┌─────────────────────────────┐
          │ 直接返回 │    │ 6. 查找父 FrameNode         │
          └─────────┘    │ 遍历父节点链                  │
                         └─────────────────────────────┘
                                    ↓
                         ┌─────────────────────────────┐
                         │ 7. 通知父容器子节点已更新     │
                         │ frameNode->ChildrenUpdatedFrom(0)│
                         │ 源码:if_else_node.cpp:87    │
                         └─────────────────────────────┘
                                    ↓
                         ┌─────────────────────────────┐
                         │ 8. 标记需要刷新               │
                         │ MarkNeedFrameFlushDirty(     │
                         │   PROPERTY_UPDATE_BY_CHILD_REQUEST)│
                         │ 源码:if_else_node.cpp:93    │
                         └─────────────────────────────┘
                                    ↓
                         ┌─────────────────────────────┐
                         │ 9. 清除变更标志               │
                         │ branchIdChanged_ = false     │
                         └─────────────────────────────┘
                                    ↓
                    [触发布局和渲染更新]

源码分析

① SetBranchId 实现 - if_else_node.cpp:65-78

void IfElseNode::SetBranchId(int32_t value, std::list<int32_t>& removedElmtId, std::list<int32_t>& reservedElmtId)
{
    // 检查分支是否真正变化
    branchIdChanged_ = (branchId_ != value);

    TAG_LOGD(AceLogTag::ACE_IF, "IfElse(%{public}d).SetBranchId branchIdChanged_: %{public}d",
        GetId(), branchIdChanged_);

    if (branchIdChanged_) {
        // 收集所有子节点及其子节点的 elmtId
        // 这些将被移除,但如果存在动画可能会有延迟
        // list of elmtIds is sent back to calling TS ViewPU.ifElseBranchUpdateFunction()

        // Clean() 方法:
        // - cleanDirectly = false: 不立即清理,允许动画
        // - allowTransition = true: 允许转场动画
        // - branchId_: 当前分支 ID(用于识别要清理的分支)
        Clean(false, true, branchId_);

        // 收集已清理的子节点 ID
        // - removedElmtId: 被移除的节点 ID
        // - reservedElmtId: 被保留的节点 ID(用于复用)
        CollectCleanedChildren(GetChildren(), removedElmtId, reservedElmtId, true);

        // 更新分支 ID
        branchId_ = value;
    }
}

关键参数解释

参数 含义
cleanDirectly false 不立即清理,允许执行消失动画
allowTransition true 允许转场动画(如 transition 参数指定的动画)
branchId branchId_ 当前分支 ID(在更新前),用于标识要清理的分支

② FlushUpdateAndMarkDirty 实现 - if_else_node.cpp:80-96

void IfElseNode::FlushUpdateAndMarkDirty()
{
    if (branchIdChanged_) {
        // 查找父节点
        auto parent = GetParent();
        while (parent) {
            // 尝试转换为 FrameNode
            auto frameNode = AceType::DynamicCast<FrameNode>(parent);
            if (frameNode) {
                // 通知父容器从索引 0 开始的子节点已更新
                // 这将触发父容器重新测量和布局
                frameNode->ChildrenUpdatedFrom(0);
                break;
            }
            parent = parent->GetParent();
        }

        // 标记父节点 dirty 以触发 measure
        // PROPERTY_UPDATE_BY_CHILD_REQUEST: 由子节点请求的属性更新
        MarkNeedFrameFlushDirty(PROPERTY_UPDATE_BY_CHILD_REQUEST);
    }

    // 清除分支变更标志
    branchIdChanged_ = false;
}

关键逻辑

  1. 向上查找 FrameNode

    • 遍历父节点链,直到找到第一个 FrameNode
    • IfElseNode 本身是 UINode,其父节点可能是另一个 UINodeFrameNode
  2. 通知父容器子节点更新

    • ChildrenUpdatedFrom(0):通知父容器从索引 0 开始的所有子节点都可能已更新
    • 这将触发父容器的布局算法重新计算
  3. 标记脏状态

    • PROPERTY_UPDATE_BY_CHILD_REQUEST:由子节点请求的属性更新
    • 触发 OnDirtyLayoutWrapperSwap 等生命周期方法

TryRetake 机制

基本原理

TryRetake 机制允许在分支切换时复用之前渲染过的节点,而不是每次都重新创建。这对于以下场景非常有用:

  • 保留 UI 状态:如输入框内容、滚动位置
  • 性能优化:避免重复创建和销毁节点
  • 动画过渡:配合 geometryTransition 实现平滑过渡

调用时机

┌─────────────────────────────────────────────────────────┐
│  前端条件变化                                             │
│  if (this.condition)  →  else if (this.otherCondition)  │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  前端调用 IfElseNode.SetBranchId()                       │
│  - 旧分支节点被标记为消失(disappearing)                  │
│  - 旧分支节点未被立即销毁,而是保留在 disappearing 队列   │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  前端构建新分支内容                                        │
│  - 对每个子节点,先调用 If.canRetake(id)                 │
│  - 如果返回 true,调用 IfElseNode.TryRetake(id)          │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  TryRetake 执行流程                                       │
│  1. GetDisappearingChildById(id, branchId_)             │
│     从消失队列中查找节点                                  │
│  2. SetJSViewActive(true)                               │
│     重新激活节点                                          │
│  3. AddChild(node)                                       │
│     将节点重新添加到子节点列表                             │
│  4. UpdateAllGeometryTransition(node)                   │
│     更新几何过渡动画                                       │
│  5. CollectRetakenNodes(node)                           │
│     收集被复用的节点 ID                                    │
└─────────────────────────────────────────────────────────┘

源码分析

TryRetake 实现 - if_else_node.cpp:98-111

bool IfElseNode::TryRetake(const std::string& id)
{
    // 从消失队列中获取指定 ID 的节点
    // branchId_: 之前分支的 ID,用于限定搜索范围
    auto node = GetDisappearingChildById(id, branchId_);
    if (node) {
        ACE_SCOPED_TRACE("IfElse TryRetake validate.");

        // 重新激活节点
        node->SetJSViewActive(true);

        // 将节点重新添加到子节点列表
        AddChild(node);

        // 对于 geometryTransition,让所有重用的子节点调用 UpdateGeometryTransition
        LayoutProperty::UpdateAllGeometryTransition(node);

        // 收集被复用的节点 ID
        CollectRetakenNodes(node);

        return true;  // 成功复用
    }
    return false;  // 未找到可复用的节点
}

CollectRetakenNodes 实现 - if_else_node.cpp:113-121

void IfElseNode::CollectRetakenNodes(const RefPtr<UINode>& node)
{
    // 将节点 ID 添加到复用列表
    retakenElmtIds_.emplace_back(node->GetId());

    // 如果不是 CustomNode,递归收集所有子节点的 ID
    if (GetTag() != V2::JS_VIEW_ETS_TAG) {
        for (auto const& child : node->GetChildren()) {
            CollectRetakenNodes(child);
        }
    }
}

GetRetakenElmtIds 实现 - if_else_node.cpp:123-130

bool IfElseNode::GetRetakenElmtIds(std::list<int32_t>& retakenElmtIds)
{
    if (retakenElmtIds_.size() == 0) {
        return false;
    }

    // 将复用列表移动到输出参数
    retakenElmtIds.splice(retakenElmtIds.end(), retakenElmtIds_);
    return true;
}

分支管理机制

branchId 标识

编译器生成的分支 ID

ArkTS 编译器在处理 if-elseif-else 语法时,会为每个分支生成唯一的常量 ID:

// ArkTS 源码
if (this.condition1) {
  Text("Branch A")
} else if (this.condition2) {
  Text("Branch B")
} else {
  Text("Branch C")
}

// 编译器生成的伪代码(简化版)
If.create()
if (this.condition1) {
  this.ifElseBranchUpdateFunction(0, () => {  // branchId = 0
    Text("Branch A")
  })
} else if (this.condition2) {
  this.ifElseBranchUpdateFunction(1, () => {  // branchId = 1
    Text("Branch B")
  })
} else {
  this.ifElseBranchUpdateFunction(2, () => {  // branchId = 2
    Text("Branch C")
  })
}
If.pop()

源码注释 - if_else_node.cpp:35-64

/*
 * how if else works
 * the transpiler generated JS code is like this(simplified, but main points shown)
 * this.observeComponentCreation(..., () => {
 * If.create();
 * if (condition1) {
 *    this.ifElseBranchUpdateFunction(compilerGeneratedUniqueConstBranchID, () =>
 *          code for children first render
 *    )
 * } else if (condition2) {
 *    this.ifElseBranchUpdateFunction(compilerGeneratedUniqueConstBranchID, () =>
 *          code for children first render
 *    )
 * } else {
 *    // else added by the transpiler
 *    If.setBranchId(compilerGeneratedUniqueConstBranchID)
 * }
 * If.pop()
 * - if re-renders whenever one of the variables used in 'condition' during last render has changed.
 *      - The dependent variables can change from one render of if to the next, depending if 'condition1' and
 *        'condition2' bind to different variables.
 *      - If can re-render, but the branch does not change, e.g. if 'condition1' is (this.i > 20)
 *        then If will re-render whenever i changes.
 * - eDSL to JS traspiler generates a unique id for each branch of the code inside.
 *   Thereby the framework can detect that the branch has actually changed.
 *   In this case ViewPU.ifElseBranchUpdateFunction will call IfElseNode::SetBranchId to upate
 *   and it will execute the 2nd parameter lambda function to re-generate the children.
 * - In case of if without else, or if ... else if ... , eDSL to JS traspiler generates
 *   an extra else clause in which to set the branchId (calls IfElseNode::SetBranchId)
 * - IfElseNode::FlushUpdateAndMarkDirty is called upon If.Pop()
 */

分支 ID 状态转换

┌─────────────────────────────────────────────────────────┐
│  初始状态:branchId_ = -1                                │
│  表示未初始化,首次渲染前                                 │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  首次渲染:SetBranchId(0)                                 │
│  branchId_ = 0(if 分支)                                 │
│  branchIdChanged_ = true                                 │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  条件变化:SetBranchId(1)                                 │
│  branchId_ = 1(elseif 分支)                             │
│  branchIdChanged_ = true                                 │
│  清理分支 0 的子节点                                       │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  条件再次变化:SetBranchId(0)                             │
│  branchId_ = 0(if 分支)                                 │
│  branchIdChanged_ = true                                 │
│  清理分支 1 的子节点                                       │
│  可以通过 TryRetake 复用分支 0 的旧节点                    │
└─────────────────────────────────────────────────────────┘

分支切换流程

完整流程图

┌─────────────────────────────────────────────────────────┐
│  1. 条件变量变化                                           │
│  this.condition1 从 true 变为 false                      │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  2. 状态管理框架触发重新渲染                               │
│  @State 装饰的变量变化会触发组件重新渲染                    │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  3. 前端执行条件判断                                       │
│  if (this.condition1) { ... }                            │
│  else if (this.condition2) { ... }                       │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  4. 调用 ifElseBranchUpdateFunction                       │
│  this.ifElseBranchUpdateFunction(newBranchId, () => {   │
│    // 重新构建新分支的子节点                               │
│  })                                                       │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  5. 桥接层调用 SetBranchId                                │
│  IfElseModelNG::SetBranchId(newBranchId, ...)           │
│  源码:if_else_model_ng.cpp:36-42                        │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  6. IfElseNode::SetBranchId                              │
│  - 比较 branchId_ 和 newBranchId                         │
│  - 如果不同,清理旧分支子节点                               │
│  - 更新 branchId_ = newBranchId                          │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  7. 前端构建新分支内容                                     │
│  - 对每个子节点,先尝试 TryRetake                          │
│  - 如果失败,创建新节点                                    │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  8. 调用 If.Pop()                                         │
│  IfElseModelNG::Pop()                                    │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  9. IfElseNode::FlushUpdateAndMarkDirty                  │
│  - 通知父容器子节点已更新                                   │
│  - 标记需要刷新                                            │
│  - 清除 branchIdChanged_ 标志                            │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  10. 触发布局和渲染更新                                    │
│  - 父容器重新测量和布局                                    │
│  - 渲染管道绘制新分支内容                                  │
└─────────────────────────────────────────────────────────┘

节点复用策略

复用条件

节点可以被复用需要满足以下条件:

  1. 节点 ID 匹配

    • 前端提供的 ID 必须与之前分支中某个节点的 ID 相同
    • ID 通常由组件的 id 属性或编译器生成的唯一标识符决定
  2. 节点仍在消失队列中

    • 节点不能已经被销毁
    • Clean() 方法的 cleanDirectly 参数必须为 false
  3. 节点来自正确的分支

    • GetDisappearingChildById(id, branchId_) 会检查节点是否来自指定分支

复用策略对比

场景 能否复用 原因
同一分支内重新渲染 ✅ 可以 branchId 不变,子节点直接更新
切换到已存在的分支 ✅ 可以 通过 TryRetake 复用旧节点
切换到新分支 ❌ 不可以 需要创建新节点
分支切换后切回原分支 ✅ 可以 如果旧节点未销毁,可以复用

性能影响

不复用(每次创建新节点):
- 时间:~50-100ms/节点(创建 + 初始化 + 渲染)
- 内存:频繁分配和释放
- 状态:UI 状态丢失(如滚动位置、输入内容)

复用节点:
- 时间:~5-10ms/节点(查找 + 重新激活)
- 内存:无需额外分配
- 状态:UI 状态保留

状态保留与复用

TryRetake 详解

使用场景

场景 1:保留输入框内容

@State showPanel: boolean = false

if (this.showPanel) {
  Column() {
    TextInput({ placeholder: 'Enter text' })
      .id('myInput')  // 关键:设置固定 ID
  }
}

工作原理

  1. 用户在输入框输入内容
  2. showPanel 变为 false,输入框被移除但保留在消失队列
  3. showPanel 变为 true,前端调用 TryRetake('myInput')
  4. 输入框被重新添加到渲染树,内容保留

场景 2:保留滚动位置

@State tabIndex: number = 0

if (this.tabIndex === 0) {
  List() {
    ForEach(this.items, (item) => {
      ListItem() { Text(item.name) }
    })
  }
  .id('tab0List')  // 关键:设置固定 ID
}

前端调用流程

声明式前端 - js_if_else.cpp:57-72

void JSIfElse::CanRetake(const JSCallbackInfo& info)
{
    if (info.Length() != 1) {
        TAG_LOGW(AceLogTag::ACE_IF, "CanRetake needs only 1 param");
        info.SetReturnValue(JSRef<JSVal>::Make(ToJSValue(false)));
        return;
    }
    if (!info[0]->IsString()) {
        TAG_LOGW(AceLogTag::ACE_IF, "CanRetake needs string param");
        info.SetReturnValue(JSRef<JSVal>::Make(ToJSValue(false)));
        return;
    }
    auto id = info[0]->ToString();
    auto result = IfElseModel::GetInstance()->CanRetake(id);
    info.SetReturnValue(JSRef<JSVal>::Make(ToJSValue(result)));
}

对应的 ArkTS 调用(伪代码):

// 前端编译器生成的伪代码
if (this.condition) {
  const nodeId = 'myComponent'
  if (If.canRetake(nodeId)) {
    // 复用已有节点
    If.retake(nodeId)
  } else {
    // 创建新节点
    MyComponent().id(nodeId)
  }
}

CollectRetakenNodes

功能说明

CollectRetakenNodes 递归收集被复用的节点及其所有子节点的 ID:

// 源码:if_else_node.cpp:113-121
void IfElseNode::CollectRetakenNodes(const RefPtr<UINode>& node)
{
    // 添加当前节点 ID
    retakenElmtIds_.emplace_back(node->GetId());

    // 如果不是 CustomNode,递归收集子节点 ID
    if (GetTag() != V2::JS_VIEW_ETS_TAG) {
        for (auto const& child : node->GetChildren()) {
            CollectRetakenNodes(child);
        }
    }
}

为什么需要递归收集

  • 复用一个父节点时,其所有子节点也会被复用
  • 前端需要知道所有被复用的节点 ID,避免重复创建

使用示例

// 假设复用以下节点结构:
// Column (id: 'col1')
//   ├── Text (id: 'txt1')
//   └── Row (id: 'row1')
//       ├── Button (id: 'btn1')
//       └── Button (id: 'btn2')

IfElseNode::TryRetake('col1')

// retakenElmtIds_ 将包含:
// ['col1', 'txt1', 'row1', 'btn1', 'btn2']

几何过渡支持

geometryTransition 动画

geometryTransition 是 ArkUI 提供的自动布局过渡动画,可以在组件尺寸、位置变化时自动添加平滑过渡。

与 TryRetake 的配合

// 源码:if_else_node.cpp:105-106
// 对于 geometryTransition,让所有重用的子节点调用 UpdateGeometryTransition
LayoutProperty::UpdateAllGeometryTransition(node);

使用示例

@State showDetail: boolean = false

if (this.showDetail) {
  Column() {
    Text('Detail View')
      .fontSize(20)
  }
  .geometryTransition('detailView')  // 关键:设置几何过渡 ID
} else {
  Column() {
    Text('Summary View')
      .fontSize(14)
  }
  .geometryTransition('detailView')  // 相同 ID = 过渡动画
}

效果

  • showDetail 切换时,两个 Column 会平滑过渡
  • 位置、尺寸、圆角等属性会自动插值
  • 无需手动编写动画代码

使用示例

基本用法

示例 1:简单的 if-else

@Entry
@Component
struct BasicIfElse {
  @State isVisible: boolean = true

  build() {
    Column() {
      if (this.isVisible) {
        Text('Content is visible')
          .fontSize(20)
      } else {
        Text('Content is hidden')
          .fontSize(20)
          .fontColor(Color.Gray)
      }

      Button('Toggle')
        .onClick(() => {
          this.isVisible = !this.isVisible
        })
    }
  }
}

工作流程

  1. 初始状态:isVisible = true,显示 "Content is visible"
  2. 点击按钮:isVisible = false
  3. 触发重新渲染:
    • SetBranchId(0)SetBranchId(1)(假设 if 分支 ID 为 0,else 分支 ID 为 1)
    • 清理 "Content is visible" 节点
    • 创建 "Content is hidden" 节点
    • 刷新布局和渲染

示例 2:if-elseif-else

@Entry
@Component
struct ScoreLevel {
  @State score: number = 85

  build() {
    Column() {
      if (this.score >= 90) {
        Text('优秀')
          .fontSize(30)
          .fontColor(Color.Green)
      } else if (this.score >= 60) {
        Text('及格')
          .fontSize(30)
          .fontColor(Color.Orange)
      } else {
        Text('不及格')
          .fontSize(30)
          .fontColor(Color.Red)
      }

      Slider({ value: this.score, min: 0, max: 100 })
        .onChange((value: number) => {
          this.score = value
        })
    }
  }
}

分支 ID 分配(推测):

  • if 分支(score >= 90):ID = 0
  • elseif 分支(score >= 60):ID = 1
  • else 分支:ID = 2

多分支条件

示例 3:多个 elseif

@Entry
@Component
struct WeatherStatus {
  @State temperature: number = 25

  build() {
    Column() {
      if (this.temperature > 30) {
        this.WeatherCard('炎热', Color.Red, '☀️')
      } else if (this.temperature > 20) {
        this.WeatherCard('温暖', Color.Orange, '⛅')
      } else if (this.temperature > 10) {
        this.WeatherCard('凉爽', Color.Blue, '☁️')
      } else if (this.temperature > 0) {
        this.WeatherCard('寒冷', Color.Cyan, '❄️')
      } else {
        this.WeatherCard('严寒', Color.White, '🥶')
      }

      Slider({ value: this.temperature, min: -10, max: 40 })
        .onChange((value: number) => {
          this.temperature = value
        })
    }
  }

  @Builder WeatherCard(title: string, color: Color, icon: string) {
    Row() {
      Text(icon).fontSize(40)
      Text(title).fontSize(24).fontColor(color)
    }
    .padding(20)
    .backgroundColor(Color.Grey)
    .borderRadius(10)
  }
}

状态保留示例

示例 4:保留输入框内容

@Entry
@Component
struct FormWithToggle {
  @State showForm: boolean = false
  @State inputValue: string = ''

  build() {
    Column() {
      Button(this.showForm ? 'Hide Form' : 'Show Form')
        .onClick(() => {
          this.showForm = !this.showForm
        })

      if (this.showForm) {
        Column() {
          TextInput({ placeholder: 'Enter your name', text: this.inputValue })
            .id('nameInput')  // 关键:设置固定 ID
            .onChange((value: string) => {
              this.inputValue = value
            })

          Text(`Current value: ${this.inputValue}`)
            .margin({ top: 10 })
        }
        .padding(20)
        .backgroundColor(Color.Grey)
        .borderRadius(10)
      }
    }
  }
}

工作原理

  1. 用户输入 "John"
  2. 点击 "Hide Form",表单被移除
  3. 点击 "Show Form",表单重新显示
  4. 关键:输入框内容 "John" 被保留(通过 TryRetake 机制)

示例 5:保留滚动位置

@Entry
@Component
struct TabContent {
  @State currentTab: number = 0
  private tabs: string[] = ['Tab 1', 'Tab 2', 'Tab 3']

  build() {
    Column() {
      Row() {
        ForEach(this.tabs, (tab: string, index: number) => {
          Text(tab)
            .padding(10)
            .backgroundColor(this.currentTab === index ? Color.Blue : Color.Grey)
            .onClick(() => {
              this.currentTab = index
            })
        })
      }

      if (this.currentTab === 0) {
        this.TabContent(0, Array.from({ length: 100 }, (_, i) => `Item ${i}`))
      } else if (this.currentTab === 1) {
        this.TabContent(1, Array.from({ length: 50 }, (_, i) => `Option ${i}`))
      } else if (this.currentTab === 2) {
        this.TabContent(2, Array.from({ length: 200 }, (_, i) => `Data ${i}`))
      }
    }
  }

  @Builder TabContent(tabIndex: number, items: string[]) {
    List() {
      ForEach(items, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(50)
        }
      })
    }
    .id(`tab${tabIndex}List`)  // 关键:为每个 Tab 的 List 设置唯一 ID
    .width('100%')
    .height('100%')
  }
}

效果

  • 切换到 Tab 2,滚动到中间位置
  • 切换到 Tab 1
  • 切换回 Tab 2
  • 滚动位置保留(通过 TryRetake 复用 List 节点)

性能优化

分支切换性能

性能影响因素

1. 分支复杂度

简单分支(单个组件):
- 切换时间:~10-20ms
- 创建/销毁开销:低

复杂分支(嵌套组件 + 列表):
- 切换时间:~100-500ms
- 创建/销毁开销:高

2. 动画影响

无动画:
- 切换时间:~10-100ms
- 用户体验:突兀

有动画(transition/geometryTransition):
- 切换时间:~300-600ms(包括动画时长)
- 用户体验:流畅

优化建议

1. 减少分支复杂度

❌ 不推荐:

if (this.condition) {
  Column() {
    ForEach(this.largeArray, (item) => {
      ComplexItem({ item })  // 复杂组件
    })
  }
}

✅ 推荐:

// 使用 LazyForEach 替代
if (this.condition) {
  List() {
    LazyForEach(this.dataSource, (item) => {
      ListItem() {
        ComplexItem({ item })
      }
    })
  }
}

2. 避免频繁切换

❌ 不推荐:

@State toggle: boolean = false

// 每 100ms 切换一次
aboutToAppear() {
  setInterval(() => {
    this.toggle = !this.toggle
  }, 100)
}

✅ 推荐:

// 使用动画过渡,降低视觉突变
if (this.toggle) {
  Column() { ... }
  .transition({ type: TransitionType.All, opacity: 0 })
}

节点复用优化

复用性能对比

场景:切换包含 100 个 ListItem 的分支

不复用:
- 创建 100 个 ListItem:~500ms
- 销毁 100 个 ListItem:~200ms
- 总计:~700ms

复用:
- TryRetake 查找:~10ms
- 重新激活 100 个 ListItem:~50ms
- 总计:~60ms

性能提升:~90%

优化策略

1. 为需要复用的组件设置固定 ID

if (this.showPanel) {
  Column() {
    TextInput({ text: this.input })
      .id('fixedInput')  // 固定 ID

    List() {
      ForEach(this.items, (item) => {
        ListItem() { Text(item) }
      })
    }
    .id('fixedList')  // 固定 ID
  }
  .id('fixedColumn')  // 固定 ID
}

2. 避免动态生成的 ID

❌ 不推荐:

ForEach(this.items, (item, index) => {
  Text(item)
    .id(`text_${index}_${Date.now()}`)  // 每次生成都不同
})

✅ 推荐:

ForEach(this.items, (item) => {
  Text(item)
    .id(`text_${item.id}`)  // 使用数据中的固定 ID
})

动画优化建议

geometryTransition 优化

适用场景

  • 分支切换时位置、尺寸变化
  • 需要平滑过渡效果

性能考虑

  • geometryTransition 会触发额外的布局计算
  • 复杂组件可能影响性能

示例

@State expanded: boolean = false

if (this.expanded) {
  Column() {
    Text('Expanded content')
      .fontSize(20)
  }
  .width('100%')
  .height(200)
  .geometryTransition('expandingPanel')  // 启用几何过渡
} else {
  Column() {
    Text('Collapsed content')
      .fontSize(14)
  }
  .width('100%')
  .height(50)
  .geometryTransition('expandingPanel')  // 相同 ID
}

transition 动画优化

适用场景

  • 入场/出场动画
  • 透明度、旋转等效果

示例

if (this.visible) {
  Text('Hello')
    .fontSize(30)
    .transition({ type: TransitionType.Insert, opacity: 0 })
    .animation({ duration: 300, curve: Curve.EaseInOut })
} else {
  Text()  // 空节点
}

性能建议

  • 避免同时使用多个复杂动画
  • 限制动画帧率(如果可能)
  • 使用硬件加速的属性(transform > opacity > width/height)

常见问题

分支切换不生效

问题 1:条件变量未触发更新

症状

  • 修改条件变量后,UI 不更新

原因

  • 条件变量未使用 @State 装饰
  • 条件表达式未依赖 @State 变量

解决方案

❌ 错误:

toggle: boolean = false  // 缺少 @State

if (this.toggle) {
  Text('On')
}

✅ 正确:

@State toggle: boolean = false  // 添加 @State

if (this.toggle) {
  Text('On')
}

问题 2:条件表达式依赖非响应式变量

症状

  • 依赖的变量变化,但条件不重新评估

原因

  • 条件表达式中的变量不是响应式的

解决方案

❌ 错误:

private counter: number = 0  // 非 @State

@State toggle: boolean = false

if (this.counter > 10) {  // counter 变化不会触发重新渲染
  Text('Counter > 10')
}

✅ 正确:

@State counter: number = 0  // 使用 @State

if (this.counter > 10) {  // counter 变化会触发重新渲染
  Text('Counter > 10')
}

状态丢失问题

问题 1:输入框内容丢失

症状

  • 切换分支后,输入框内容被清空

原因

  • 未为输入框设置固定 ID
  • 前端无法复用节点

解决方案

if (this.showInput) {
  TextInput({ text: this.inputValue })
    .id('myInput')  // 关键:设置固定 ID
    .onChange((value: string) => {
      this.inputValue = value
    })
}

问题 2:滚动位置丢失

症状

  • 切换 Tab 后,List 滚动位置重置

原因

  • List 未设置固定 ID
  • 每次切换都创建新的 List 实例

解决方案

if (this.currentTab === 0) {
  List() {
    ForEach(this.items, (item) => {
      ListItem() { Text(item) }
    })
  }
  .id('tab0List')  // 关键:为每个 Tab 的 List 设置唯一 ID
}

性能问题

问题 1:分支切换卡顿

症状

  • 分支切换时 UI 卡顿

原因

  • 分支内容过于复杂
  • 包含大量子组件或列表

解决方案

方案 1:使用 LazyForEach

if (this.showList) {
  List() {
    LazyForEach(this.dataSource, (item) => {
      ListItem() {
        ComplexItem({ item })
      }
    })
  }
}

方案 2:使用虚拟滚动

if (this.showList) {
  List() {
    RepeatVirtualScroll(this.items)  // API 16+
      .itemCount(this.items.length)
      .itemCreator((index) => {
        ListItem() { Text(this.items[index]) }
      })
  }
}

方案 3:添加加载指示器

@State isLoading: boolean = false
@State showContent: boolean = false

async toggleContent() {
  this.isLoading = true
  this.showContent = !this.showContent

  // 等待下一帧
  await new Promise(resolve => setTimeout(resolve, 0))

  this.isLoading = false
}

if (this.isLoading) {
  LoadingProgress()
} else if (this.showContent) {
  Content()
}

问题 2:内存占用过高

症状

  • 应用内存占用持续增长
  • 多次切换分支后内存泄漏

原因

  • 分支节点未正确销毁
  • TryRetain 保留的节点未释放

解决方案

方案 1:避免不必要的复用

// 如果不需要保留状态,让节点正常销毁
if (this.showPanel) {
  Panel() { ... }
  // 不设置 ID,不使用 TryRetake
}

方案 2:手动清理状态

@State showPanel: boolean = false

aboutToDisappear() {
  // 组件销毁时清理状态
  this.inputValue = ''
  this.scrollOffset = 0
}

动画问题

问题 1:动画不生效

症状

  • 分支切换时动画不播放

原因

  • 未设置 transitiongeometryTransition
  • 动画配置错误

解决方案

if (this.showContent) {
  Column() {
    Text('Content')
  }
  .transition({
    type: TransitionType.All,
    opacity: 0,
    translate: { x: 100 }
  })
  .animation({
    duration: 300,
    curve: Curve.EaseInOut
  })
}

问题 2:几何过渡不流畅

症状

  • geometryTransition 动画卡顿

原因

  • 分支内容差异过大
  • 包含复杂布局计算

解决方案

方案 1:简化分支内容

// 确保两个分支的结构相似
if (this.expanded) {
  Column() {
    Text('Title')
    Text('Description')
  }
  .height(200)
  .geometryTransition('panel')
} else {
  Column() {
    Text('Title')
    // 移除 Description,保持结构相似
  }
  .height(50)
  .geometryTransition('panel')
}

方案 2:禁用部分属性的动画

Column() {
  Text('Content')
}
.geometryTransition('panel')
.animation({
  duration: 300,
  curve: Curve.EaseInOut
})
// 避免 width/height 变化,只使用 opacity/transform

与 if-else 语法对比

ArkTS if-else 语法 vs JavaScript if-else

特性 ArkTS if-else JavaScript if-else
语法 声明式 UI 构造 语句级控制流
编译 编译时转换为组件树 运行时执行
状态管理 自动依赖 @State 变量 无状态管理
性能优化 智能差量更新、节点复用 每次重新执行
动画支持 内置 transitiongeometryTransition 需手动实现

与其他条件渲染方案对比

方案 1:if-else vs visibility

特性 if-else visibility
渲染方式 条件渲染(创建/销毁) 可见性切换
内存占用 低(不渲染时不占用) 高(始终占用)
适用场景 互斥的分支 频繁切换的内容
状态保留 通过 TryRetake 自动保留

if-else 适合

  • 互斥的分支(同一时间只显示一个)
  • 分支内容差异大
  • 需要释放内存

visibility 适合

  • 频繁切换的内容
  • 需要保留状态
  • 内容差异小

方案 2:if-else vs switch

ArkTS 没有直接的 switch 语法,但可以使用多层 if-elseif-else 模拟:

// 模拟 switch
if (this.value === 1) {
  Content1()
} else if (this.value === 2) {
  Content2()
} else if (this.value === 3) {
  Content3()
} else {
  DefaultContent()
}

性能对比

  • switch(如果存在):O(1) 跳转
  • if-elseif-else:O(n) 顺序判断

建议

  • 分支较少(< 5 个):使用 if-elseif-else
  • 分支较多(≥ 5 个):考虑使用 Map 或对象查找

附录

源码文件清单

核心层

文件 路径 说明
if_else_node.h frameworks/core/components_ng/syntax/if_else_node.h IfElseNode 头文件
if_else_node.cpp frameworks/core/components_ng/syntax/if_else_node.cpp IfElseNode 实现
if_else_model.h frameworks/core/components_ng/syntax/if_else_model.h Model 接口定义
if_else_model_ng.h frameworks/core/components_ng/syntax/if_else_model_ng.h NG Model 头文件
if_else_model_ng.cpp frameworks/core/components_ng/syntax/if_else_model_ng.cpp NG Model 实现

桥接层 - 声明式前端

文件 路径 说明
js_if_else.h frameworks/bridge/declarative_frontend/jsview/js_if_else.h JSIfElse 头文件
js_if_else.cpp frameworks/bridge/declarative_frontend/jsview/js_if_else.cpp JSIfElse 实现

桥接层 - CJ 前端

文件 路径 说明
cj_if_else_ffi.h frameworks/bridge/cj_frontend/interfaces/cj_ffi/cj_if_else_ffi.h CJ FFI 头文件
cj_if_else_ffi.cpp frameworks/bridge/cj_frontend/interfaces/cj_ffi/cj_if_else_ffi.cpp CJ FFI 实现

关键方法速查表

方法 位置 功能
SetBranchId() if_else_node.cpp:65-78 设置分支 ID,触发分支切换
FlushUpdateAndMarkDirty() if_else_node.cpp:80-96 刷新并标记脏状态
TryRetake() if_else_node.cpp:98-111 尝试复用之前分支的节点
CollectRetakenNodes() if_else_node.cpp:113-121 收集被复用的节点 ID
GetRetakenElmtIds() if_else_node.cpp:123-130 获取复用节点 ID 列表
Clean() ui_node.cpp 清理子节点(UINode 基类方法)
GetDisappearingChildById() ui_node.cpp 从消失队列获取节点
CollectCleanedChildren() ui_node.cpp 收集已清理的子节点

调试技巧

1. 启用 ACE_IF 日志标签

// 在 if_else_node.cpp 中已有日志
TAG_LOGD(AceLogTag::ACE_IF, "IfElse(%{public}d).SetBranchId branchIdChanged_: %{public}d",
    GetId(), branchIdChanged_);

如何查看日志

# 使用 hidumper 查看日志
hdc shell hidumper -s AceEditor -a -d

# 过滤 ACE_IF 标签
hdc shell "hilog | grep ACE_IF"

2. 使用 DevEco Studio 调试

断点位置

  • SetBranchId():查看分支切换逻辑
  • TryRetake():查看节点复用情况
  • FlushUpdateAndMarkDirty():查看刷新流程

调试信息

// 在 ArkTS 中添加日志
if (this.condition) {
  console.log('Branch 0 activated')
  Text('Branch 0')
}

3. 使用 Inspector 工具

# 查看 UI 树结构
hdc shell hidumper -s WindowManagerService -a -d

# 查看组件信息
hdc shell aa dump -a

性能分析

使用性能分析工具

// 使用 @Trace 装饰器追踪性能
import { Trace } from '@kit.PerformanceAnalysisKit'

@Entry
@Component
struct MyComponent {
  @State condition: boolean = false

  build() {
    Column() {
      if (this.condition) {
        // Trace.startTrace('branch0')
        ExpensiveContent()
        // Trace.stopTrace('branch0')
      }
    }
  }
}

性能指标

关键指标

  • 分支切换时间:目标 < 100ms
  • 内存占用:目标 < 10MB/分支
  • 动画帧率:目标 60 FPS

优化建议

  • 减少分支复杂度
  • 使用节点复用
  • 使用虚拟滚动

相关文档

  • 循环组件Loop_Components_Family.md - ForEach、LazyForEach 等
  • 状态管理../../bridge/declarative_frontend/state_mgmt/ - AppStorage、LocalStorage
  • 性能优化docs/best_practices/ - 性能优化指南
  • 动画系统docs/pattern/animation/ - 动画完整指南

参考资源


文档结束

如有疑问或需要补充,请参考源码或联系 ACE Engine 团队。