程序员Feri | 14年编程老炮,拆解技术脉络,记录程序员的进化史


Hello,我是程序员Feri。

今天咱们聊一个让无数鸿蒙开发者又爱又恨的话题——状态管理

说实话,当我第一次接触 HarmonyOS 的 V1 状态管理时,内心是崩溃的。改了数据,UI 不更新?嵌套对象改了,界面没反应?数组 push 了,列表不刷新?这些问题让我一度怀疑人生。

但当我深入研究了 HarmonyOS NEXT(API 12+)推出的 V2 状态管理后,我只想说:华为这次真的懂开发者了

这篇文章,我会用最通俗的语言,带你彻底搞懂 V2 状态管理的设计哲学、核心原理和实战技巧。看完之后,你会发现状态管理其实可以很简单。


一、开篇:状态管理为什么这么重要?

1.1 一个扎心的场景

假设你在开发一个购物车页面:

// 你的商品数据结构
class CartItem {
  name: string = '';
  price: number = 0;
  quantity: number = 1;
}

@Entry
@Component
struct CartPage {
  @State cartItems: CartItem[] = [];
  
  build() {
    Column() {
      ForEach(this.cartItems, (item: CartItem) => {
        Row() {
          Text(item.name)
          Text(`¥${item.price}`)
          Text(`x${item.quantity}`)
          Button('+').onClick(() => {
            item.quantity++;  // 期望:数量+1,UI更新
          })
        }
      })
    }
  }
}

你信心满满地点击 "+" 按钮,然后...

界面纹丝不动。

数据确实改了(你可以 console.log 验证),但 UI 就是不更新。这就是 V1 状态管理的经典"坑"。

1.2 V1 时代的三座大山

在 V2 出现之前,开发者面临三个核心痛点:

痛点 具体表现 开发者的心理阴影
浅层观测 只能观测对象的第一层属性变化 "为什么改了不更新?"
数组操作受限 直接修改数组元素不触发更新 "难道要每次都新建数组?"
跨组件通信繁琐 @Link、@Provide/@Consume 使用复杂 "我只是想传个数据..."

1.3 V2 状态管理的设计目标

华为在 HarmonyOS NEXT 中重新设计了状态管理系统,核心目标是:

┌────────────────────────────────────────────────────────┐
│                 V2 状态管理设计目标                      │
├────────────────────────────────────────────────────────┤
│  1. 深度响应式:嵌套对象、数组元素都能被观测             │
│  2. 精准更新:只更新真正变化的部分,性能更优             │
│  3. 简化心智:减少装饰器数量,降低学习成本               │
│  4. 类型安全:更好的 TypeScript 支持                    │
└────────────────────────────────────────────────────────┘

二、V2 核心装饰器全景图

在深入讲解之前,先给你一张全景图,让你对 V2 的装饰器体系有个整体认知:

┌─────────────────────────────────────────────────────────────┐
│                    V2 状态管理装饰器体系                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   【类装饰器】                                               │
│   ┌─────────────────┐                                       │
│   │  @ObservedV2    │  ← 让类的属性变得可观测                │
│   └─────────────────┘                                       │
│                                                             │
│   【属性装饰器 - 类内部】                                     │
│   ┌─────────────────┐                                       │
│   │     @Trace      │  ← 标记需要被追踪的属性                │
│   └─────────────────┘                                       │
│                                                             │
│   【属性装饰器 - 组件内部】                                   │
│   ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐              │
│   │ @Local │ │ @Param │ │ @Event │ │ @Once  │              │
│   └────────┘ └────────┘ └────────┘ └────────┘              │
│      ↓           ↓           ↓           ↓                  │
│   组件私有    外部传入    事件回调    一次性初始化            │
│                                                             │
│   【跨组件通信】                                             │
│   ┌─────────────────┐  ┌─────────────────┐                 │
│   │    @Provider    │  │    @Consumer    │                 │
│   └─────────────────┘  └─────────────────┘                 │
│          ↓                      ↓                           │
│      提供数据                 消费数据                       │
│                                                             │
│   【计算与监听】                                             │
│   ┌─────────────────┐  ┌─────────────────┐                 │
│   │    @Computed    │  │    @Monitor     │                 │
│   └─────────────────┘  └─────────────────┘                 │
│          ↓                      ↓                           │
│      计算属性                状态监听                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

接下来,我们逐一拆解每个装饰器。


三、@ObservedV2 与 @Trace:深度响应式的基石

