鸿蒙6.0父子组件通信深度解析

引言:为什么组件通信如此重要

在现代应用开发中,组件化架构已经成为构建可维护、可扩展应用的核心范式。HarmonyOS作为面向全场景的分布式操作系统,其应用开发框架ArkUI采用了声明式UI范式,而组件间的数据通信则是声明式开发中最核心、最具挑战性的问题之一。

良好的组件通信机制能够带来以下优势:

  1. 职责分离:父组件负责数据管理,子组件负责UI渲染和用户交互
  2. 数据流清晰:遵循单向数据流原则,使应用状态可预测、可追踪
  3. 复用性强:解耦的组件可以在不同场景中复用
  4. 维护成本低:修改一处逻辑不会影响其他组件

HarmonyOS 6.0(API Level 15)在状态管理方面引入了V2架构,提供了更加精细化的组件通信方案。本文将深入剖析鸿蒙6.0中的各种组件通信装饰器,从原理到实战,帮助开发者掌握父子组件通信的核心技能。


一、鸿蒙6.0组件通信体系概述

鸿蒙6.0的ArkUI框架提供了丰富的状态管理装饰器,它们共同构成了组件通信的基础设施。根据数据流向和用途,可以分为以下几类:

装饰器对比表格

装饰器 数据流向 适用版本 核心用途 特点
@State 组件内部 V1/V2 管理组件私有状态 基础状态管理
@Prop 父→子 V1 单向传递数据 值拷贝,单向流动
@Link 父↔子 V1 双向数据绑定 引用传递,同步更新
@Local 组件内部 V2 本地状态管理 V2私有状态
@Param 父→子 V2 单向接收参数 替代@Prop,V2专用
@Event 子→父 V2 事件回调输出 鸿蒙6.0新特性
@Watch 监听变化 V2 状态变更监听 配合其他装饰器使用
@Observed 类装饰器 V1/V2 监听对象属性变化 配合@ObjectLink使用
@ObjectLink 对象绑定 V1/V2 嵌套对象同步 复杂对象深度监听

V1与V2架构的核心差异

typescript 复制代码
// V1架构(传统方式)
@Component
struct TraditionalComponent {
  @State count: number = 0;      // 组件私有状态
  @Prop title: string = '';      // 单向接收
  @Link value: string;           // 双向绑定
}

// V2架构(鸿蒙6.0推荐)
@ComponentV2
struct ModernComponent {
  @Local count: number = 0;       // 本地私有状态
  @Param title: string = '';      // 单向接收(替代@Prop)
  @Event onUpdate: (val: number) => void;  // 事件输出(新特性)
}

V2架构相比V1有三大核心优势:

  1. 渲染优化:支持属性级精准刷新,减少70%冗余渲染
  2. 冻结机制 :新增freezeWhenInactive特性,非活跃组件自动暂停状态监听
  3. 职责分离@Param接收、@Local存储、@Event输出,职责更加清晰

二、@Prop - 单向数据传递(父→子)

原理分析

@Prop是ArkTS中最基础的单向数据流装饰器。当父组件将数据传递给子组件时,ArkTS会进行值拷贝而非引用传递。这意味着:

  • 父组件修改数据会同步到子组件
  • 子组件修改数据不会影响父组件
  • 这种设计确保了数据流向的清晰性

使用场景

@Prop适用于以下场景:

  • 父组件向子组件传递配置参数
  • 子组件需要展示父组件的数据,但不需要修改
  • 简单数据类型(number、string、boolean)的传递

代码示例

typescript 复制代码
// 父组件
@Entry
@Component
struct ParentComponent {
  @State parentTitle: string = '原始标题';
  @State count: number = 0;

