也可以传入状态变量作为参数,当状态改变时,UI 可以正常刷新渲染
@Extend(Text) function fancy(fontSize: number) { .fontColor(Color.Red) .fontSize(fontSize) } @Entry @Component struct FancyUse { @State fontSizeValue: number = 20 build() { Row({ space: 10 }) { Text('Fancy') .fancy(this.fontSizeValue) .onClick(() => { this.fontSizeValue = 30 }) } } }
我们可以使用 @Extend 对下面的代码进行简化
@Entry @Component struct FancyUse { @State label: string = 'Hello World' build() { Row({ space: 10 }) { Text(`${this.label}`) .fontStyle(FontStyle.Italic) .fontWeight(100) .backgroundColor(Color.Blue) Text(`${this.label}`) .fontStyle(FontStyle.Italic) .fontWeight(200) .backgroundColor(Color.Pink) Text(`${this.label}`) .fontStyle(FontStyle.Italic) .fontWeight(300) .backgroundColor(Color.Orange) }.margin('20%') } }
首先使用 @Extend 抽离 Text 的样式
@Extend(Text) function fancy(w: number, color: Color) { .fontStyle(FontStyle.Italic) .fontWeight(w) .backgroundColor(color) }
然后在组件中使用他
@Entry @Component struct FancyUse { @State label: string = 'Hello World' build() { Row({ space: 10 }) { Text(`${this.label}`) .fancy(100, Color.Blue) Text(`${this.label}`) .fancy(200, Color.Pink) Text(`${this.label}`) .fancy(300, Color.Orange) }.margin('20%') } }
stateStyles
类似于 css 的伪类,可以设置组件在不同状态时的样式,arkUI 提供了如下四种状态
- normal 正常状态
- focused 获得焦点
- pressed 按下状态
- disabled 禁用状态
使用方式如下
@Entry @Component struct StateStylesSample { build() { Column() { Button('Click me') .stateStyles({ focused: { .backgroundColor(Color.Pink) }, pressed: { .backgroundColor(Color.Black) }, normal: { .backgroundColor(Color.Yellow) } }) }.margin('30%') } }
02自定义组件
自定义组件是逻辑复用的重要手段。最基本结构如下
@Component struct MyComponent { build() { Text('hello world!') } }
组件封装好之后,使用时只能用如下方式传参
MyComponent({ name: 'world' })
传入的参数中,key 值 name 会覆盖在组件内部定义的同名属性
@Component struct MyComponent { private name = 'china' build() { Text(`hello ${this.name}!`) } }
自定义组件的导出和引用,与 TS 模块的语法是一致的,这里不在扩展冗余介绍
03状态
和 React/Vue 一样,arkUI 也是基于数据驱动 UI 的核心思想来设计。不过 arkUI 中的数据状态非常不一样,它有更复杂的机制和逻辑
arkUI 中将会影响 UI 的数据称之为状态,他们常常需要特定的装饰器来声明
@State
先来实现一个经典的 count 案例
@Entry @Component struct MyComponent { @State private count: number = 0 build() { Column() { Text(`hello ${this.count}!`) Button('++++') .onClick(() => this.count++) } } }
@State 支持如下强类型的按值和按引用类型,及这些强类型构成的数组
- class 、 Array<class>
- number 、Array<number>
- boolean 、Array<boolean>
- string 、Array<string>
- object 、Array<object>
不支持 any,不支持简单类型和复杂类型的联合类型,不允许使用 undefined 和 null
建议不要装饰 Date 类型,应用可能会产生异常行为。不支持 Length、ResourceStr、ResourceColor 类型,Length、ResourceStr、ResourceColor 为简单类型和复杂类型的联合类型。
@State 装饰的属性只能在组件内部访问,子组件也不能访问
讲道理,规则有点多,用的时候再说吧,如果用错了,也会报提示,也不用刻意去记
这里需要特别注意的是,@State 只能观察监听到数据的浅层「第一层」。无法观测到更深层次的数据变化,因此层级结构复杂的数据类型的变化无法使用 @State 监听到完整的数据变化
嵌套类对象的属性变化需要使用 @Observed 与 @ObjectLink 来观测数据的变化,具体的使用我们后面介绍
@prop
如果我们将父组件中,@State 定义的状态传递给子组件,默认情况下,父组件只会将当前的值传递子组件用于初始化,后续父组件的变化则与子组件无关
例如我们定义这样一个子组件
@Component struct ChildComponent { private count: number build() { Text(`Child Count: ${this.count}}`) } }
然后再父组件中,将 @State count 传递给子组件
@Entry @Component struct MyComponent { @State private count: number = 0 build() { Column() { Text(`hello world ${this.count}!`) ChildComponent({ count: this.count }) Button('++++') .onClick(() => this.count++) } } }
当 count 发生变化时,子组件不会跟着变化。如果我们想要子组件的状态与父组件建立绑定关系,则可以在子组件中,使用 @Prop 装饰 count,这样一个单向的绑定关系就建立成功了
- 单向关系表现为:
- 父组件中修改 count,子组件会同步更新
- 子组件中修改 count,父组件不会有反应
- 子组件更新后,父组件再更新,子组件中的状态会被父组件最新的值覆盖
因此,在子组件中,给 count 字段添加一个 @Prop 装饰即可
@Component struct ChildComponent { @Prop private count: number build() { Text(`Child Count: ${this.count}}`) } }
当作为子组件时,@Prop 可以被父组件中的其他任意装饰器状态初始化。
当作为父组件时,@Prop 可以初始化子组件的常规变量、@State、@Link、@Prop、Provide
@Prop 装饰的变量是私有的,只能在组件内部访问
@Link
如果你想要和子组件建立双向绑定的关系,则需要使用 @Link
- 双向关系表现为:
- 父组件中修改 count,子组件会同步更新
- 子组件中修改 count,父组件会同步更新
- 子组件不能初始化,只能接收父组件的参数初始化
- 父组件必须以按引用传递的方式传参
子组件代码,使用 @Link 装饰状态
@Component struct ChildComponent { @Link private count: number build() { Column() { Text(`Child Count: ${this.count}}`) Button('ChildCount') .onClick(() => this.count++) } } }
父组件代码,按引用传参
@Entry @Component struct MyComponent { @State private count: number = 0 build() { Column() { Text(`hello world ${this.count}!`) ChildComponent({ count: $count }) Button('++++').onClick(() => this.count++) } } }
其实学习到这里,我已经逐渐有点裂开了。这规则也太多了吧 ~ 别急,还有一点,@Link 只能与父组件的 @State Link StorageLink 建立双向绑定关系
@Provide 与 @Consume
类似于 React 中的 context,用于跨组件层级传递参数。其中 @Provide 作用于约定范围内的根节点,@Consume 作用于后代组件中,他们之间的关系是双向绑定
这两个知识点的使用反而简单,看如下案例即可
@Entry @Component struct MyComponent { @Provide private count: number = 0 build() { Column() { Text(`hello world ${this.count}!`) ChildComponent({ count: $count }) Button('++++') .onClick(() => this.count++) } } } @Component struct ChildComponent { @Link private count: number build() { Column() { Text(`Child Count: ${this.count}}`) D() } } } @Component struct D { @Consume private count: number build() { Button('我在深层子组件') .onClick(() => this.count++) } }
@Observed 与 @ObjectLink
上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed @ObjectLink装饰器
对他们使用主要步骤如下
- 父组件中,使用 @Observed 装饰的 class 对象初始化 @State 变量
- 子组件中,使用 @ObjectLink 接收父组件传递过来的参数
示例如下,首先使用 @Observed 定义复杂数据结构的对象
// objectLinkNestedObjects.ets let NextID: number = 1; @Observed class ClassA { public id: number; public c: number; constructor(c: number) { this.id = NextID++; this.c = c; } } @Observed class ClassB { public a: ClassA; constructor(a: ClassA) { this.a = a; } }
然后在父组件中,使用刚才定义的复杂对象初始化 @State
@State b: ClassB = new ClassB(new ClassA(0));
然后在子组件中,使用 @ObjectLink 接收参数
@Component struct ViewA { label: string = 'ViewA1'; @ObjectLink a: ClassA; build() { Row() { Button(`ViewA [${this.label}] this.a.c=${this.a.c} +1`) .onClick(() => { this.a.c += 1; }) } } }
父组件完整代码如下,包括状态初始化,参数传递
@Entry @Component struct ViewB { @State b: ClassB = new ClassB(new ClassA(0)); build() { Column() { ViewA({ label: 'ViewA #1', a: this.b.a }) ViewA({ label: 'ViewA #2', a: this.b.a }) Button(`ViewB: this.b.a.c+= 1`) .onClick(() => { this.b.a.c += 1; }) Button(`ViewB: this.b.a = new ClassA(0)`) .onClick(() => { this.b.a = new ClassA(0); }) Button(`ViewB: this.b = new ClassB(ClassA(0))`) .onClick(() => { this.b = new ClassB(new ClassA(0)); }) } } }
04总结
学习相关内容只用了一天,但是写这篇文章就用了三天时间 ~ ~ 因为官方文档的内容有点零散,关于自定义组件的内容分布在了几个不同的地方,因此为了确保每一个表达的准确性,反复翻阅文档和写代码验证花费了不少时间,真不是一个轻松的过程
不过写这篇文章本身也是一个总结的过程,让我对自定义组件的相关内容有了更深刻的理解。整体感受下来就是 arkUI 对于状态的区分更为细化,因此在实践中要结合具体情况选择合适的状态,就不得不对这些状态的基本情况有比较详细的了解
除了能够熟练使用之外,官方文档对于内部逻辑的运行机制都分别做了介绍,这可以作为一个进阶内容在后续的过程中学习,不过如果你理解 React 和 Vue 的底层原理的话,大概也能猜到他是如何实现的
虽然我学得挺快的,不过可以预想,对于零基础的同学来说,arkUI 的学习成本非常高,要掌握的概念和细节很多,写这篇文章我感觉自己都被干冒烟了,本来预计还有很大一部分内容要写,放到下一篇文章里来说吧