鸿蒙开发实战:Component V1与V2的核心差异及迁移指南_副本

鸿蒙开发实战:Component V1 与 V2 的核心差异及迁移指南

参考文档: https://developer.huawei.com/consumer/cn/doc/architecture-guides/common-v1_26-ts_88-0000002312256121

在鸿蒙应用开发中,Component(组件)是构建 UI 的基础单元。随着 ArkUI 框架的迭代,Component 经历了从 V1 到 V2 的演进,两者在设计理念、使用方式和功能支持上存在显著差异。很多开发者在项目中会纠结:新项目该用 V1 还是 V2?老项目是否需要迁移到 V2?本文结合实际开发经验,从核心定位、关键差异、代码实现到迁移建议,全方位解析二者的区别。

一、状态管理概述

在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时状态作为参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化导致的UI重新渲染,在ArkUI中统称为状态管理机制。

在这里插入图片描述

  • View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
  • State:状态,指驱动UI更新的数据。通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。

二、状态管理V1

1. @State装饰器

被状态变量装饰器装饰的变量称为状态变量,使普通变量具备状态属性。当状态变量改变时,会触发其直接绑定的UI组件渲染更新。

  • @State装饰的变量生命周期与其所属自定义组件的生命周期相同。
// 简单类型
@State count: number = 0;
// 可以观察到值的变化
this.count = 1;

2. @Prop装饰器

@Prop装饰的变量可以和父组件建立单向同步关系。

  • @Prop装饰的变量允许本地修改,但修改不会同步回父组件。
  • 当数据源更改时,@Prop装饰的变量都会更新,并且会覆盖本地所有更改。
@Component
struct Son {
  @Prop message: string = 'Hi';

  build() {
    Column() {
      Text(this.message)
    }
  }
}

@Entry
@Component
struct Father {
  @State message: string = 'Hello';

  build() {
    Column() {
      Text(this.message)
      Button(`father click`).onClick(() => {
        this.message += '*';
      })
      Son({ message: this.message })
    }
  }
}

3. @Link装饰器

@Link装饰的变量与其父组件中的数据源共享相同的值。

class Info {
  info: string = 'Hello';
}

@Component
struct Child {
  // 正确写法
  @Link test: Info;

  build() {
    Text(this.test.info)
  }
}

@Entry
@Component
struct LinkExample {
  @State info: Info = new Info();

  build() {
    Column() {
      // 正确写法
      Child({test: this.info})
    }
  }
}

4. @Provide装饰器和@Consume装饰器

应用于与后代组件的双向数据同步、状态数据在多个层级之间传递的场景。

  • @Provide装饰的状态变量自动对其所有后代组件可用,开发者不需要多次在组件之间传递变量。
  • 后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以更便捷的在多层级父子组件之间传递。
  • @Provide和@Consume通过变量名或者变量别名绑定,需要类型相同,否则会发生类型隐式转换,从而导致应用行为异常。
@Component
struct Child {
  @Consume num: number;
  // 从API version 20开始,@Consume装饰的变量支持设置默认值
  @Consume num1: number = 17;

  build() {
    Column() {
      Text(`num的值: ${this.num}`)
      Text(`num1的值:${this.num1}`)
    }
  }
}

@Entry
@Component
struct Parent {
  @Provide num: number = 10;

  build() {
    Column() {
      Text(`num的值: ${this.num}`)
      Child()
    }
  }
}

5. @Observed装饰器和@ObjectLink装饰器

  • @State@Prop@Link@Provide和@Consume装饰器,仅能观察到第一层的变化
  • @ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步
  • 使用new创建被@Observed装饰的类,可以被观察到属性的变化。
  • 组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
  • @Observed用于嵌套类场景中,观察对象类属性变化,要配合自定义组件使用(示例详见嵌套对象),如果要做数据双/单向同步,需要搭配@ObjectLink或者@Prop使用(示例详见@Prop与@ObjectLink的差异)。
@Observed
class DateClass extends Date {
  constructor(args: number | string) {
    super(args);
  }
}

@Observed
class NewDate {
  public data: DateClass;

  constructor(data: DateClass) {
    this.data = data;
  }
}

