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@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         │
  │ (全局注册表)      │
  └─────────────────┘
  1. owningView_ 引用:属性知道"我属于哪个 ViewPU"

    • 构造函数中赋值 this.owningView_ = owningView
    • 用于属性变化时找到正确的 ViewPU 发起通知
  2. 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 子类(ObservedPropertyObjectPUSynchedPropertyTwoWayPUSynchedPropertyOneWayPU)有此方法。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();
}

这个调用实际做了三件事:

  1. 标记 View 为 dirty:在 Native 渲染树中标记该节点需要重新布局/绘制
  2. 注册 Vsync 回调:确保下一次 Vsync 信号到达时纳入渲染调度
  3. 返回不阻塞:调用立即返回,不等待实际渲染完成

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) 内部逻辑

  1. 查询元素:通过 elmtIdelementTree_ 中定位到具体元素节点
  2. 调用 build():对该节点执行完整 build() 函数,重新生成子树的虚拟节点
  3. 对比差异(Diff):新虚拟节点树与当前 Native 层树进行 Diff(Native 层完成)
  4. 提交变更: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() 只在首次添加脏元素时调用

设计原则

  1. 订阅即构建:依赖关系在首次 build() 时通过 getter 动态发现,一次性确定,后续不变
  2. 分离触发与执行:属性变化只做脏标记(同步),实际渲染在下一帧(异步)——避免重复渲染
  3. 按需最小更新:PU 路径通过 dependentElmtIdsByProperty_ 精确追踪元素→属性依赖,只重建受影响元素
  4. FU vs PU 双路径:简单 @State 走 FU(直接 ViewPU 重建),对象/链接属性走 PU(精确追踪依赖元素)
  5. 全局与局部隔离:@State 局限在组件内,@StorageLink 通过 AppStorage 全局共享,底层一致
posted @ 2026-05-25 17:45  getmoon  阅读(8)  评论(0)    收藏  举报