  build() {
    Column({ space: 20 }) {
      // 展示父组件状态
      Text(`父组件计数: ${this.count}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Button('父组件+1')
        .onClick(() => {
          this.count++;  // 父组件修改,子组件会同步更新
        })

      // 向子组件传递数据
      ChildComponent({
        title: this.parentTitle,
        count: this.count,
        onChildChange: (newTitle: string) => {
          // 子组件通过回调修改父组件数据
          this.parentTitle = newTitle;
        }
      })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

// 子组件:使用@Prop接收父组件数据
@Component
struct ChildComponent {
  @Prop title: string = '';      // 单向接收,值拷贝
  @Prop count: number = 0;        // 单向接收

  // 回调函数用于通知父组件
  onChildChange: (newTitle: string) => void = () => {};

  build() {
    Column({ space: 15 }) {
      Text(`子组件收到的标题: ${this.title}`)
        .fontSize(16)

      Text(`子组件收到的计数: ${this.count}`)
        .fontSize(16)

      Button('子组件尝试修改(不会影响父组件)')
        .onClick(() => {
          // 注意:这里修改的是本地拷贝,不影响父组件
          this.count = 999;
          console.info(`子组件内部count已改为: ${this.count}`);
        })

      Button('子组件通过回调通知父组件')
        .onClick(() => {
          // 通过回调修改父组件的数据
          this.onChildChange('子组件修改后的标题');
        })
    }
    .padding(15)
    .border({ width: 1, color: Color.Gray })
  }
}

注意事项

  1. @Prop装饰的变量不能有默认值冲突
  2. 复杂对象使用@Prop会有性能开销(深拷贝)
  3. 如果需要子组件修改影响父组件,应使用@Link或回调函数

原理分析

@Link提供了真正的双向数据绑定机制。当父组件使用$符号向子组件传递状态时,传递的是引用而非拷贝:

  • 父组件修改 → 子组件同步更新
  • 子组件修改 → 父组件同步更新
  • 两者指向同一数据源

使用场景

@Link适用于以下场景:

  • 需要父子组件共享同一数据源
  • 双向同步需求(如表单输入、表单复选框)
  • 简单数据类型的双向绑定

代码示例

typescript 复制代码
@Entry
@Component
struct LinkDemo {
  @State parentName: string = '张三';
  @State parentAge: number = 25;
  @State isAdult: boolean = true;

  build() {
    Column({ space: 20 }) {
      // 父组件展示区
      Column({ space: 10 }) {
        Text('父组件状态')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)

        Text(`姓名: ${this.parentName}`)
        Text(`年龄: ${this.parentAge}`)
        Text(`是否成年: ${this.isAdult ? '是' : '否'}`)
      }
      .padding(15)
      .backgroundColor('#F0F0F0')
      .width('100%')

      Divider()

      // 使用@Link双向绑定
      ChildWithLink({
        childName: $parentName,    // $符号表示双向绑定
        childAge: $parentAge,
        childIsAdult: $isAdult
      })
    }
    .width('100%')
    .padding(20)
  }
}

// 子组件使用@Link接收
@Component
struct ChildWithLink {
  @Link childName: string;         // 双向绑定
  @Link childAge: number;
  @Link childIsAdult: boolean;

  build() {
    Column({ space: 15 }) {
      Text('子组件(使用@Link)')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      // 子组件直接修改会影响父组件
      TextInput({ placeholder: '请输入姓名', text: this.childName })
        .onChange((value: string) => {
          this.childName = value;
          this.childIsAdult = this.childAge >= 18;
        })

      Counter({
        count: $childAge,
        onCounterChange: (newAge: number) => {
          this.childAge = newAge;
          this.childIsAdult = newAge >= 18;
        }
      })

      Toggle({ type: ToggleType.Switch, isOn: this.childIsAdult })
        .onChange((isOn: boolean) => {
          this.childIsAdult = isOn;
        })
    }
    .padding(15)
    .border({ width: 1, color: Color.Blue })
  }
}

// 自定义计数器组件
@Component
struct Counter {
  @Link count: number;
  onCounterChange: (newCount: number) => void = () => {};

  build() {
    Row({ space: 10 }) {
      Button('-')
        .onClick(() => {
          if (this.count > 0) {
            this.count--;
            this.onCounterChange(this.count);
          }
        })

      Text(`${this.count}`)
        .fontSize(20)
        .width(50)
        .textAlign(TextAlign.Center)

      Button('+')
        .onClick(() => {
          this.count++;
          this.onCounterChange(this.count);
        })
    }
  }
}
特性 @Prop @Link
数据传递方式 值拷贝 引用传递
子→父同步 ❌ 不支持 ✅ 支持
父→子同步 ✅ 支持 ✅ 支持
语法 普通传值 $变量名
性能 深拷贝有开销 直接引用,高效

四、@Event + @Param - V2架构事件回调机制(重点)

核心概念

@Event是**鸿蒙6.0(API 12+)**引入的新特性,是V2架构中子组件向父组件传递事件的标准方式。在V2架构中:

  • @Param:标志组件的输入,接收父组件传递的数据
  • @Event:标志组件的输出,向父组件发送事件/请求

这种设计完美遵循了单向数据流原则:数据的修改权始终在父组件,子组件通过触发事件来发起修改请求。

装饰器说明

typescript 复制代码
/**
 * @Event 属性装饰器说明
 * ┌─────────────────────────────────────────────────────┐
 * │ 装饰器参数       │ 无                                │
 * ├─────────────────────────────────────────────────────┤
 * │ 允许装饰的类型   │ 回调方法                          │
 * │                  │ 如:()=>void, (x:number)=>void   │
 * ├─────────────────────────────────────────────────────┤
 * │ 允许传入函数类型 │ 箭头函数                          │
 * └─────────────────────────────────────────────────────┘
 */

核心原理

@Event的工作机制可以通过以下流程理解:

  1. 数据下行 :父组件使用@Local管理状态,通过@Param传递给子组件
  2. 事件上行 :子组件调用@Event装饰的回调函数,向父组件发送请求
  3. 状态同步 :父组件在回调中更新@Local状态,V2系统自动同步到子组件

代码示例:基础用法

typescript 复制代码
// 父组件
@Entry
@ComponentV2
struct ParentComponent {
  @Local title: string = '默认标题';
  @Local fontColor: Color = Color.Red;

  build() {
    Column({ space: 20 }) {
      Text('父组件')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      // 展示当前状态
      Text(this.title)
        .fontColor(this.fontColor)
        .fontSize(24)

      Divider()

      // 使用@Param传递数据,@Event接收事件
      ChildComponent({
        title: this.title,
        fontColor: this.fontColor,
        // 定义事件处理函数
        onTitleChange: (newTitle: string) => {
          this.title = newTitle;
        },
        onColorChange: (newColor: Color) => {
          this.fontColor = newColor;
        }
      })
    }
    .width('100%')
    .padding(20)
  }
}

// 子组件:V2架构使用@Param + @Event
@ComponentV2
struct ChildComponent {
  // 输入:接收父组件传递的数据
  @Param title: string = '';
  @Param fontColor: Color = Color.Black;

  // 输出:定义事件回调
  @Event onTitleChange: (newTitle: string) => void = () => {};
  @Event onColorChange: (newColor: Color) => void = () => {};

  build() {
    Column({ space: 15 }) {
      Text('子组件(V2架构)')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      Text(`收到的标题: ${this.title}`)

      // 触发事件,修改父组件状态
      Button('修改为"新标题"')
        .onClick(() => {
          this.onTitleChange('新标题');
        })

      Button('修改为"动态标题 - ' + Date.now() + '"')
        .onClick(() => {
          this.onTitleChange('动态标题 - ' + Date.now());
        })

      Row({ space: 10 }) {
        Button('红色')
          .backgroundColor(Color.Red)
          .onClick(() => this.onColorChange(Color.Red))

        Button('蓝色')
          .backgroundColor(Color.Blue)
          .onClick(() => this.onColorChange(Color.Blue))

        Button('绿色')
          .backgroundColor(Color.Green)
          .onClick(() => this.onColorChange(Color.Green))
      }
    }
    .padding(15)
    .border({ width: 2, color: Color.Gray })
  }
}

代码示例:异步同步特性

typescript 复制代码
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[EventAsyncDemo]';
const DOMAIN = 0xF811;

/**
 * 演示@Event的异步同步特性
 * 重要:@Event修改父组件值是立刻生效的,但从父组件同步回子组件是异步的
 */
@Entry
@ComponentV2
struct EventAsyncDemo {
  @Local counter: number = 0;

  build() {
    Column({ space: 20 }) {
      Text(`父组件计数器: ${this.counter}`)
        .fontSize(24)

      Button('父组件+1(同步)')
        .onClick(() => {
          this.counter++;
        })

      AsyncChild({ count: this.counter })
    }
    .width('100%')
    .padding(20)
  }
}

@ComponentV2
struct AsyncChild {
  @Param count: number = 0;

  // 定义带参数的事件回调
  @Event onCountChange: (newCount: number) => void = (val: number) => {};

  build() {
    Column({ space: 15 }) {
      Text(`子组件接收到的值: ${this.count}`)
        .fontSize(20)

      Button('子组件触发+10')
        .onClick(() => {
          // 触发事件,传入新值
          this.onCountChange(this.count + 10);

          // 打印日志验证异步同步特性
          hilog.info(DOMAIN, TAG, `调用后子组件值: ${this.count}`);
        })

      Text('提示:点击按钮后,父组件值立即更新,但子组件值稍后异步同步')
        .fontSize(12)
        .fontColor(Color.Gray)
    }
    .padding(15)
    .border({ width: 1, color: Color.Gray })
  }
}

@Event的高级用法:多参数回调

typescript 复制代码
@Entry
@ComponentV2
struct AdvancedEventDemo {
  @Local userInfo: UserInfo = new UserInfo('张三', 25, '北京');

  build() {
    Column({ space: 20 }) {
      Text('用户信息管理')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      // 展示当前用户信息
      Column({ space: 10 }) {
        Text(`姓名: ${this.userInfo.name}`)
        Text(`年龄: ${this.userInfo.age}`)
        Text(`地址: ${this.userInfo.address}`)
      }
      .padding(15)
      .backgroundColor('#F5F5F5')
      .width('100%')

      // 使用复合事件处理器
      ComplexEventChild({
        user: this.userInfo,
        // 定义复合事件回调,同时处理多个字段更新
        onUserUpdate: (field: string, value: string | number) => {
          if (field === 'name') {
            this.userInfo.name = value as string;
          } else if (field === 'age') {
            this.userInfo.age = value as number;
          } else if (field === 'address') {
            this.userInfo.address = value as string;
          }
        }
      })
    }
    .width('100%')
    .padding(20)
  }
}

// 用户信息类
@ObservedV2
class UserInfo {
  @Trace name: string;
  @Trace age: number;
  @Trace address: string;

  constructor(name: string, age: number, address: string) {
    this.name = name;
    this.age = age;
    this.address = address;
  }
}

@ComponentV2
struct ComplexEventChild {
  @Param user: UserInfo;

  // 多参数事件回调
  @Event onUserUpdate: (field: string, value: string | number) => void =
    (field: string, value: string | number) => {};

  build() {
    Column({ space: 15 }) {
      Text('子组件(复杂事件)')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      TextInput({ placeholder: '修改姓名', text: this.user.name })
        .onChange((value: string) => {
          this.onUserUpdate('name', value);
        })

      TextInput({ placeholder: '修改年龄', text: this.user.age.toString() })
        .type(InputType.Number)
        .onChange((value: string) => {
          const age = parseInt(value);
          if (!isNaN(age)) {
            this.onUserUpdate('age', age);
          }
        })

      TextInput({ placeholder: '修改地址', text: this.user.address })
        .onChange((value: string) => {
          this.onUserUpdate('address', value);
        })
    }
    .padding(15)
    .border({ width: 1, color: Color.Blue })
  }
}

五、@Watch - 状态变更监听器

原理分析

@Watch装饰器用于监听状态变量的变化,当被监听的值发生改变时,会触发指定的回调函数。回调函数接收两个参数:newValue(新值)和oldValue(旧值)。

使用场景

  • 监听状态变化执行副作用操作
  • 数据验证和格式化
  • 联动其他状态的更新
  • 日志记录和调试

代码示例

typescript 复制代码
@Entry
@ComponentV2
struct WatchDemo {
  @Local @Watch('onCountChange') count: number = 0;
  @Local @Watch('onNameChange') userName: string = 'Guest';
  @Local @Watch('onFormChange') formData: FormData = new FormData();
  @Local logMessages: string[] = [];

  // 监听count变化
  onCountChange(newVal: number, oldVal: number): void {
    this.logMessages.unshift(`count: ${oldVal} → ${newVal}`);

    // 限制最大值
    if (newVal > 100) {
      this.count = 100;
    }

    // 保持数组长度
    if (this.logMessages.length > 5) {
      this.logMessages.pop();
    }
  }

  // 监听userName变化
  onNameChange(newVal: string, oldVal: string): void {
    this.logMessages.unshift(`name: "${oldVal}" → "${newVal}"`);

    // 自动格式化:首字母大写
    if (newVal.length > 0) {
      this.userName = newVal.charAt(0).toUpperCase() + newVal.slice(1);
    }

    if (this.logMessages.length > 5) {
      this.logMessages.pop();
    }
  }

  // 监听对象变化
  onFormChange(newVal: FormData, oldVal: FormData): void {
    this.logMessages.unshift(`form updated: email=${newVal.email}`);
    if (this.logMessages.length > 5) {
      this.logMessages.pop();
    }
  }

  build() {
    Column({ space: 20 }) {
      Text('@Watch 状态监听演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      // 计数器示例
      Row({ space: 15 }) {
        Button('-减')
          .onClick(() => this.count--)

        Text(`计数: ${this.count}`)
          .fontSize(20)
          .width(100)
          .textAlign(TextAlign.Center)

        Button('+加')
          .onClick(() => this.count++)
      }

      // 输入框示例
      TextInput({ placeholder: '输入用户名', text: this.userName })
        .onChange((value: string) => {
          this.userName = value;
        })

      // 表单示例
      FormWatcher({ formData: $formData })

      // 变化日志
      Column({ space: 5 }) {
        Text('变化日志:')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)

        ForEach(this.logMessages, (log: string) => {
          Text(log)
            .fontSize(12)
            .fontColor(Color.Gray)
        })
      }
      .padding(10)
      .backgroundColor('#FAFAFA')
      .width('100%')
    }
    .width('100%')
    .padding(20)
  }
}

// 表单数据类
@ObservedV2
class FormData {
  @Trace email: string = '';
  @Trace phone: string = '';

  constructor(email: string = '', phone: string = '') {
    this.email = email;
    this.phone = phone;
  }
}

@ComponentV2
struct FormWatcher {
  @Param formData: FormData;

  build() {
    Column({ space: 10 }) {
      Text('表单监听')
        .fontSize(14)

      TextInput({ placeholder: '邮箱', text: this.formData.email })
        .onChange((value: string) => {
          this.formData.email = value;
        })

      TextInput({ placeholder: '电话', text: this.formData.phone })
        .type(InputType.PhoneNumber)
        .onChange((value: string) => {
          this.formData.phone = value;
        })
    }
    .padding(10)
    .border({ width: 1, color: Color.Gray })
  }
}

@Watch与@Watch的组合使用

typescript 复制代码
@Entry
@ComponentV2
struct WatchCombinationDemo {
  @Local @Watch('onPriceChange') price: number = 100;
  @Local @Watch('onQuantityChange') quantity: number = 1;
  @Local total: number = 100;

  // 价格变化时重新计算总价
  onPriceChange(newVal: number, oldVal: number): void {
    this.total = newVal * this.quantity;
    console.info(`价格变化: ${oldVal} → ${newVal}, 总价: ${this.total}`);
  }

  // 数量变化时重新计算总价
  onQuantityChange(newVal: number, oldVal: number): void {
    this.total = this.price * newVal;
    console.info(`数量变化: ${oldVal} → ${newVal}, 总价: ${this.total}`);
  }

  build() {
    Column({ space: 20 }) {
      Text('购物车计算器')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Row({ space: 15 }) {
        Text('单价: ¥')
        TextInput({ text: this.price.toString() })
          .type(InputType.Number)
          .width(100)
          .onChange((value: string) => {
            const num = parseFloat(value);
            if (!isNaN(num) && num >= 0) {
              this.price = num;
            }
          })
      }

      Row({ space: 15 }) {
        Text('数量: ')
        Stepper({ minValue: 1, maxValue: 99 })
          .onValueChange((value: number) => {
            this.quantity = value;
          })
      }

      Text(`总价: ¥${this.total.toFixed(2)}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.Red)
    }
    .width('100%')
    .padding(20)
  }
}