@Component
struct Child {
  label: string = 'date';
  @ObjectLink data: DateClass;

  build() {
    Column() {
      Button(`child increase the day by 1`)
        .onClick(() => {
          this.data.setDate(this.data.getDate() + 1);
        })
      DatePicker({
        start: new Date('1970-1-1'),
        end: new Date('2100-1-1'),
        selected: this.data
      })
    }
  }
}

@Entry
@Component
struct Parent {
  @State newData: NewDate = new NewDate(new DateClass('2023-1-1'));

  build() {
    Column() {
      Child({ label: 'date', data: this.newData.data })

      Button(`parent update the new date`)
        .onClick(() => {
          this.newData.data = new DateClass('2023-07-07');
        })
      Button(`ViewB: this.newData = new NewDate(new DateClass('2023-08-20'))`)
        .onClick(() => {
          this.newData = new NewDate(new DateClass('2023-08-20'));
        })
    }
  }
}

6. @Watch装饰器

@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。

@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当严格相等判断的结果是false(即不相等)的情况下,就会触发@Watch的回调。

@Component
struct TotalView {
  @Prop @Watch('onCountUpdated') count: number = 0;
  @State total: number = 0;
  // @Watch 回调
  onCountUpdated(propName: string): void {
    this.total += this.count;
  }

  build() {
    Text(`Total: ${this.total}`)
  }
}

@Entry
@Component
struct CountModifier {
  @State count: number = 0;

  build() {
    Column() {
      Button('add to basket')
        .onClick(() => {
          this.count++
        })
      TotalView({ count: this.count })
    }
  }
}

7. @Track装饰器

@Track是class对象的属性装饰器。当一个class对象是状态变量时,@Track装饰的属性发生变化,只会触发该属性关联的UI更新;如果class类中使用了@Track装饰器,则未被@Track装饰器装饰的属性不能在UI中使用,如果使用,会发生运行时报错。

class LogTrack {
  @Track str1: string;
  @Track str2: string;

  constructor(str1: string) {
    this.str1 = str1;
    this.str2 = 'World';
  }
}

class LogNotTrack {
  str1: string;
  str2: string;

  constructor(str1: string) {
    this.str1 = str1;
    this.str2 = '世界';
  }
}

@Entry
@Component
struct AddLog {
  @State logTrack: LogTrack = new LogTrack('Hello');
  @State logNotTrack: LogNotTrack = new LogNotTrack('你好');

  isRender(index: number) {
    console.log(`Text ${index} is rendered`);
    return 50;
  }