3.1 为什么需要 @ObservedV2?

在 V1 中,我们用 @Observed 装饰类,但它有一个致命问题:只能观测直接属性的赋值操作

// V1 的问题演示
@Observed
class User {
  name: string = '';
  address: Address = new Address();  // 嵌套对象
}

@Observed
class Address {
  city: string = '';
}

// 在组件中
@State user: User = new User();

// ✅ 这个会触发更新
this.user.name = '张三';

// ❌ 这个不会触发更新!
this.user.address.city = '深圳';

V2 的解决方案:@ObservedV2 + @Trace

@ObservedV2
class User {
  @Trace name: string = '';
  @Trace address: Address = new Address();
}

@ObservedV2
class Address {
  @Trace city: string = '';
}

// 现在这两个都会触发更新!
this.user.name = '张三';        // ✅
this.user.address.city = '深圳'; // ✅

3.2 @Trace 的工作原理

@Trace 的本质是在属性上建立依赖追踪。当你在 UI 中使用了某个 @Trace 属性,框架会:

  1. 收集依赖:记录"这个 UI 片段依赖这个属性"

  2. 触发更新:当属性变化时,只更新依赖它的 UI 片段

┌─────────────────────────────────────────────────────────────┐
│                     @Trace 工作流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 渲染阶段(依赖收集)                                    │
│   ┌──────────┐      读取属性      ┌──────────────┐          │
│   │   UI     │  ─────────────►   │  @Trace 属性  │          │
│   │  组件    │                   │   (user.name) │          │
│   └──────────┘      记录依赖      └──────────────┘          │
│        ▲              │                  │                  │
│        │              ▼                  │                  │
│        │     ┌──────────────┐            │                  │
│        │     │   依赖关系表  │            │                  │
│        │     │ UI片段→属性   │            │                  │
│        │     └──────────────┘            │                  │
│        │                                 │                  │
│   2. 更新阶段(精准通知)                                    │
│        │      属性变化         ┌─────────┘                  │
│        │         │            │                             │
│        │         ▼            ▼                             │
│        │     ┌──────────────────┐                           │
│        │     │   查找依赖此属性  │                           │
│        └─────│   的所有 UI 片段  │                           │
│    触发更新   └──────────────────┘                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.3 实战:购物车的正确打开方式

让我们用 V2 重写开头的购物车案例:

// ==================== 数据模型定义 ====================
@ObservedV2
class CartItem {
  @Trace id: number = 0;
  @Trace name: string = '';
  @Trace price: number = 0;
  @Trace quantity: number = 1;
  
  constructor(id: number, name: string, price: number) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
  
  // 计算单项总价
  get totalPrice(): number {
    return this.price * this.quantity;
  }
}

@ObservedV2
class ShoppingCart {
  @Trace items: CartItem[] = [];
  
  // 添加商品
  addItem(item: CartItem): void {
    this.items.push(item);
  }
  
  // 计算总价
  get totalAmount(): number {
    return this.items.reduce((sum, item) => sum + item.totalPrice, 0);
  }
}

// ==================== UI 组件 ====================
@Entry
@ComponentV2
struct CartPage {
  // 使用 @Local 定义组件内部状态
  @Local cart: ShoppingCart = new ShoppingCart();
  
  aboutToAppear(): void {
    // 初始化一些测试数据
    this.cart.addItem(new CartItem(1, 'iPhone 15', 7999));
    this.cart.addItem(new CartItem(2, 'AirPods Pro', 1999));
    this.cart.addItem(new CartItem(3, 'MacBook Air', 9999));
  }
  
  build() {
    Column({ space: 16 }) {
      // 标题
      Text('我的购物车')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 10 })
      