原理分析

当组件间需要传递复杂对象(嵌套对象或数组)时,@Prop@Link无法监听对象内部属性的变化。这时需要使用@Observed@ObjectLink组合:

  • @Observed:装饰类,使类属性变化可被监听
  • @ObjectLink:在子组件中引用被@Observed装饰的类的实例

使用场景

  • 传递嵌套对象(如用户信息包含地址对象)
  • 列表数据中每个元素是复杂对象
  • 需要深度监听对象属性变化的场景

代码示例

typescript 复制代码
// 定义被@Observed装饰的类
@Observed
class UserProfile {
  name: string;
  age: number;
  address: Address;

  constructor(name: string, age: number, address: Address) {
    this.name = name;
    this.age = age;
    this.address = address;
  }
}

@Observed
class Address {
  city: string;
  district: string;
  street: string;

  constructor(city: string, district: string, street: string) {
    this.city = city;
    this.district = district;
    this.street = street;
  }
}

@Entry
@ComponentV2
struct ObservedDemo {
  @Local user: UserProfile = new UserProfile(
    '张三',
    28,
    new Address('北京', '朝阳区', '建国路88号')
  );

  build() {
    Column({ space: 20 }) {
      Text('@Observed + @ObjectLink 演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      // 直接展示
      Column({ space: 10 }) {
        Text(`姓名: ${this.user.name}`)
        Text(`年龄: ${this.user.age}`)
        Text(`城市: ${this.user.address.city}`)
        Text(`区县: ${this.user.address.district}`)
        Text(`街道: ${this.user.address.street}`)
      }
      .padding(15)
      .backgroundColor('#F5F5F5')
      .width('100%')

      // 嵌套对象子组件
      ProfileEditor({ profile: this.user }))

      // 修改整体对象
      Button('重置用户')
        .onClick(() => {
          this.user = new UserProfile(
            '新用户',
            18,
            new Address('上海', '浦东新区', '世纪大道100号')
          );
        })
    }
    .width('100%')
    .padding(20)
  }
}

