作用

我们在之前学习的那些控件中,各有特点,也各有缺陷,至今没有痛痛快快的出现过真正能跨组件的双向绑定的装饰器。

比如

  • @Local装饰器,不能跨组件
  • @Param装饰器呢,能跨组件传递,但是仅仅就是下一层组件接收参数。另外,它是单向传递,不可被重新赋值。如果您非要改值则用@Once修饰,改了父组件也不会同步。非要达成双向传递的效果,那就搞个@Event写回调函数,让父组件实现,完成改值的能力。比较麻烦。

我们有时候需要一种能力是,对于组件而言。爷爷组件直接传给孙子组件,父组件不需要某状态变量。并且孙子组件如果改了值,或者爷爷组件改了值,双向同步给彼此。从而是界面刷新逻辑正常。

这个@Provider和@Consumer装饰器的配合使用,就是来完成这种效果的。

使用方式

介绍

@Provider需要与@Consumer配合使用。两者通过一个key建立联结。此key表现在,这两个装饰器都接收一个叫aliasName的参数,用来指定其修饰的变量的别名。之后靠这个别名来寻找绑定关系。

@Provider属性装饰器

说明

装饰器参数

aliasName?:string, 别名,缺省时默认为属性名,建议最好写这个参数

支持类型

自定义组件中成员变量。属性类型可以为number,string, boolean, class, Array, Date, Map, Set 等类型。支持装饰箭头函数。

从父组件初始化

禁止

本地初始化

必须本地初始化

观察能力

能力等同于@Trace。变化同步给对应的@Consumer

@Consumer属性装饰器

说明

装饰器参数

aliasName?:string, 别名,缺省时默认为属性名,向上查找最近的@Provider,建议最好写这个参数

可装饰的变量

自定义组件中的成员变量。属性的类型可以是number,string,boolean,Data,Array,Map,Set等类型,支持箭头函数。

从父组件初始化

禁止

本地初始化

必须本地初始化

观察能力

能力等同于@Trace。变化会同步给对应的@Provider。

上述提到的Data,Array,Map,Set等类型,可以观测到的是API变化,具体如下:

类型

可观测变化的API

Array

push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort

Date

setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds

Map

set, clear, delete

Set

add, clear, delete

双向绑定使用案例

简单使用场景:

@Entry
  @ComponentV2
  struct ProviderConsumerTest {
    @Provider() str: string = 'hello'
    build() {
      Column() {
        Button(this.str)
          .onClick(()=>{
            this.str += 0
          })

        Child2()
      }
      .height('100%')
        .width('100%')
    }
  }

@ComponentV2
  struct Child2{
    @Consumer() str:string = "world" //没写参数,则意味着要找父组件中叫 str 的变量
    build() {
      Column(){
        Button(this.str)
          .onClick(()=>{
            this.str += 0
          })
      }
    }
  }

展示界面:

注意

关于aliasName设置上会出现的问题

上方表格里提到了aliasName,我们在代码里,仔细看的话,发现没有填这个aliasName。 因为他们都是有个问号,也就是其实用户在使用的时候可以填入也可以不填入。那么现实中很可能出现

  • Provider填了,Consume没填
  • Provider没填,Consumer没填
  • Provider填了,Consumer填了
  • Provider没填,Consumer填了

看起来挺乱的,我们之前讲过,这两个是靠这个aliasName寻找来绑定的。寻找规则为

  • 两者都是,装饰器修饰的时候没有传入aliasName,那么全部按照其变量名当做aliasName,寻找彼此。
  • 上方代码是父组件变量名是str,子组件也是str, 这样两个寻找的时候都按照str这个名字找的,能找得到。但是如果您但凡把父控件改成str1,或者其他的名字。或者子组件也改了名字,照这样写代码,肯定会出现找不到彼此这种问题,从而引发逻辑错误。这样双向绑定就错了!
  • 综上所述,为维护代码的健壮性和稳定性,建议用Provider和Consumer的时候加上aliasName!您要是不写,别人在维护您代码的时候改了个变量名,就会引起不必要的bug,浪费时间。

下方是当父组件与子组件变量名称不一样的时候的最佳写法

太多同名Provider(aliasName)时,Consumer的查找逻辑

@Provider在组件树上是可以重名的,@Consumer会向上查找其最近父节点的@Provider数据。

案例:

@Entry
@ComponentV2
struct ProviderConsumerTest {
  @Provider() val: number = 10;

  build() {
    Column() {
      Parent()
    }
    .backgroundColor(Color.Orange)
    .width('100%')
    .height('50%')
  }
}

@ComponentV2
struct Parent {
  @Provider() val: number = 20;
  @Consumer("val") val2: number = 0; //用ProviderConsumerTest中的 10