  build() {
    Row() {
      Column() {
        Text(this.logTrack.str1) // Text1
          .fontSize(this.isRender(1))
          .fontWeight(FontWeight.Bold)
        Text(this.logTrack.str2) // Text2
          .fontSize(this.isRender(2))
          .fontWeight(FontWeight.Bold)
        Button('change logTrack.str1')
          .onClick(() => {
            this.logTrack.str1 = 'Bye';
          })
        Text(this.logNotTrack.str1) // Text3
          .fontSize(this.isRender(3))
          .fontWeight(FontWeight.Bold)
        Text(this.logNotTrack.str2) // Text4
          .fontSize(this.isRender(4))
          .fontWeight(FontWeight.Bold)
        Button('change logNotTrack.str1')
          .onClick(() => {
            this.logNotTrack.str1 = '再见';
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

三、状态管理V2

1. @Local装饰器

@Local表示组件内部的状态,使得自定义组件内部的变量具有观测变化的能力

  • 被@Local装饰的变量无法从外部初始化,因此必须在组件内部进行初始化。

  • 当被@Local装饰的变量变化时,会刷新使用该变量的组件。

  • @Local支持观测number、boolean、string、Object、class等基本类型以及ArraySetMapDate等内嵌类型。

  • @Local的观测能力仅限于被装饰的变量本身。当装饰简单类型时,能够观测到对变量的赋值;当装饰对象类型时,仅能观测到对对象整体的赋值;当装饰数组类型时,能观测到数组整体以及数组元素项的变化;当装饰Array、Set、Map、Date等内嵌类型时,可以观测到通过API调用带来的变化。详见观察变化

  • @Local支持null、undefined以及联合类型

    @Entry
    @ComponentV2
    struct Index {
      @Local count: number = 0;
      @Local message: string = 'Hello';
      @Local flag: boolean = false;
      build() {
        Column() {
          Text(`${this.count}`)
          Text(`${this.message}`)
          Text(`${this.flag}`)
          Button('change Local')
            .onClick(()=>{
              // 当@Local装饰简单类型时,能够观测到对变量的赋值
              this.count++;
              this.message += ' World';
              this.flag = !this.flag;
          })
        }
      }
    }
    

2. @Param装饰器

@Param表示组件从外部传入的状态,使得父子组件之间的数据能够进行同步

  • @Param装饰的变量支持本地初始化,但不允许在组件内部直接修改。
  • 被@Param装饰的变量能够在初始化自定义组件时从外部传入,当数据源也是状态变量时,数据源的修改会同步给@Param。
  • @Param可以接受任意类型的数据源,包括普通变量、状态变量、常量、函数返回值等。
  • @Param装饰的变量变化时,会刷新该变量关联的组件。
  • @Param支持对基本类型(如number、boolean、string、Object、class)、内嵌类型(如ArraySetMapDate),以及null、undefined和联合类型进行观测。
  • 对于复杂类型如类对象,@Param会接受数据源的引用。在组件内可以修改类对象中的属性,该修改会同步到数据源。
  • @Param的观测能力仅限于被装饰的变量本身。
@Entry
@ComponentV2
struct Index {
  @Local count: number = 0;
  @Local message: string = 'Hello';
  @Local flag: boolean = false;
  build() {
    Column() {
      Text(`Local ${this.count}`)
      Text(`Local ${this.message}`)
      Text(`Local ${this.flag}`)
      Button('change Local')
        .onClick(()=>{
          // 对数据源的更改会同步给子组件
          this.count++;
          this.message += ' World';
          this.flag = !this.flag;
      })
      Child({
        count: this.count,
        message: this.message,
        flag: this.flag
      })
    }
  }
}
@ComponentV2
struct Child {
  @Require @Param count: number;
  @Require @Param message: string;
  @Require @Param flag: boolean;
  build() {
    Column() {
      Text(`Param ${this.count}`)
      Text(`Param ${this.message}`)
      Text(`Param ${this.flag}`)
    }
  }
}

3. @Once装饰器

@Once装饰器在变量初始化时接受外部传入值进行初始化,后续数据源更改不会同步给子组件

  • @Once必须搭配@Param使用,单独使用或搭配其他装饰器使用都是不允许的。
  • @Once不影响@Param的观测能力,仅针对数据源的变化做拦截。
  • @Once与@Param装饰变量的先后顺序不影响使用功能。
  • @Once与@Param搭配使用时,可以在本地修改@Param变量的值。
@ComponentV2
struct ChildComponent {
  @Param @Once onceParam: string = '';
  build() {
      Column() {
        Text(`onceParam: ${this.onceParam}`)
      }
  }
}
@Entry
@ComponentV2
struct MyComponent {
  @Local message: string = 'Hello World';
  build() {
      Column() {
      Text(`Parent message: ${this.message}`)
      Button('change message')
        .onClick(() => {
          this.message = 'Hello Tomorrow';
        })
      ChildComponent({ onceParam: this.message })
      }
  }
}

4. @Event装饰器

由于@Param装饰的变量在本地无法更改,使用@Event装饰器装饰回调方法并调用,可以实现更新数据源的变量,再通过@Local的同步机制,将修改同步回@Param,以此达到主动更新@Param装饰变量的效果。

@Entry
@ComponentV2
struct Index {
  @Local title: string = 'Title One';
  @Local fontColor: Color = Color.Red;

  build() {
    Column() {
      Child({
        title: this.title,
        fontColor: this.fontColor,
        changeFactory: (type: number) => {
          if (type == 1) {
            this.title = 'Title One';
            this.fontColor = Color.Red;
          } else if (type == 2) {
            this.title = 'Title Two';
            this.fontColor = Color.Green;
          }
        }
      })
    }
  }
}

@ComponentV2
struct Child {
  @Param title: string = '';
  @Param fontColor: Color = Color.Black;
  @Event changeFactory: (x: number) => void = (x: number) => {};

  build() {
    Column() {
      Text(`${this.title}`)
        .fontColor(this.fontColor)
      Button('change to Title Two')
        .onClick(() => {
          this.changeFactory(2);
        })
      Button('change to Title One')
        .onClick(() => {
          this.changeFactory(1);
        })
    }
  }
}

5. @Monitor装饰器

@Monitor装饰器用于监听状态变量修改,使得状态变量具有深度监听的能力

@Entry
@ComponentV2
struct Index {
  @Local message: string = 'Hello World';
  @Local name: string = 'Tom';
  @Local age: number = 24;
  @Monitor('message', 'name')
  onStrChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      console.info(`${path} changed from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
    });
  }
  build() {
    Column() {
      Button('change string')
        .onClick(() => {
          this.message += '!';
          this.name = 'Jack';
      })
    }
  }
}

6.@ObservedV2装饰器和@Trace装饰器

@ObservedV2装饰器与@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力

@ObservedV2
class Son {
  @Trace age: number = 100;
}
class Father {
  son: Son = new Son();
}
@Entry
@ComponentV2
struct Index {
  father: Father = new Father();

  build() {
    Column() {
      // 当点击改变age时,Text组件会刷新
      Text(`${this.father.son.age}`)
        .onClick(() => {
          this.father.son.age++;
        })
    }
  }
}

###7.@Provider装饰器和@Consumer装饰器

@Provider,即数据提供方,其所有的子组件都可以通过@Consumer绑定相同的key来获取@Provider提供的数据。

@Consumer,即数据消费方,可以通过绑定同样的key获取其最近父节点的@Provider的数据,当查找不到@Provider的数据时,使用本地默认值

@Entry
@ComponentV2
struct Parent {
  @Provider() str: string = 'hello';

  build() {
    Column() {
      Button(this.str)
        .onClick(() => {
          this.str += '0';
        })
      Child()
    }
  }
}

@ComponentV2
struct Child {
  // @Consumer装饰的属性str和Parent组件中@Provider装饰的属性str名称相同,因此建立了双向绑定关系
  @Consumer() str: string = 'world';

  build() {
    Column() {
      Button(this.str)
        .onClick(() => {
          this.str += '0';
        })
    }
  }
}

###8.@Computed装饰器

@Computed为方法装饰器,装饰getter方法。@Computed会检测被计算的属性变化,当被计算的属性变化时,@Computed只会被求解一次。

@ComponentV2
struct Child {
  @Param double: number = 100;
  @Event $double: (val: number) => void;

  build() {
    Button('ChildChange')
      .onClick(() => {
        this.$double(200);
      })
  }
}

@Entry
@ComponentV2
struct Index {
  @Local count: number = 100;

  @Computed
  get double() {
    return this.count * 2;
  }

  // @Computed装饰的属性是只读的,开发者自己实现的setter不生效,且产生编译时报错
  set double(newValue : number) {
    this.count = newValue / 2;
  }

  build() {
    Scroll() {
      Column({ space: 3 }) {
        Text(`${this.count}`)
        // 错误写法,@Computed装饰的属性是只读的,无法与双向绑定连用。
        Child({ double: this.double!! })
      }
    }
  }
}

9. @Type装饰器

@Type标记类属性,使得类属性序列化时不丢失类型信息,便于类的反序列化。

class Sample {
  data: number = 0;
}
@ObservedV2
class Info {
  @Type(Sample)
  @Trace sample: Sample = new Sample(); // 正确用法
}
@Observed
class Info2 {
  @Type(Sample)
  sample: Sample = new Sample(); // 错误用法,不能用在@Observed装饰的类中,编译时报错
}
@ComponentV2
struct Index {
  @Type(Sample)
  sample: Sample = new Sample(); // 错误用法,不能用在自定义组件中
  build() {
  }
}

###10.@ReusableV2装饰器

@ReusableV2用于装饰V2的自定义组件,表明该自定义组件具有被复用的能力

@ReusableV2 // 装饰ComponentV2的自定义组件
@ComponentV2
struct ReusableV2Component {
  @Local message: string = 'Hello World';
  build () {
      Column() {
        Text(this.message)
      }
  }
}

四、V1和V2能力对比

V1装饰器名\场景V2装饰器名\API说明
@Observed@ObservedV2表明当前对象为可观察对象。但两者能力并不相同。@Observed可观察第一层的属性,需要搭配@ObjectLink使用才能生效。@ObservedV2本身无观察能力,仅代表当前class可被观察,如果要观察其属性,需要搭配@Trace使用。详情见@ObjectLink/@Observed/@Track迁移场景
@Track@TraceV1装饰器@Track为精确观察,不使用则无法做到类属性的精准观察。V2@Trace装饰的属性可以被精确跟踪观察。详情见@ObjectLink/@Observed/@Track迁移场景
@Component@ComponentV2@Component为搭配V1状态变量使用的自定义组件装饰器。@ComponentV2为搭配V2状态变量使用的自定义组件装饰器。
@State无外部初始化:@Local外部初始化一次:@Param@Once@State和@Local类似都是数据源的概念,在不需要外部传入初始化时,可直接迁移。如果需要外部传入初始化,则可以迁移为@Param@Once。详情见@State迁移场景
@Prop@Param@Prop和@Param类似都是自定义组件参数的概念。当输入参数为复杂类型时,@Prop为深拷贝,@Param为引用。详情见@Prop迁移场景
@Link@Param@Event@Link是框架自己封装实现的双向同步,对于V2开发者可以通过@Param@Event自己实现双向同步。详情见@Link迁移场景
@ObjectLink@ObservedV2@Trace直接兼容,@ObjectLink需要被@Observed装饰的class的实例初始化,主要应用于观察嵌套类场景。在状态管理V2中可以使用@ObservedV2@Trace。详情见@ObjectLink/@Observed/@Track迁移场景
@Provide@Provider兼容。详情见@Provide@Consume迁移场景
@Consume@Consumer兼容。详情见@Provide@Consume迁移场景
@Watch@Monitor@Watch用于监听V1状态变量的变化,具有监听状态变量本身和其第一层属性变化的能力。状态变量可观察到的变化会触发其@Watch监听事件。@Monitor用于监听V2状态变量的变化,搭配@Trace使用,可有深层监听的能力。状态变量在一次事件中多次变化时,仅会以最终的结果判断是否触发@Monitor监听事件。详情见@Watch迁移场景
重复计算@Computed状态管理V1无计算属性相关能力,状态管理V2可使用@Computed避免重复计算。详情见@Computed使用场景
LocalStorage@ObservedV2@Trace兼容。详情见LocalStorage迁移场景
AppStorageAppStorageV2兼容。详情见AppStorage迁移场景
Environment调用Ability接口获取系统环境变量Environment获取环境变量能力和AppStorage耦合。在V2中可直接调用Ability接口获取系统环境变量。详情见Environment迁移场景
PersistentStoragePersistenceV2PersistentStorage持久化能力和AppStorage耦合,PersistenceV2持久化能力可独立使用。详情见PersistentStorage迁移场景

五、V1V2混用使用

优先选 V2 的场景

  1. 新项目开发:直接采用 V2,享受更简洁的语法和更全的新特性支持

  2. 跨设备适配项目:V2 对手机、平板、智慧屏的适配更友好

  3. 复杂 UI 交互项目:V2 的组合式 API 和状态管理更适合处理复杂逻辑

  4. 追求开发效率的项目:V2 的代码量更少,上手后开发速度更快

继续用 V1 的场景

  1. 维护老项目:若项目已基于 V1 开发完成,无需强制迁移

  2. 团队技术栈适配:团队成员多为安卓 /iOS 转过来,对类继承模式更熟悉

  3. 简单 UI 组件开发:如纯展示类组件,V1 的写法更直观

混合使用的场景

  • 老项目新功能开发:新功能用 V2 实现,通过 “桥接组件” 与 V1 代码兼容
  • 组件库开发:核心组件用 V2 实现,提供 V1 的适配层供老项目使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值