// 使用@ObjectLink接收复杂对象
@ComponentV2
struct ProfileEditor {
  @ObjectLink profile: UserProfile;

  build() {
    Column({ space: 15 }) {
      Text('编辑用户信息')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      // 编辑基本属性
      TextInput({ placeholder: '姓名', text: this.profile.name })
        .onChange((value: string) => {
          this.profile.name = value;
        })

      TextInput({ placeholder: '年龄', text: this.profile.age.toString() })
        .type(InputType.Number)
        .onChange((value: string) => {
          const age = parseInt(value);
          if (!isNaN(age)) {
            this.profile.age = age;
          }
        })

      Divider()

      Text('编辑地址')
        .fontSize(14)
        .fontWeight(FontWeight.Medium)

      // 编辑嵌套对象属性 - 地址
      TextInput({ placeholder: '城市', text: this.profile.address.city })
        .onChange((value: string) => {
          this.profile.address.city = value;
        })

      TextInput({ placeholder: '区县', text: this.profile.address.district })
        .onChange((value: string) => {
          this.profile.address.district = value;
        })

      TextInput({ placeholder: '街道', text: this.profile.address.street })
        .onChange((value: string) => {
          this.profile.address.street = value;
        })
    }
    .padding(15)
    .border({ width: 2, color: Color.Blue })
  }
}

