ArkUI 条件渲染组件完整指南
文档版本:v1.0 更新时间:2026-02-05 源码版本:OpenHarmony ace_engine (master 分支) 作者:基于 CLAUDE.md 规范自动生成
目录
概述
组件定位
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;
}
关键逻辑:
-
向上查找 FrameNode:
- 遍历父节点链,直到找到第一个
FrameNode IfElseNode本身是UINode,其父节点可能是另一个UINode或FrameNode
- 遍历父节点链,直到找到第一个
-
通知父容器子节点更新:
ChildrenUpdatedFrom(0):通知父容器从索引 0 开始的所有子节点都可能已更新- 这将触发父容器的布局算法重新计算
-
标记脏状态:
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. 触发布局和渲染更新 │
│ - 父容器重新测量和布局 │
│ - 渲染管道绘制新分支内容 │
└─────────────────────────────────────────────────────────┘
节点复用策略
复用条件
节点可以被复用需要满足以下条件:
-
节点 ID 匹配:
- 前端提供的 ID 必须与之前分支中某个节点的 ID 相同
- ID 通常由组件的
id属性或编译器生成的唯一标识符决定
-
节点仍在消失队列中:
- 节点不能已经被销毁
Clean()方法的cleanDirectly参数必须为false
-
节点来自正确的分支:
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
}
}
工作原理:
- 用户在输入框输入内容
showPanel变为false,输入框被移除但保留在消失队列showPanel变为true,前端调用TryRetake('myInput')- 输入框被重新添加到渲染树,内容保留
场景 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
})
}
}
}
工作流程:
- 初始状态:
isVisible = true,显示 "Content is visible" - 点击按钮:
isVisible = false - 触发重新渲染:
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)
}
}
}
}
工作原理:
- 用户输入 "John"
- 点击 "Hide Form",表单被移除
- 点击 "Show Form",表单重新显示
- 关键:输入框内容 "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:动画不生效
症状:
- 分支切换时动画不播放
原因:
- 未设置
transition或geometryTransition - 动画配置错误
解决方案:
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 变量 |
无状态管理 |
| 性能优化 | 智能差量更新、节点复用 | 每次重新执行 |
| 动画支持 | 内置 transition、geometryTransition |
需手动实现 |
与其他条件渲染方案对比
方案 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/- 动画完整指南
参考资源
- ArkTS 官方文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-conditional-rendering-V5
- OpenHarmony 源码:https://gitee.com/openharmony/arkui_ace_engine
- ACE Engine CLAUDE.md:
/mnt/data/w30045790/code/foundation/arkui/ace_engine/CLAUDE.md
文档结束
如有疑问或需要补充,请参考源码或联系 ACE Engine 团队。