ArkUI 装饰器属性变化触发 UI 更新机制
ArkUI 装饰器属性变化触发 UI 更新机制
文档版本: v1.4
更新时间: 2026-05-25
源码版本: OpenHarmony ace_engine (master 分支)
术语说明:
| 缩写 | 全称 | 说明 |
|---|---|---|
| PU | Partial Update | 部分更新路径——精确追踪哪些元素依赖哪个属性,只重建受影响元素 |
| FU | Full Update | 全量更新路径——属性变化时重建整个 View,适用于简单场景 |
| ObservedProperty | 可观察属性 | 装饰器生成的属性包装器,持有值 + 订阅者列表 |
| ViewPU | 视图(PU 路径) | 每个自定义组件对应的视图实例,管理脏元素和构建 |
一、概述
ArkUI 的响应式系统通过装饰器(@State、@Link、@Prop、@StorageLink 等)实现数据与 UI 的绑定。当装饰器属性变化时,系统自动触发 UI 更新。
一条完整的更新链路分为三个阶段:
订阅阶段(首次构建):
装饰器初始化 → 创建 ObservedProperty → 首次 build() 发现依赖 → 注册订阅关系
(组件首次渲染时建立 Property ↔ View 的绑定网络)
触发阶段(应用线程,同步):
属性赋值 → 订阅通知 → 脏元素标记 → markNeedUpdate
(发生在属性 setter 被调用的同一帧内)
更新阶段(渲染线程,异步):
Vsync 信号 → 遍历脏元素 → UpdateElement → 重新构建
(在下一帧 Vsync 到达时触发)
本文从源码角度拆解每个环节,核心逻辑集中在 ace_engine 的 state_mgmt 模块。
二、核心类层次与关系
2.1 ObservedPropertyAbstract 继承体系
ObservedPropertyAbstract(抽象基类:定义 set/notifyHasChanged/subscribers 接口)
├── ObservedPropertySimplePU<T> ← @State(简单类型:string/number/boolean)
├── SubscribedAbstractProperty(PU 路径:管理与 ViewPU 的订阅关系)
│ ├── SynchedPropertyTwoWayPU<T> ← @Link、@StorageLink(双向同步)
│ ├── SynchedPropertyOneWayPU<T> ← @Prop、@StorageProp(单向同步)
│ └── ObservedPropertyObjectPU<T> ← @State(对象/数组类型)
└── SynchedPropertyTwoWayTWPU<T> ← Twins 双向同步(跨页面/跨 Ability)
关键方法说明
基类 ObservedPropertyAbstract 定义接口,各子类实现或重写:
| 方法 | 定义位置 | 说明 |
|---|---|---|
get() |
各子类实现 | 返回值;PU 路径子类在 get() 中同时触发依赖追踪 |
set(newValue) |
各子类实现 | 比较新旧值 → 不同则更新 value_ + notifyHasChanged() |
notifyHasChanged(newValue) |
基类提供 | 遍历 subscribers_,调用 hasChanged() / propertyHasChanged() |
subscribers_ |
基类持有 | Set<number> 存储 SubscriberManager 分配的 ID |
SimplePU 与 ObjectPU 的核心差异
| 方面 | ObservedPropertySimplePU | ObservedPropertyObjectPU |
|---|---|---|
| 通知路径 | FU(直接通知 ViewPU hasChanged) | PU(通过 dependentElmtIdsByProperty_ 精确追踪依赖元素) |
| 比较方式 | 值比较(===) |
引用比较(===) |
| 继承自 | ObservedPropertyAbstract |
SubscribedAbstractProperty |
| 触发条件 | this.title = "new" |
this.obj = newObj(改内部属性不改引用→不触发) |
| 更新粒度 | View 整体重建 | 只重建依赖的特定元素 |
SimplePU 和 ObjectPU 共享同一个
notifyHasChanged()基类实现。差异在于 SimplePU 的订阅者通常是 ViewPU(走hasChanged()FU 路径),而 ObjectPU 的订阅者通过propertyHasChanged()进入notifyPropertyHasChangedPU()PU 路径。
继承树背后的设计意图
ObservedPropertyAbstract (基类)
│
├── SimplePU: 轻量路径。简单类型无需追踪元素依赖,直接通知 ViewPU
│ 整个重建——获取属性变化快,适用于高频简单数据。
│
└── SubscribedAbstractProperty (PU 路径)
│
├── ObjectPU: 对象类型需要追踪内部属性的哪些元素依赖它。
│ 只有 affected 元素重建,不碰其他元素。
│
├── TwoWayPU: @Link/@StorageLink 不仅需要通知自己所属 View,
│ 还要将变化同步回父组件/AppStorage——需要两条通知链。
│
└── OneWayPU: @Prop/@StorageProp 只读代理,单向同步。
修改被拦截,通过 source_ 关联父/AppStorage。
2.2 ViewPU 继承体系
ViewPU(partial_update/pu_view.ts)
└─ 提供脏元素管理、@Watch 回调、updateDirtyElements
└─ PUV2ViewBase(puv2_common/puv2_view_base.ts)
└─ 封装 NativeViewPartialUpdate.markNeedUpdate() 调用
└─ ViewBuildNodeBase(build 函数持有者)
各层职责:
| 层 | 位置 | 核心职责 |
|---|---|---|
ViewPU |
pu_view.ts |
脏元素集合管理、updateDirtyElements()、@Watch 回调分发、首帧构建 |
PUV2ViewBase |
puv2_view_base.ts |
桥接:将 ViewPU 的 markNeedUpdate() 委托给 Native 层 |
ViewBuildNodeBase |
构建函数持有 | 持有 build() 函数引用,重建时调用 |
ViewPU 核心数据结构
| 字段 | 类型 | 用途 | 在哪填充 |
|---|---|---|---|
dirtDescendantElementIds_ |
Set<number> |
需重绘的元素 ID 集合 | viewPropertyHasChanged() 中追加 |
dependentElmtIdsByProperty_ |
Map | 属性名 → 依赖元素 ID 列表 | 首次 build() 的 getter 依赖追踪中 |
watchedProps |
Map | @Watch 回调(属性名 → 回调函数) |
组件初始化时由装饰器注册 |
elementTree_ |
元素树 | 当前渲染的元素树(用于 UpdateElement 查询) | 每次 build() 后更新 |
isRenderInProgress |
boolean | 渲染中保护锁(禁止渲染中修改 @State) | startBuild() / endBuild() 管理 |
isFirstRender_ |
boolean | 首帧标志(首帧不走脏标记,直接 commit) | 生命周期 |
ViewPU 生命周期
组件创建
│
├── constructor: 初始化 ViewPU 基类,注册到 ComponentManager
│
├── initialize(): 创建 ObservedProperty 实例(@State/@Link 等)
│
├── firstBuild():
│ ├── startBuild() ← 设置 isRenderInProgress = true
│ ├── build() 执行 ← getter 触发依赖发现(见第四章)
│ ├── endBuild() ← 清除标志
│ └── commit 到 Native ← 首次完整渲染
│
├── [运行时: 多次触发/更新循环]
│ └── viewPropertyHasChanged() → markNeedUpdate()
│ └── Vsync → updateDirtyElements() → UpdateElement()
│
└── destroy(): 清理 subscriber 注册,释放元素树
2.3 AppStorage 与 LocalStorage
// app_storage.ts:496
private static getOrCreate(): AppStorage {
if (!AppStorage.instance_) {
AppStorage.instance_ = new AppStorage({});
}
return AppStorage.instance_;
}
懒单例:首次访问 AppStorage 任意静态方法时创建实例,全局唯一。
AppStorage 继承自 LocalStorage,底层存储结构和订阅机制一致。区别在于作用域:
| 类 | 作用域 | 创建方式 | 生命周期 |
|---|---|---|---|
AppStorage |
全局(整个进程) | 懒单例,首次访问时 | 进程生命周期 |
LocalStorage |
组件树/页面 | 显式 new LocalStorage() |
传入的组件树销毁时 |
API:
| API | 作用 |
|---|---|
AppStorage.Link(key) |
创建双向同步连接,@StorageLink 的底层实现 |
AppStorage.SetAndLink(key, value) |
设初值后创建连接 |
AppStorage.Set(key, value) |
直接设值 |
AppStorage.Get(key) |
读取值 |
2.4 SubscriberManager 全局订阅管理器
SubscriberManager 是 ArkUI state_mgmt 中的全局注册表,维护 subscriber ID → subscriber 对象的映射关系。
SubscriberManager (单例)
│
├── subscribers: Map<number, ISubscriber>
│
├── Subscribe(subscriber) → number ← 注册订阅者,返回 ID
├── Find(id) → ISubscriber ← 通过 ID 查找订阅者
└── Unsubscribe(id) ← 组件销毁时解注册
订阅者类型:
| 订阅者 | 实现接口 | 注册时机 |
|---|---|---|
| ViewPU | ISinglePropertyChangeSubscriber |
ViewPU 初始化时 |
| ObservedProperty (PU 路径) | IMultiPropertiesChangeSubscriber |
嵌套属性订阅时 |
| 其他 Property | 两者之一 | 父子组件属性绑定时 |
SubscriberManager 是整个通知链的路由中心——notifyHasChanged() 遍历的 subscribers_ 集合中存储的就是 SubscriberManager 分配的整数 ID,通过 Find(id) 定位到实际对象。
2.5 五种装饰器与跨组件状态管理
| 装饰器 | 作用域 | 数据源 | 写入方向 | 底层 Property | 使用场景 |
|---|---|---|---|---|---|
@State |
组件内部 | 自身 | 内部读写 | ObservedPropertySimplePU / ObjectPU |
普通组件状态 |
@Prop |
组件内部 | 父组件 | 父→子单向 | SynchedPropertyOneWayPU |
子组件只读展示 |
@Link |
组件内部 | 父组件 | 父子双向 | SynchedPropertyTwoWayPU |
子组件修改父状态 |
@StorageLink |
全局 | AppStorage | 双向同步 | SynchedPropertyTwoWayPU(代理到 AppStorage) |
跨组件/跨页面共享 |
@StorageProp |
全局 | AppStorage | App→组件单向 | SynchedPropertyOneWayPU(代理到 AppStorage) |
全局配置只读 |
@StorageLink 赋值机制
@StorageLink 与 @Link 底层使用同一个 SynchedPropertyTwoWayPU 类,区别在于数据源不同:@Link 绑定到父组件的 @State,@StorageLink 绑定到全局 AppStorage。
// 用户代码
@StorageLink("title") title: string = "default";
this.title = "new"; // 实际赋值给 SynchedPropertyTwoWayPU 代理
// SynchedPropertyTwoWayPU.set() 内部逻辑
set(newValue: T) {
// SynchedPropertyTwoWayPU 是纯代理,不自持 value_
if (this.source_.value_ !== newValue) {
this.source_.set(newValue); // 委托给 AppStorage 的 ObservedProperty
}
}
关键设计:SynchedPropertyTwoWayPU 是纯代理——不自持 value_,所有读写通过 source_ 委托给 AppStorage 内部的 ObservedProperty。this.source_.set(newValue) 触发 AppStorage 内部 ObservedProperty.notifyHasChanged(),通知扩散到所有监听同一 key 的 @StorageLink / @StorageProp 组件。
因此 @StorageLink 的触发链路与 @State 一致(set → notifyHasChanged → 脏标记 → markNeedUpdate),只是多了通过 AppStorage 跨组件广播这一步。
三、整体链路
订阅阶段、触发阶段、更新阶段是三个阶段的分述。这张全景图展示它们的完整衔接关系,可作为阅读后续章节的路线图。
┌──────────────────────────────────────────────────────────────────────────┐
│ 装饰器属性变化触发 UI 更新完整链路 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────── 订阅阶段(首次构建,一次性)───────────────────────┐ │
│ │ │ │
│ │ Component 创建 → 装饰器初始化创建 ObservedProperty │ │
│ │ → 首次 build() 执行 │ │
│ │ → getter 依赖发现(dependentElmtIdsByProperty_ 记录) │ │
│ │ → 订阅注册(subscribers_ + SubscriberManager) │ │
│ │ → 订阅关系就绪,等待属性变化 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (后续每次属性变化走 ↓) │
│ │
│ ┌───────────────────── 触发阶段(应用线程,同步)──────────────────────┐ │
│ │ │ │
│ │ 1. this.title = "new value" ← 用户代码 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 2. ObservedProperty.subclass.set(newValue) ← 新旧值比较 │ │
│ │ │ (若不同则更新 value_ + 触发通知) │ │
│ │ ▼ │ │
│ │ 3. notifyHasChanged(newValue) ← 遍历 subscribers_ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 4. SubscriberManager 查找订阅者 │ │
│ │ │ (订阅者是 ViewPU 或其他 Property) │ │
│ │ ├──► 5a. [FU路径] hasChanged(newValue) ← ISingleProperty │ │
│ │ │ (仅 SimplePU: 直接通知 ViewPU 整个重建) │ │
│ │ └──► 5b. [PU路径] propertyHasChanged() ← IMultiProperties │ │
│ │ │ (ObjectPU/Link/Prop: 通知依赖元素列表) │ │
│ │ ▼ │ │
│ │ 6. notifyPropertyHasChangedPU() ← PU 路径的订阅通知 │ │
│ │ (仅 SubscribedAbstractProperty 子类有此方法) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 7. owningView_.viewPropertyHasChanged() ← 通知拥有 View │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 8. dirtDescendantElementIds_.add(elmtId) ← 标记脏元素 │ │
│ │ │ (首次添加时触发 markNeedUpdate) │ │
│ │ ▼ │ │
│ │ 9. markNeedUpdate() ← 通知 Native 层"下一帧要更新" │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────── 更新阶段(渲染线程,异步)─────────────────────┐ │
│ │ │ │
│ │ 10. Vsync 信号到达 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 11. 调度 AceLayoutTaskGroup(布局任务)← 渲染引擎入口 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 12. updateDirtyElements() ← 遍历脏元素 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 13. UpdateElement(elmtId) ← 逐个重新构建元素 │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
编号对应后续章节:订阅阶段展开在"四、订阅阶段",Step 1-9 展开在"五、触发阶段",Step 10-13 展开在"六、更新阶段"。
四、订阅阶段:装饰器初始化与依赖注册
三段式链路的第一环:在属性变化发生之前,系统必须先建立 Property → ViewPU → Element 的绑定网络。
4.1 装饰器初始化:编译器生成的构造代码
声明 @State title: string = "old" 时,编译器在组件构造函数中展开为:
// 等效初始化代码(编译器生成)
this.__title = new ObservedPropertySimplePU<string>("old", this, /* PropertyInfo */);
源码定位:
| 属性类型 | 构造位置 | 说明 |
|---|---|---|
ObservedPropertySimplePU |
observed_property_simple_pu.ts |
@State 简单类型 |
ObservedPropertyObjectPU |
pu_observed_property_abstract.ts |
@State 对象/数组类型 |
SynchedPropertyTwoWayPU |
pu_observed_property_abstract.ts |
@Link / @StorageLink |
SynchedPropertyOneWayPU |
pu_observed_property_abstract.ts |
@Prop / @StorageProp |
ObservedPropertyAbstract 基类构造函数 |
observed_property_abstract.ts |
通用初始化逻辑 |
4.2 构造函数建立的两条链接
构件时(constructor):
ObservedProperty ViewPU
┌─────────────────┐ ┌──────────────────┐
│ owningView_ ──────────→ │ ViewPU 实例 │
│ value_ = "old" │ │ │
│ info_ │ │ elementTree_ │
│ subscribers_ [] │ │ dirtyElementIds │
└─────────────────┘ └──────────────────┘
│
│ SubscriberManager.Subscribe(id)
▼
┌─────────────────┐
│ Subscriber │
│ Manager │
│ (全局注册表) │
└─────────────────┘
-
owningView_ 引用:属性知道"我属于哪个 ViewPU"
- 构造函数中赋值
this.owningView_ = owningView - 用于属性变化时找到正确的 ViewPU 发起通知
- 构造函数中赋值
-
SubscriberManager 注册:属性获得一个订阅者列表
subscribers_初始为空(Set<number>),将在首次 build() 中被填充- 每个 subscriber 由 SubscriberManager 分配一个整数 ID
4.3 首次 build():依赖发现与订阅注册
订阅关系不是静态定义的——而是在 首次调用 build() 执行时通过 getter 动态发现:
// 组件首次渲染
build() {
Column() {
Text(this.title) // ← getter 被调用,触发依赖追踪
Button(this.count.toString()) // ← 另一个 @State 的 getter
}
}
执行流程:
① ViewPU.startBuild() ← pu_view.ts
│ 设置 isRenderInProgress = true
│
② Column 节点创建,分配 elmtId_1
│
③ Text(this.title) 执行
│
├──→ this.__title.get() 被调用 ← property getter
│ │
│ ├──→ 检测到 isRenderInProgress = true
│ ├──→ 记录依赖:elmtId_1 依赖属性 "title"
│ │ └── dependentElmtIdsByProperty_["title"].add(elmtId_1)
│ │
│ ├──→ 注册订阅:将 ViewPU 加入 subscribers_
│ │ └── subscribers_.add(subscriberIdOfViewPU)
│ │
│ └──→ 返回 value_("old")
│
└──→ Text 节点创建,关联 elmtId_1
│
④ Button(this.count.toString()) 执行
│ 重复上述流程,elmtId_2 依赖属性 "count"
│
⑤ ViewPU.endBuild() ← pu_view.ts
│ 清除 isRenderInProgress
│
⑥ 首次渲染提交:将元素树 commit 到 Native 层
依赖发现的核心:getter 检测到当前处于构建上下文中时,自动将当前正在构建的元素 ID 与属性关联。这类似于 Vue 的 track() 机制,但 ArkUI 在 getter 中内置此逻辑,不需要显式的 watchEffect。
4.4 订阅关系最终状态(就绪)
构建完成后:
dependentElmtIdsByProperty_
ObservedProperty "title" ──→ { elmtId_1, elmtId_3 }
│
│ subscribers_
│ ──→ [ViewPU(id=view_1)]
│
│ owningView_
│ ──→ ViewPU(id=view_1)
│
▼
当属性变化时:
this.title = "new"
→ set() → notifyHasChanged()
→ subscribers_.forEach(→ ViewPU.viewPropertyHasChanged())
→ view 通过 dependentElmtIds 知道哪些元素要更新
订阅是一次性的:subscribers_ 和 dependentElmtIdsByProperty_ 在首次 build() 后即固定。后续属性变化不会重新发现依赖——这是 ArkUI 的设计约束。
五、触发阶段:属性赋值 → 脏标记
当属性赋值发生时,系统走完从 ObservedProperty.set() 到 markNeedUpdate() 的同步路径。本阶段全部在应用线程完成,不阻塞渲染线程。
Step 1: 用户代码属性赋值
// ArkTS 代码
@State title: string = "old";
this.title = "new"; // 触发 @State 生成的 setter
setter 由装饰器编译器生成,内部调用对应子类的 set() 方法(ObservedPropertySimplePU.set() 或 ObservedPropertyObjectPU.set())。
Step 2: 子类 set() — 新旧值比较
// observed_property_simple_pu.ts(简单类型实现)
set(newValue: T) {
if (this.value_ === newValue) return; // 值相等→跳过,避免死循环
this.value_ = newValue;
this.notifyHasChanged(newValue); // 值变了→触发通知
}
ObservedPropertyObjectPU 对对象类型做引用比较(===),不进行深比较。修改对象内部属性不改引用,不会触发更新——这是 ArkTS 的显式约束。
Step 3-5: notifyHasChanged() — 遍历订阅者
// observed_property_abstract.ts:138-156
protected notifyHasChanged(newValue: T) {
stateMgmtProfiler.begin('ObservedPropertyAbstract.notifyHasChanged');
this.subscribers_?.forEach((subscribedId) => {
let subscriber = SubscriberManager.Find(subscribedId);
if (subscriber) {
// FU 路径:订阅者是 ViewPU 时走此分支(常见于 SimplePU)
if ('hasChanged' in subscriber) {
(subscriber as ISinglePropertyChangeSubscriber<T>).hasChanged(newValue);
}
// PU 路径:订阅者是另一个 PU Property 时走此分支(嵌套属性)
if ('propertyHasChanged' in subscriber) {
(subscriber as IMultiPropertiesChangeSubscriber).propertyHasChanged(this.info_);
}
}
});
stateMgmtProfiler.end();
}
一个订阅者可以同时实现两个接口,FU 和 PU 路径不是互斥的。
两种订阅接口的区别:
| 接口 | 通知内容 | 实现者 | 使用场景 |
|---|---|---|---|
ISinglePropertyChangeSubscriber |
传递新值 | ViewPU(FU 路径) | 属性直接绑定到视图 |
IMultiPropertiesChangeSubscriber |
只传 PropertyInfo |
PU ObservedProperty | 属性嵌套链,只需知道"哪个属性变了" |
Step 6: notifyPropertyHasChangedPU() — PU 路径通知
仅
SubscribedAbstractProperty子类(ObservedPropertyObjectPU、SynchedPropertyTwoWayPU、SynchedPropertyOneWayPU)有此方法。ObservedPropertySimplePU不走此路径。
// pu_observed_property_abstract.ts:343-373
protected notifyPropertyHasChangedPU(isSync: boolean = false): void {
if (this.owningView_) {
if (this.delayedNotification_ === ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) {
if (!isSync) {
this.owningView_.viewPropertyHasChanged(
this.info_,
this.dependentElmtIdsByProperty_.getAllPropertyDependencies()
);
} else {
this.owningView_.collectElementsNeedToUpdateSynchronously(...);
}
} else {
this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending;
}
}
}
三种通知模式:
| 模式 | 触发条件 | 行为 |
|---|---|---|
| 立即模式(默认) | do_not_delay + isSync=false |
同步走完全程:set → viewPropertyHasChanged → 脏标记 |
| 延迟模式 | delay_notification_pending |
只标记 pending,后续统一消费,用于批量优化 |
| 同步收集模式 | do_not_delay + isSync=true |
同步收集所有受影响元素,用于 @Watch 等场景 |
Step 7-8: viewPropertyHasChanged() — 脏元素标记
// pu_view.ts:585-635
viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number> | undefined): void {
if (this.isRenderInProgress) {
return;
}
if (dependentElmtIds?.size && !this.isFirstRender()) {
if (!this.dirtDescendantElementIds_.size && !this.runReuse_) {
this.markNeedUpdate(); // 首次添加脏元素时触发
}
for (const elmtId of dependentElmtIds) {
this.dirtDescendantElementIds_.add(elmtId);
}
}
let cb = this.watchedProps.get(varName);
if (cb && typeof cb === 'function') {
cb.call(this, varName);
}
}
关键细节:markNeedUpdate() 只在首次添加脏元素时调用一次。后续添加只操作集合大小,不重复通知 Native 层——一次 Vsync 周期内多次属性变化只需一次渲染。
Step 9: markNeedUpdate() — 通知渲染引擎
// puv2_view_base.ts:208-210
public markNeedUpdate(): void {
return this.nativeViewPartialUpdate.markNeedUpdate();
}
这个调用实际做了三件事:
- 标记 View 为 dirty:在 Native 渲染树中标记该节点需要重新布局/绘制
- 注册 Vsync 回调:确保下一次 Vsync 信号到达时纳入渲染调度
- 返回不阻塞:调用立即返回,不等待实际渲染完成
markNeedUpdate()只负责"注册"需要更新的事实,具体更新由下一帧 Vsync 驱动。
六、更新阶段:Vsync → 元素重建
上一章描述了属性变化如何标记脏元素并通知 Native 层的 markNeedUpdate()。实际 UI 重建由渲染线程在下一帧 Vsync 到达时驱动。
Vsync 驱动的帧同步机制
┌─────────────────────────────────────────────────────────┐
│ 渲染帧循环 │
├─────────────────────────────────────────────────────────┤
│ │
│ 帧 N(应用线程): │
│ └─ 用户修改 @State 属性 │
│ └─ Step 1-9: 属性变化加入 dirtDescendantElementIds_ │
│ └─ markNeedUpdate() → register to Vsync │
│ │
│ ─────────────────── Vsync 间隔 ──────────────────────── │
│ │
│ 帧 N+1(渲染线程): │
│ └─ Vsync 信号到达 │
│ └─ 渲染调度引擎拉起 AceLayoutTaskGroup │
│ └─ 执行 updateDirtyElements() │
│ └─ 遍历脏元素逐个 UpdateElement │
│ └─ 触发完整 rebuild │
│ │
└─────────────────────────────────────────────────────────┘
AceLayoutTaskGroup 是渲染调度引擎中的任务组。当 markNeedUpdate() 注册完毕后,渲染引擎在下一帧 Vsync 到达时通过此任务组遍历所有 dirty View,执行布局和绘制。
脏元素遍历:updateDirtyElements()
// pu_view.ts:854-884
updateDirtyElements(dirtRetakenElementIds?: Set<number>): void {
const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_)
.sort(ViewPU.compareNumber);
for (const elmtId of dirtElmtIdsFromRootNode) {
this.dirtDescendantElementIds_.delete(elmtId);
this.purgeDeletedElmtIds();
this.UpdateElement(elmtId);
}
if (this.dirtDescendantElementIds_.size) {
this.dirtDescendantElementIds_.add(dirtRetakenElementId);
}
}
UpdateElement(elmtId) 内部逻辑:
- 查询元素:通过
elmtId在elementTree_中定位到具体元素节点 - 调用 build():对该节点执行完整
build()函数,重新生成子树的虚拟节点 - 对比差异(Diff):新虚拟节点树与当前 Native 层树进行 Diff(Native 层完成)
- 提交变更:Diff 结果应用到 Native 渲染树(布局→绘制)
完整时序图
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌─────────────┐
│ 用户代码 │ │ ObservedProperty│ │ ViewPU │ │ Native │
│ this.title │ │ │ │ │ │ Renderer │
└──────┬───────┘ └────────┬────────┘ └──────┬───────┘ └──────┬──────┘
│ │ │ │
│ set("new") │ │ │
│──────────────────────►│ │ │
│ │ 内部: notifyHasChanged() │
│ │──┐ │ │
│ │ │ forEach subscribers │ │
│ │◄─┘ │ │
│ │ │ │
│ │ hasChanged() / │ │
│ │ notifyPropertyHasChangedPU() │
│ │──────────────────────►│ │
│ │ │ │
│ │ viewPropertyHasChanged() │
│ │ │──┐ │
│ │ │ │ add to dirty set │
│ │ │◄─┘ │
│ │ │ │
│ │ markNeedUpdate() │ │
│ │ │─────────────────────►│
│ │ │ │
│ │ │ [返回,不阻塞] │
│ │ │◄─────────────────────│
│ │ │ │
│ [用户代码继续执行] │ │ │
│ │ │ │
│ ───── Vsync 间隔 ────│───────────────────────│──────────────────────│
│ │ │ │
│ │ │ Vsync 信号到达 │
│ │ │◄─────────────────────│
│ │ │ AceLayoutTaskGroup │
│ │ │ │
│ │ updateDirtyElements()│ │
│ │ │──┐ │
│ │ │ │ UpdateElement() │
│ │ │ │ → build() 重建 │
│ │ │◄─┘ │
│ │ │ │
七、关键文件索引
| 功能 | 文件路径 |
|---|---|
| ObservedProperty 基类(定义 set/notifyHasChanged 接口) | state_mgmt/src/lib/common/observed_property_abstract.ts |
| SubscriberManager(全局订阅管理器) | state_mgmt/src/lib/common/subscriber_manager.ts |
| PU 路径 ObservedProperty(ObjectPU/Link/Prop) | state_mgmt/src/lib/partial_update/pu_observed_property_abstract.ts |
| @State 简单类型实现(SimplePU) | state_mgmt/src/lib/partial_update/observed_property_simple_pu.ts |
| ViewPU 基类(脏元素管理、updateDirtyElements) | state_mgmt/src/lib/partial_update/pu_view.ts |
| PUV2ViewBase(Native 桥接层) | state_mgmt/src/lib/puv2_common/puv2_view_base.ts |
| AppStorage(全局状态单例) | state_mgmt/src/lib/sdk/app_storage.ts |
| LocalStorage(AppStorage 父类) | state_mgmt/src/lib/sdk/local_storage.ts |
八、总结
核心链路一句话
组件首次 build() 建立订阅 → 属性变化触发订阅通知 → ViewPU 标记脏元素 → markNeedUpdate
→ Vsync 到达 → updateDirtyElements → UpdateElement 逐个重建
关键问题速查
| 问题 | 答案 |
|---|---|
| 订阅关系何时建立?如何建立? | 首次 build() 时,getter 检测 isRenderInProgress,自动注册 subscribers_ + 记录 dependentElmtIdsByProperty_。一次性固定 |
| @State 变化如何触发 UI 更新? | set() → notifyHasChanged() → 遍历 subscribers_ → hasChanged() / propertyHasChanged() → viewPropertyHasChanged() → dirtDescendantElementIds_.add() → markNeedUpdate() |
| 实际渲染何时执行? | 异步。下一帧 Vsync 到达时,渲染引擎通过 AceLayoutTaskGroup 执行 updateDirtyElements() |
| 脏元素是什么? | ViewPU.dirtDescendantElementIds_ 集合,需重新构建的元素 ID |
markNeedUpdate() 做了什么? |
通知 Native 注册 Vsync 回调,标记 View 为 dirty——渲染在下一帧异步执行 |
UpdateElement() 如何工作? |
通过 elmtId 定位节点后触发 build(),Diff 新旧虚拟树,应用到 Native 渲染树 |
| SimplePU 和 ObjectPU 的区别? | SimplePU 值比较→FU 路径整重建;ObjectPU 引用比较→PU 路径精确追踪 |
| @StorageLink 和 @State 的区别? | @State 组件内部;@StorageLink 通过 SynchedPropertyTwoWayPU 纯代理同步到 AppStorage |
| 一次 Vsync 内多次改属性,渲染几次? | 一次。markNeedUpdate() 只在首次添加脏元素时调用 |
设计原则
- 订阅即构建:依赖关系在首次 build() 时通过 getter 动态发现,一次性确定,后续不变
- 分离触发与执行:属性变化只做脏标记(同步),实际渲染在下一帧(异步)——避免重复渲染
- 按需最小更新:PU 路径通过
dependentElmtIdsByProperty_精确追踪元素→属性依赖,只重建受影响元素 - FU vs PU 双路径:简单 @State 走 FU(直接 ViewPU 重建),对象/链接属性走 PU(精确追踪依赖元素)
- 全局与局部隔离:@State 局限在组件内,@StorageLink 通过 AppStorage 全局共享,底层一致

浙公网安备 33010602011771号