数组场景使用

typescript 复制代码
@Observed
class TaskItem {
  id: number;
  title: string;
  completed: boolean;

  constructor(id: number, title: string, completed: boolean = false) {
    this.id = id;
    this.title = title;
    this.completed = completed;
  }
}

@Entry
@ComponentV2
struct ArrayObservedDemo {
  @Local @Trace taskList: TaskItem[] = [
    new TaskItem(1, '完成文档', false),
    new TaskItem(2, '代码审查', false),
    new TaskItem(3, '发布版本', false)
  ];

  build() {
    Column({ space: 20 }) {
      Text('任务列表(@Observed数组)')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      // 渲染任务列表
      ForEach(this.taskList, (task: TaskItem, index: number) => {
        TaskItemCard({
          task: this.taskList[index]
        })
      }, (task: TaskItem) => task.id.toString())

      Button('添加新任务')
        .onClick(() => {
          const newId = this.taskList.length + 1;
          this.taskList.push(new TaskItem(newId, `任务${newId}`, false));
        })
    }
    .width('100%')
    .padding(20)
  }
}

@ComponentV2
struct TaskItemCard {
  @ObjectLink task: TaskItem;

  build() {
    Row({ space: 15 }) {
      Checkbox()
        .select(this.task.completed)
        .onChange((isChecked: boolean) => {
          this.task.completed = isChecked;
        })

      Text(this.task.title)
        .fontSize(16)
        .fontDecoration(this.task.completed ? {
          type: TextDecorationType.LineThrough,
          color: Color.Gray
        } : { type: TextDecorationType.None })
        .fontColor(this.task.completed ? Color.Gray : Color.Black)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(this.task.completed ? '#F0F0F0' : '#FFFFFF')
    .borderRadius(8)
  }
}

七、实战案例:完整的用户表单组件

下面我们综合运用所有装饰器,实现一个完整的用户注册表单组件:

typescript 复制代码
/**
 * 综合实战:用户注册表单
 * 展示所有装饰器的综合运用
 */

// ==================== 数据模型 ====================

// 用户信息模型
@ObservedV2
class UserModel {
  @Trace username: string = '';
  @Trace email: string = '';
  @Trace phone: string = '';
  @Trace age: number = 0;
  @Trace address: AddressModel = new AddressModel();