  build() {
    Column() {
      Text(`${this.val2}`)
      Child()
    }
    .backgroundColor(Color.Pink)
  }
}

@ComponentV2
struct Child {
  @Consumer() val: number = 0; //用Parent组件中的 20

  build() {
    Column() {
      Text(`${this.val}`)
    }
    .backgroundColor(Color.Yellow)
  }
}

上面的例子中:

  • Parent中@Consumer向上查找,查找到ProviderConsumerTest中定义的 @Provider() val: number = 10,所以初始化为10。
  • Child中@Consumer向上查找,查找到Parent中定义的@Provider() val: number = 20后停止,不会继续向上查找,所以初始化为20。

使用场景

@Provider与@Consumer修饰回调事件

这两个装饰器也能修饰回调事件。如果您看了之前的文章的话,可能会回想起来@Event不是也是修饰回调的么?文章链接: 25041103-ArkUI V2装饰器-@Event:规范组件输出

但是这里面有一个比较本质的区别!就是,@Provider与@Consumer是可以跨组件传递的。而且@Consumer修饰的子组件,在使用的时候根本不用配置@Consumer修饰的状态变量为什么。 我们可以想一个场景,就是爷爷组件,直接给孙子组件,跨过父组件。@Event是做不到的, 它得由爷爷传给父亲,再由父亲传给孙子!这是这里面比较重要的区别。 @Provider与@Consumer最最最重要的就是跨组件中的”跨“!

代码:

@Entry
@ComponentV2
struct ProviderConsumerTest{
  @Local fontColor: Color = Color.Red
  @Provider() changeColor: (color: Color) => void = (color: Color) => {
    this.fontColor = color
  }
  build() {
    Column(){
      Text(`颜色`)
        .fontColor(this.fontColor)
      Child3()
    }
    .width('100%')
    .height('100%')
  }
}

@ComponentV2
struct Child3 {
  @Consumer() changeColor: (color: Color) => void = (color: Color) => {};
  build() {
    Button("changed")
      .onClick(()=>{
        this.changeColor(Color.Green);
      })

  }
}

@Provider和@Consumer装饰复杂类型,配合@Trace一起使用

@Provider和@Consumer只能观察到数据本身的变化。对于类结构上的深层观测能力是不具备的。所以要借助@ObservedV2和@Trace一起使用。

@ObservedV2
class User {
  @Trace name: string;
  @Trace age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
const data: User[] = [new User('Json', 10), new User('Eric', 15)];
@Entry
@ComponentV2
struct Parent {
  @Provider('data') users: User[] = data;

  build() {
    Column() {
      Child()
      Button('add new user')
        .onClick(() => {
          this.users.push(new User('Molly', 18));
        })
      Button('age++')
        .onClick(() => {
          this.users[0].age++;
        })
      Button('change name')
        .onClick(() => {
          this.users[0].name = 'Shelly';
        })
    }
  }
}

@ComponentV2
struct Child {
  @Consumer('data') users: User[] = [];

  build() {
    Column() {
      ForEach(this.users, (item: User) => {
        Column() {
          Text(`name: ${item.name}`).fontSize(30)
          Text(`age: ${item.age}`).fontSize(30)
          Divider()
        }
      })
    }
  }
}

@Provider和@Consumer初始化@Param 传递链路分析

Provider修饰的状态变量是可以传值给Param的。

@Entry
@ComponentV2
struct ProviderConsumerTest{
  @Provider() val: number = 10
  build() {
    Column(){
      Parent({valParam: this.val})
    }
  }
}

@ComponentV2
struct Parent{
  @Consumer() val: number = 0 //接收ProviderConsumerTest组件的 val
  @Param valParam: number = 0
  build() {
    Column(){
      Text(`Parent @Consumer val: ${this.val}`).fontSize(30).onClick(() => {
        this.val++;
      })
      Text(`Parent @Param val2: ${this.valParam}`).fontSize(30)
      Child4({ val: this.val })
    }
  }
}

@ComponentV2
struct Child4{
  @Param val:number = 0
  build() {
    Column() {
      Text(`Child @Param val ${this.val}`).fontSize(30)
    }
    .border({ width: 2, color: Color.Pink })
  }
}

上述代码中的状态变量变化传递链路:

使用限制

  • @Provider和@Consumer属于状态管理V2装饰器,所以只能在@ComponentV2组件中才能使用。
  • @Provider和@Consumer是自定义组件装饰器,只能用在自定义组件struct里,而class中不可以。
  • @Provider和@Consumer仅支持本地初始化,不支持外部组件传入。因为这俩装饰器已经使值跨组件透传了,不需要再自己专门写属性以初始化的形式传。

Logo

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

更多推荐