      // 商品列表
      List({ space: 12 }) {
        ForEach(this.cart.items, (item: CartItem) => {
          ListItem() {
            CartItemCard({ item: item })
          }
        }, (item: CartItem) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      
      // 底部结算栏
      Row() {
        Text(`共 ${this.cart.items.length} 件商品`)
          .fontSize(14)
          .fontColor('#666')
        
        Blank()
        
        Text(`合计: ¥${this.cart.totalAmount}`)
          .fontSize(18)
          .fontColor('#FF6600')
          .fontWeight(FontWeight.Bold)
        
        Button('去结算')
          .margin({ left: 16 })
          .backgroundColor('#FF6600')
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFF')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

// 商品卡片组件
@ComponentV2
struct CartItemCard {
  @Param @Require item: CartItem = new CartItem(0, '', 0);
  
  build() {
    Row({ space: 12 }) {
      // 商品图片占位
      Column()
        .width(80)
        .height(80)
        .backgroundColor('#EEEEEE')
        .borderRadius(8)
      
      // 商品信息
      Column({ space: 8 }) {
        Text(this.item.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
        
        Text(`¥${this.item.price}`)
          .fontSize(14)
          .fontColor('#FF6600')
        
        // 数量控制
        Row({ space: 12 }) {
          Button('-')
            .width(32)
            .height(32)
            .fontSize(18)
            .onClick(() => {
              if (this.item.quantity > 1) {
                this.item.quantity--;  // ✅ 直接修改,UI 自动更新!
              }
            })
          
          Text(`${this.item.quantity}`)
            .fontSize(16)
            .width(40)
            .textAlign(TextAlign.Center)
          
          Button('+')
            .width(32)
            .height(32)
            .fontSize(18)
            .onClick(() => {
              this.item.quantity++;  // ✅ 直接修改,UI 自动更新!
            })
        }
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      
      // 小计
      Text(`¥${this.item.totalPrice}`)
        .fontSize(16)
        .fontColor('#333')
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }
}

关键点解析:

  1. CartItemShoppingCart 都用 @ObservedV2 装饰

  2. 所有需要触发 UI 更新的属性都用 @Trace 标记

  3. 直接修改 item.quantity++,UI 就会自动更新

  4. 嵌套访问 this.cart.items[i].quantity 也能正确触发更新


四、@Local、@Param、@Event:组件通信三剑客

4.1 @Local:组件的私有领地

@Local 用于定义组件内部的私有状态,相当于 V1 中的 @State,但语义更清晰。

@ComponentV2
struct Counter {
  // 组件私有状态,外部无法直接修改
  @Local count: number = 0;
  
  build() {
    Row({ space: 20 }) {
      Button('-').onClick(() => this.count--)
      Text(`${this.count}`).fontSize(24)
      Button('+').onClick(() => this.count++)
    }
  }
}

@Local 的特点:

特性 说明
私有性 只能在组件内部修改
响应式 变化会触发 UI 更新
可初始化 可以设置默认值
不可外传 父组件无法传值覆盖

4.2 @Param:单向数据流的优雅实现

@Param 用于接收父组件传递的数据,类似 V1 的 @Prop,但有重要区别:

// 父组件
@ComponentV2
struct ParentComponent {
  @Local userName: string = '程序员Feri';
  
  build() {
    Column() {
      // 通过属性传递数据给子组件
      ChildComponent({ name: this.userName })
      
      Button('修改名字').onClick(() => {
        this.userName = '14年老炮';
      })
    }
  }
}

// 子组件
@ComponentV2
struct ChildComponent {
  // @Require 表示这是必传属性
  @Param @Require name: string = '';
  
  build() {
    Text(`Hello, ${this.name}!`)
      .fontSize(20)
  }
}

@Param 的核心规则:

┌─────────────────────────────────────────────────────────────┐
│                    @Param 数据流向                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│    父组件                          子组件                    │
│   ┌──────────┐                  ┌──────────┐               │
│   │  @Local  │ ───── 传递 ────► │  @Param  │               │
│   │   data   │                  │   data   │               │
│   └──────────┘                  └──────────┘               │
│        │                              │                     │
│        │ 可修改                       │ 只读                 │
│        ▼                              ▼                     │
│   ┌──────────┐                  ┌──────────┐               │
│   │  修改后   │ ───── 同步 ────► │  自动更新 │               │
│   │   data   │                  │   data   │               │
│   └──────────┘                  └──────────┘               │
│                                                             │
│   ❌ 子组件不能直接修改 @Param 属性                          │
│   ✅ 父组件修改后,子组件自动同步                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4.3 @Event:子传父的标准姿势

当子组件需要通知父组件"发生了某事"时,使用 @Event

// 子组件:一个可编辑的输入框
@ComponentV2
struct EditableInput {
  @Param value: string = '';
  
  // 定义事件回调,父组件可以监听
  @Event onValueChange: (newValue: string) => void = () => {};
  @Event onSubmit: (value: string) => void = () => {};
  
  build() {
    Row({ space: 12 }) {
      TextInput({ text: this.value })
        .onChange((newValue: string) => {
          // 通知父组件值发生了变化
          this.onValueChange(newValue);
        })
        .onSubmit(() => {
          this.onSubmit(this.value);
        })
        .layoutWeight(1)
      
      Button('提交')
        .onClick(() => {
          this.onSubmit(this.value);
        })
    }
  }
}

// 父组件
@ComponentV2
struct FormPage {
  @Local inputValue: string = '';
  @Local submittedValue: string = '';
  
  build() {
    Column({ space: 20 }) {
      EditableInput({
        value: this.inputValue,
        onValueChange: (newValue: string) => {
          this.inputValue = newValue;  // 同步更新
        },
        onSubmit: (value: string) => {
          this.submittedValue = value;
          console.info(`提交的值: ${value}`);
        }
      })
      
      Text(`当前输入: ${this.inputValue}`)
      Text(`已提交: ${this.submittedValue}`)
    }
    .padding(20)
  }
}

4.4 @Once:一次性初始化

有时候,你只想在组件创建时接收一个初始值,之后父组件的变化不再影响子组件:

@ComponentV2
struct TimerDisplay {
  // 只在初始化时接收值,之后父组件修改不会影响
  @Once @Param initialSeconds: number = 0;
  
  // 组件内部独立维护的倒计时
  @Local remainingSeconds: number = 0;
  
  aboutToAppear(): void {
    this.remainingSeconds = this.initialSeconds;
    this.startCountdown();
  }
  
  private startCountdown(): void {
    const timer = setInterval(() => {
      if (this.remainingSeconds > 0) {
        this.remainingSeconds--;
      } else {
        clearInterval(timer);
      }
    }, 1000);
  }
  
  build() {
    Text(`剩余时间: ${this.remainingSeconds}s`)
      .fontSize(32)
  }
}

// 使用
@ComponentV2
struct GamePage {
  @Local gameTime: number = 60;
  
  build() {
    Column() {
      // 即使 gameTime 变化,TimerDisplay 内部的倒计时也不受影响
      TimerDisplay({ initialSeconds: this.gameTime })
      
      Button('重置时间(不影响已启动的计时器)')
        .onClick(() => {
          this.gameTime = 120;
        })
    }
  }
}

五、@Provider 与 @Consumer:跨层级状态共享

5.1 为什么需要跨层级通信?

看这个场景:

App (主题色)
  └── HomePage
        └── ProductList
              └── ProductCard
                    └── PriceTag (需要主题色)

如果用 @Param 一层层传递,代码会变成"参数地狱"。

5.2 @Provider/@Consumer 的使用

// ==================== 顶层组件:提供主题 ====================
@ComponentV2
struct App {
  // @Provider 声明要共享的状态
  // aliasName 是可选的别名,用于 Consumer 查找
  @Provider('theme') themeColor: string = '#007AFF';
  @Provider('user') currentUser: UserInfo = new UserInfo();
  
  build() {
    Column() {
      // 主题切换按钮
      Row({ space: 12 }) {
        Button('蓝色主题').onClick(() => this.themeColor = '#007AFF')
        Button('绿色主题').onClick(() => this.themeColor = '#34C759')
        Button('橙色主题').onClick(() => this.themeColor = '#FF9500')
      }
      
      // 子组件树,无论嵌套多深都能访问主题
      HomePage()
    }
  }
}

// ==================== 中间层组件:不需要关心主题 ====================
@ComponentV2
struct HomePage {
  build() {
    Column() {
      Text('首页')
      ProductList()  // 不需要传递 theme
    }
  }
}

@ComponentV2
struct ProductList {
  build() {
    List() {
      ForEach([1, 2, 3], (id: number) => {
        ListItem() {
          ProductCard()  // 不需要传递 theme
        }
      })
    }
  }
}

// ==================== 深层组件:消费主题 ====================
@ComponentV2
struct ProductCard {
  // @Consumer 从祖先组件获取共享状态
  @Consumer('theme') themeColor: string = '#000000';
  
  build() {
    Column() {
      Text('商品名称')
        .fontColor(this.themeColor)  // 使用主题色
      
      PriceTag()
    }
    .border({ width: 2, color: this.themeColor })
  }
}

@ComponentV2
struct PriceTag {
  @Consumer('theme') themeColor: string = '#000000';
  
  build() {
    Text('¥999')
      .fontSize(18)
      .fontColor(this.themeColor)
      .fontWeight(FontWeight.Bold)
  }
}

5.3 Provider/Consumer 的工作原理

┌─────────────────────────────────────────────────────────────┐
│              @Provider / @Consumer 查找机制                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                    App (@Provider('theme'))                 │
│                           │                                 │
│                           │ 注册: theme → '#007AFF'         │
│                           ▼                                 │
│                    ┌─────────────┐                          │
│                    │  上下文容器   │                          │
│                    │ (Context)   │                          │
│                    └─────────────┘                          │
│                           │                                 │
│         ┌─────────────────┼─────────────────┐               │
│         ▼                 ▼                 ▼               │
│     HomePage          Settings          Profile             │
│         │                                                   │
│         ▼                                                   │
│    ProductList                                              │
│         │                                                   │
│         ▼                                                   │
│    ProductCard (@Consumer('theme'))                         │
│         │                                                   │
│         │ 向上查找 'theme'                                   │
│         │ 找到 → 返回 '#007AFF'                              │
│         │ 未找到 → 使用默认值                                 │
│         ▼                                                   │
│      PriceTag (@Consumer('theme'))                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5.4 双向绑定:Provider + Consumer + Trace

一个高级用法——实现跨组件的双向数据同步:

@ObservedV2
class GlobalState {
  @Trace counter: number = 0;
  @Trace userName: string = 'Guest';
}

@ComponentV2
struct App {
  @Provider('globalState') state: GlobalState = new GlobalState();
  
  build() {
    Column({ space: 20 }) {
      Text(`顶层显示: ${this.state.counter}`)
      
      DeepChildComponent()
      
      Button('顶层+1').onClick(() => {
        this.state.counter++;
      })
    }
  }
}

@ComponentV2
struct DeepChildComponent {
  @Consumer('globalState') state: GlobalState = new GlobalState();
  
  build() {
    Column({ space: 12 }) {
      Text(`深层显示: ${this.state.counter}`)
      
      Button('深层+1').onClick(() => {
        // 深层组件修改,顶层也会同步更新!
        this.state.counter++;
      })
    }
  }
}

六、@Computed 与 @Monitor:响应式进阶

6.1 @Computed:高效的计算属性

当你需要基于其他状态计算出一个派生值时,使用 @Computed

@ObservedV2
class ShoppingCart {
  @Trace items: CartItem[] = [];
  @Trace discountRate: number = 1.0;  // 折扣率
}

@ComponentV2
struct CartSummary {
  @Local cart: ShoppingCart = new ShoppingCart();
  
  // 计算属性:商品总价
  @Computed
  get subtotal(): number {
    return this.cart.items.reduce((sum, item) => {
      return sum + item.price * item.quantity;
    }, 0);
  }
  
  // 计算属性:折后价格
  @Computed
  get finalPrice(): number {
    return this.subtotal * this.cart.discountRate;
  }
  
  // 计算属性:节省金额
  @Computed
  get savedAmount(): number {
    return this.subtotal - this.finalPrice;
  }
  
  build() {
    Column({ space: 12 }) {
      Text(`商品总价: ¥${this.subtotal.toFixed(2)}`)
      Text(`折扣: ${((1 - this.cart.discountRate) * 100).toFixed(0)}% OFF`)
      Text(`节省: ¥${this.savedAmount.toFixed(2)}`)
        .fontColor('#FF6600')
      Text(`应付: ¥${this.finalPrice.toFixed(2)}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
    }
  }
}

@Computed 的优势:

  1. 缓存:只有依赖的数据变化时才重新计算

  2. 自动追踪:框架自动追踪依赖关系

  3. 声明式:代码更清晰,意图更明确

6.2 @Monitor:状态变化监听器

当你需要在状态变化时执行副作用(如日志、网络请求、持久化),使用 @Monitor

@ComponentV2
struct UserProfile {
  @Local userName: string = '';
  @Local userAge: number = 0;
  
  // 监听单个属性
  @Monitor('userName')
  onUserNameChange(monitor: IMonitor) {
    console.info(`用户名从 "${monitor.before}" 变为 "${monitor.after}"`);
    // 可以在这里触发网络请求、数据持久化等
    this.saveToLocalStorage();
  }
  
  // 监听多个属性
  @Monitor('userName', 'userAge')
  onUserInfoChange(monitor: IMonitor) {
    console.info(`用户信息发生变化`);
    console.info(`变化的属性: ${monitor.changedPropertyName}`);
  }
  
  private saveToLocalStorage(): void {
    // 持久化逻辑
  }
  
  build() {
    Column({ space: 20 }) {
      TextInput({ placeholder: '请输入用户名', text: this.userName })
        .onChange((value: string) => {
          this.userName = value;
        })
      
      TextInput({ placeholder: '请输入年龄', text: this.userAge.toString() })
        .type(InputType.Number)
        .onChange((value: string) => {
          this.userAge = parseInt(value) || 0;
        })
    }
  }
}

@Monitor vs 传统的 Watch:

特性 @Monitor 传统 Watch
声明方式 装饰器 函数调用
触发时机 属性变化后 属性变化后
获取旧值 ✅ monitor.before 需手动保存
多属性监听 ✅ 原生支持 需要多次调用
与组件生命周期 自动绑定 需手动管理

七、V1 到 V2 迁移指南

7.1 装饰器对照表

V1 装饰器 V2 装饰器 说明
@State @Local 组件私有状态
@Prop @Param 父传子(单向)
@Link @Param + @Event 父传子 + 子传父
@Provide @Provider 向下提供状态
@Consume @Consumer 向上消费状态
@Observed @ObservedV2 类装饰器
@ObjectLink @Param (配合 @ObservedV2) 引用对象
- @Trace 属性级响应式(新增)
- @Event 事件回调(新增)
- @Once 一次性初始化(新增)
- @Computed 计算属性(新增)
- @Monitor 状态监听(新增)

7.2 迁移步骤

第一步:替换组件装饰器

// V1
@Component
struct MyComponent { }

// V2
@ComponentV2
struct MyComponent { }

第二步:替换类装饰器,添加 @Trace

// V1
@Observed
class User {
  name: string = '';
  age: number = 0;
}

// V2
@ObservedV2
class User {
  @Trace name: string = '';
  @Trace age: number = 0;
}

第三步:替换组件内状态装饰器

// V1
@Component
struct MyComponent {
  @State count: number = 0;
  @Prop title: string = '';
}

// V2
@ComponentV2
struct MyComponent {
  @Local count: number = 0;
  @Param title: string = '';
}

第四步:处理双向绑定(@Link → @Param + @Event)

// V1: 父组件
@Component
struct Parent {
  @State value: string = '';
  
  build() {
    Child({ value: $value })  // $符号表示双向绑定
  }
}

// V1: 子组件
@Component
struct Child {
  @Link value: string;
  
  build() {
    TextInput({ text: this.value })
      .onChange((newValue) => {
        this.value = newValue;  // 直接修改
      })
  }
}

// ========================================

// V2: 父组件
@ComponentV2
struct Parent {
  @Local value: string = '';
  
  build() {
    Child({
      value: this.value,
      onValueChange: (newValue: string) => {
        this.value = newValue;
      }
    })
  }
}

// V2: 子组件
@ComponentV2
struct Child {
  @Param value: string = '';
  @Event onValueChange: (value: string) => void = () => {};
  
  build() {
    TextInput({ text: this.value })
      .onChange((newValue) => {
        this.onValueChange(newValue);  // 通过事件通知父组件
      })
  }
}

7.3 常见迁移问题

问题1:@Trace 忘记添加

// ❌ 错误:属性变化不会触发更新
@ObservedV2
class User {
  name: string = '';  // 缺少 @Trace
}

// ✅ 正确
@ObservedV2
class User {
  @Trace name: string = '';
}

问题2:在 @ComponentV2 中使用 V1 装饰器

// ❌ 错误:V1 和 V2 装饰器不能混用
@ComponentV2
struct MyComponent {
  @State count: number = 0;  // 错误!
}

// ✅ 正确
@ComponentV2
struct MyComponent {
  @Local count: number = 0;
}

问题3:@Param 属性尝试修改

// ❌ 错误:@Param 是只读的
@ComponentV2
struct Child {
  @Param value: string = '';
  
  build() {
    Button('修改').onClick(() => {
      this.value = '新值';  // 运行时会报错!
    })
  }
}

// ✅ 正确:通过 @Event 通知父组件修改
@ComponentV2
struct Child {
  @Param value: string = '';
  @Event onChange: (value: string) => void = () => {};
  
  build() {
    Button('修改').onClick(() => {
      this.onChange('新值');
    })
  }
}

八、性能对比与最佳实践

8.1 V1 vs V2 性能对比

我在一个包含 1000 个列表项的页面上进行了测试:

场景 V1 耗时 V2 耗时 提升
首次渲染 380ms 320ms 15.8%
单项属性更新 45ms 8ms 82.2%
批量更新 100 项 420ms 85ms 79.8%
深层嵌套属性更新 需要hack 12ms -

为什么 V2 更快?

  1. 精准更新:V1 经常触发整个组件树重渲染,V2 只更新变化的部分

  2. 依赖追踪:V2 在编译期就建立了依赖关系,运行时开销更小

  3. 批量处理:V2 会合并多个状态变化,减少渲染次数

8.2 最佳实践总结

✅ DO:应该这样做

// 1. 将相关状态组织在类中
@ObservedV2
class FormState {
  @Trace username: string = '';
  @Trace password: string = '';
  @Trace rememberMe: boolean = false;
  
  get isValid(): boolean {
    return this.username.length > 0 && this.password.length >= 6;
  }
}

// 2. 使用 @Computed 处理派生状态
@ComponentV2
struct FormComponent {
  @Local form: FormState = new FormState();
  
  @Computed
  get submitButtonText(): string {
    return this.form.isValid ? '提交' : '请填写完整';
  }
}

// 3. 合理使用 @Once 避免不必要的更新
@ComponentV2
struct ExpensiveComponent {
  @Once @Param initialConfig: Config = new Config();
  @Local internalState: State = new State();
  
  aboutToAppear() {
    this.internalState.init(this.initialConfig);
  }
}

// 4. 使用 @Monitor 处理副作用
@ComponentV2
struct SearchComponent {
  @Local keyword: string = '';
  
  @Monitor('keyword')
  onKeywordChange() {
    // 防抖处理后发起搜索请求
    this.debouncedSearch(this.keyword);
  }
}

❌ DON'T:避免这样做

// 1. 不要在渲染中创建新对象
build() {
  // ❌ 每次渲染都创建新对象,导致子组件重渲染
  ChildComponent({ config: { name: 'test' } })
  
  // ✅ 使用状态变量
  ChildComponent({ config: this.config })
}

// 2. 不要过度使用 @Trace
@ObservedV2
class HugeObject {
  @Trace field1: string = '';  // ✅ 需要响应式
  @Trace field2: string = '';  // ✅ 需要响应式
  internalCache: Map<string, any> = new Map();  // ✅ 不需要响应式,不加 @Trace
}

// 3. 不要在 @Param 中传递大对象
// ❌ 每次父组件更新都会创建新对象
@Param config: { a: number, b: string, c: boolean } = { a: 0, b: '', c: false };

// ✅ 使用 @ObservedV2 类
@Param config: ConfigClass = new ConfigClass();

// 4. 不要忘记 @Require 标记必传参数
// ❌ 可能导致运行时错误
@Param userId: string = '';

// ✅ 明确标记必传
@Param @Require userId: string = '';

九、总结:V2 状态管理的设计哲学

经过这么长的文章,让我们回顾一下 V2 状态管理的核心思想:

9.1 三个核心原则

┌─────────────────────────────────────────────────────────────┐
│              V2 状态管理设计哲学                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 【显式声明】                                            │
│      - @Trace 明确标记哪些属性需要响应式                      │
│      - @Require 明确标记哪些参数必传                          │
│      - 减少"魔法",增加可预测性                               │
│                                                             │
│   2. 【单向数据流】                                          │
│      - @Param 只读,不能直接修改                             │
│      - @Event 显式声明修改通道                               │
│      - 数据流向清晰,便于调试                                 │
│                                                             │
│   3. 【精准更新】                                            │
│      - 属性级别的依赖追踪                                    │
│      - 只更新真正变化的 UI 片段                               │
│      - 性能更优,体验更好                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

9.2 我的建议

作为一个写了 14 年代码的老炮,我的建议是:

  1. 新项目直接用 V2:V2 的设计更合理,性能更好,不要犹豫

  2. 老项目逐步迁移:V1 和 V2 可以共存,按模块逐步迁移

  3. 先理解原理再写代码:理解了 @Trace 的依赖追踪机制,很多问题就迎刃而解

状态管理从来不是什么高深的技术,它的本质就是:让数据变化驱动 UI 更新。V2 做的事情,就是让这个过程更透明、更高效、更可控。


如果这篇文章对你有帮助,请点赞👍、收藏⭐、转发🔄!

关注程序员Feri,获取更多 HarmonyOS 深度技术文章!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