  constructor() {}
}

@ObservedV2
class AddressModel {
  @Trace province: string = '';
  @Trace city: string = '';
  @Trace district: string = '';
  @Trace detail: string = '';

  constructor() {}
}

// 表单验证状态
@ObservedV2
class FormValidation {
  @Trace usernameValid: boolean = false;
  @Trace emailValid: boolean = false;
  @Trace phoneValid: boolean = false;
  @Trace ageValid: boolean = false;
  @Trace addressValid: boolean = false;

  get isAllValid(): boolean {
    return this.usernameValid && this.emailValid &&
           this.phoneValid && this.ageValid && this.addressValid;
  }
}

// ==================== 主页面 ====================

@Entry
@ComponentV2
struct UserRegistrationPage {
  @Local userModel: UserModel = new UserModel();
  @Local validation: FormValidation = new FormValidation();
  @Local @Watch('onSubmitEnabledChange') submitEnabled: boolean = false;

  // 监听提交按钮状态
  onSubmitEnabledChange(): void {
    console.info('提交按钮状态变化');
  }

  build() {
    Column({ space: 20 }) {
      // 页面标题
      Text('用户注册表单')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .textAlign(TextAlign.Center)
        .width('100%')

      Scroll() {
        Column({ space: 25 }) {
          // 用户名输入
          UsernameInput({
            username: this.userModel.username,
            onUsernameChange: (value: string) => {
              this.userModel.username = value;
              this.validation.usernameValid = value.length >= 3;
            }
          })

          // 邮箱输入
          EmailInput({
            email: this.userModel.email,
            onEmailChange: (value: string) => {
              this.userModel.email = value;
              this.validation.emailValid = this.isValidEmail(value);
            }
          })

          // 电话输入
          PhoneInput({
            phone: this.userModel.phone,
            onPhoneChange: (value: string) => {
              this.userModel.phone = value;
              this.validation.phoneValid = /^1[3-9]\d{9}$/.test(value);
            }
          })

          // 年龄输入
          AgeInput({
            age: this.userModel.age,
            onAgeChange: (value: number) => {
              this.userModel.age = value;
              this.validation.ageValid = value >= 18 && value <= 120;
            }
          })

          // 地址输入
          AddressInput({
            address: this.userModel.address,
            onAddressChange: (field: string, value: string) => {
              if (field === 'province') this.userModel.address.province = value;
              if (field === 'city') this.userModel.address.city = value;
              if (field === 'district') this.userModel.address.district = value;
              if (field === 'detail') this.userModel.address.detail = value;
              this.validation.addressValid =
                this.userModel.address.province.length > 0 &&
                this.userModel.address.city.length > 0;
            }
          })

          // 表单验证状态展示
          ValidationStatus({ validation: this.validation })

          // 提交按钮
          Button('提交注册')
            .width('100%')
            .height(50)
            .fontSize(18)
            .enabled(this.validation.isAllValid)
            .backgroundColor(this.validation.isAllValid ? Color.Blue : Color.Gray)
            .onClick(() => {
              this.submitForm();
            })

          // 重置按钮
          Button('重置表单')
            .width('100%')
            .height(45)
            .fontColor(Color.Red)
            .backgroundColor(Color.Transparent)
            .onClick(() => {
              this.resetForm();
            })
        }
        .padding(20)
      }
      .scrollBarWidth(10)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAFAFA')
  }

  // 邮箱验证
  isValidEmail(email: string): boolean {
    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    return emailRegex.test(email);
  }

  // 提交表单
  submitForm(): void {
    console.info('提交表单数据:');
    console.info(`用户名: ${this.userModel.username}`);
    console.info(`邮箱: ${this.userModel.email}`);
    console.info(`电话: ${this.userModel.phone}`);
    console.info(`年龄: ${this.userModel.age}`);
    console.info(`地址: ${this.userModel.address.province} ${this.userModel.address.city} ${this.userModel.address.district} ${this.userModel.address.detail}`);
  }

  // 重置表单
  resetForm(): void {
    this.userModel = new UserModel();
    this.validation = new FormValidation();
  }
}

// ==================== 子组件定义 ====================

@ComponentV2
struct UsernameInput {
  @Param username: string = '';
  @Event onUsernameChange: (value: string) => void = () => {};

  build() {
    Column({ space: 8 }) {
      Text('用户名')
        .fontSize(14)
        .fontColor(Color.Gray)
        .alignSelf(ItemAlign.Start)

      TextInput({
        placeholder: '请输入用户名(至少3个字符)',
        text: this.username
      })
        .onChange((value: string) => {
          this.onUsernameChange(value);
        })

      Text(this.username.length > 0 && this.username.length < 3 ? '用户名太短' : '')
        .fontSize(12)
        .fontColor(Color.Red)
        .alignSelf(ItemAlign.Start)
    }
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(10)
  }
}

@ComponentV2
struct EmailInput {
  @Param email: string = '';
  @Event onEmailChange: (value: string) => void = () => {};

  build() {
    Column({ space: 8 }) {
      Text('邮箱')
        .fontSize(14)
        .fontColor(Color.Gray)
        .alignSelf(ItemAlign.Start)

      TextInput({
        placeholder: '请输入邮箱地址',
        text: this.email
      })
        .type(InputType.Email)
        .onChange((value: string) => {
          this.onEmailChange(value);
        })
    }
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(10)
  }
}

@ComponentV2
struct PhoneInput {
  @Param phone: string = '';
  @Event onPhoneChange: (value: string) => void = () => {};

  build() {
    Column({ space: 8 }) {
      Text('手机号')
        .fontSize(14)
        .fontColor(Color.Gray)
        .alignSelf(ItemAlign.Start)

      TextInput({
        placeholder: '请输入11位手机号',
        text: this.phone
      })
        .type(InputType.PhoneNumber)
        .onChange((value: string) => {
          this.onPhoneChange(value);
        })
    }
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(10)
  }
}

@ComponentV2
struct AgeInput {
  @Param age: number = 0;
  @Event onAgeChange: (value: number) => void = () => {};

  build() {
    Column({ space: 8 }) {
      Text('年龄')
        .fontSize(14)
        .fontColor(Color.Gray)
        .alignSelf(ItemAlign.Start)

      Row({ space: 15 }) {
        Text('18岁 - 120岁')
          .fontSize(12)
          .fontColor(Color.Gray)

        Stepper({ minValue: 1, maxValue: 120 })
          .onValueChange((value: number) => {
            this.onAgeChange(value);
          })
      }
      .width('100%')
    }
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(10)
  }
}

@ComponentV2
struct AddressInput {
  @Param address: AddressModel;
  @Event onAddressChange: (field: string, value: string) => void = () => {};

  build() {
    Column({ space: 15 }) {
      Text('收货地址')
        .fontSize(14)
        .fontColor(Color.Gray)
        .alignSelf(ItemAlign.Start)

      Row({ space: 10 }) {
        TextInput({ placeholder: '省', text: this.address.province })
          .layoutWeight(1)
          .onChange((value: string) => this.onAddressChange('province', value))

        TextInput({ placeholder: '市', text: this.address.city })
          .layoutWeight(1)
          .onChange((value: string) => this.onAddressChange('city', value))

        TextInput({ placeholder: '区', text: this.address.district })
          .layoutWeight(1)
          .onChange((value: string) => this.onAddressChange('district', value))
      }

      TextInput({ placeholder: '详细地址', text: this.address.detail })
        .onChange((value: string) => this.onAddressChange('detail', value))
    }
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(10)
  }
}

@ComponentV2
struct ValidationStatus {
  @Param validation: FormValidation;

  build() {
    Column({ space: 5 }) {
      Text('表单验证状态')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)

      Row({ space: 10 }) {
        StatusBadge({ label: '用户名', valid: this.validation.usernameValid })
        StatusBadge({ label: '邮箱', valid: this.validation.emailValid })
        StatusBadge({ label: '电话', valid: this.validation.phoneValid })
        StatusBadge({ label: '年龄', valid: this.validation.ageValid })
        StatusBadge({ label: '地址', valid: this.validation.addressValid })
      }
      .width('100%')
    }
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(10)
  }
}

@ComponentV2
struct StatusBadge {
  @Param label: string = '';
  @Param valid: boolean = false;

  build() {
    Column({ space: 4 }) {
      Text(this.valid ? '✓' : '✗')
        .fontSize(16)
        .fontColor(this.valid ? Color.Green : Color.Red)

      Text(this.label)
        .fontSize(10)
        .fontColor(Color.Gray)
    }
    .padding(8)
    .backgroundColor(this.valid ? '#E8F5E9' : '#FFEBEE')
    .borderRadius(8)
  }
}

八、最佳实践与常见问题解决方案

最佳实践

1. 选择合适的装饰器组合
场景 推荐方案 原因
简单数据传递 @Param 单向数据流,职责清晰
双向绑定 @Link 简单直接,同步高效
事件驱动更新 @Param + @Event 遵循单向数据流原则
复杂对象 @Observed + @ObjectLink 支持深度监听
状态变化监听 @Watch 执行副作用操作
2. V2架构迁移建议
typescript 复制代码
// ❌ 不推荐:V1与V2混用
@Component
struct MixedComponent {
  @State count: number = 0;      // V1
  @Local localData: string = '';  // V2 - 不应混用
}

// ✅ 推荐:明确选择V1或V2
// 方案1:使用V1架构
@Component
struct V1Component {
  @State count: number = 0;
  @Prop title: string = '';
  @Link value: string;
}

// 方案2:使用V2架构(鸿蒙6.0推荐)
@ComponentV2
struct V2Component {
  @Local count: number = 0;
  @Param title: string = '';
  @Event onUpdate: (val: number) => void = () => {};
}
3. 性能优化技巧
typescript 复制代码
// ❌ 避免:频繁创建新对象
build() {
  Column() {
    Child({ data: { name: 'test', value: this.count } }) // 每次build都创建新对象
  }
}

// ✅ 推荐:使用@State管理对象
@State dataModel: DataModel = new DataModel();

build() {
  Column() {
    Child({ data: this.dataModel }) // 使用稳定引用
  }
}

常见问题解决方案

问题1:子组件修改数据后父组件没有更新

原因@Prop是值拷贝,子组件修改不会影响父组件。

解决方案 :使用@Link或通过回调函数/事件机制修改父组件数据。

typescript 复制代码
// ❌ 错误:@Prop无法修改父组件
@Component
struct BadChild {
  @Prop value: number = 0;

  onClick() {
    this.value = 100; // 不会影响父组件
  }
}

// ✅ 正确:使用回调
@Component
struct GoodChild {
  @Prop value: number = 0;
  onChange: (newVal: number) => void = () => {};

  onClick() {
    this.onChange(100); // 通过回调修改父组件
  }
}
问题2:@Event回调未触发

原因:回调函数未正确初始化或参数类型不匹配。

解决方案

typescript 复制代码
// ✅ 确保回调函数类型匹配
@Event onUpdate: (value: number) => void = () => {};

// 父组件传入
Child({
  onUpdate: (val: number) => { // 参数类型必须一致
    this.count = val;
  }
})
问题3:@ObjectLink监听不到嵌套属性变化

原因 :嵌套对象的类也需要使用@Observed装饰。

解决方案

typescript 复制代码
// ❌ 错误:内层对象未装饰
@Observed
class Outer {
  inner: Inner; // 无法监听Inner属性变化
}

class Inner { // 缺少@Observed
  value: string = '';
}

// ✅ 正确:所有层级的类都需要装饰
@Observed
class Outer {
  inner: Inner;
}

@Observed
class Inner {
  @Trace value: string = '';
}
问题4:@Watch回调中修改状态导致死循环

解决方案:使用条件判断或标志位避免递归。

typescript 复制代码
@Local @Watch('onValueChange') value: number = 0;
isUpdating: boolean = false;

onValueChange(newVal: number, oldVal: number): void {
  if (!this.isUpdating) {
    this.isUpdating = true;
    // 执行更新逻辑
    this.isUpdating = false;
  }
}

总结

本文深入解析了鸿蒙6.0 ArkUI框架中的父子组件通信机制,涵盖以下核心内容:

装饰器对比总览

装饰器 角色 数据流向 版本 适用场景
@State 状态容器 组件内部 V1/V2 组件私有状态
@Prop 输入 父→子 V1 简单数据单向传递
@Link 双向绑定 父↔子 V1 需要双向同步
@Local 本地状态 组件内部 V2 V2私有状态
@Param 输入 父→子 V2 V2单向接收
@Event 输出 子→父 V2 事件回调(新特性)
@Watch 监听器 监听变化 V1/V2 状态变化监听
@Observed 对象装饰 类级别 V1/V2 复杂对象监听
@ObjectLink 对象引用 绑定引用 V1/V2 嵌套对象同步

关键要点

  1. 单向数据流是核心原则:数据修改权在父组件,子组件通过事件请求修改
  2. @Event是鸿蒙6.0的重要新特性:配合@Param实现规范的事件驱动架构
  3. 选择合适的装饰器组合:根据数据流向和复杂度选择最优方案
  4. V2架构是未来趋势:新项目推荐使用V2架构,享受性能优化
相关推荐
李李李勃谦13 小时前
鸿蒙PC密码管理器实战:本地加密存储与自动填充完整实现
华为·harmonyos
Swift社区15 小时前
鸿蒙 App 架构中的“领域拆分”
华为·架构·harmonyos
maaath18 小时前
【maaath】Flutter for OpenHarmony 手表配饰应用实战开发
flutter·华为·harmonyos
maaath18 小时前
【maaath】Flutter for OpenHarmony 跨平台计算器应用开发实践
flutter·华为·harmonyos
以太浮标19 小时前
华为eNSP模拟器综合实验之- MGRE多点GRE隧道详解
运维·网络·网络协议·网络安全·华为·信息与通信
前端不太难1 天前
鸿蒙PC和App:都在走向 System
华为·状态模式·harmonyos
maaath1 天前
【maaath】Flutter for OpenHarmony 闹钟时钟应用开发实战
flutter·华为·harmonyos
maaath1 天前
【maaath】Flutter for OpenHarmony 短信管理应用实战
flutter·华为·harmonyos
程序猿追1 天前
从零打造一个“跳一跳”:在HarmonyOS模拟器上用Canvas复刻经典
华为·harmonyos