目录
- @Entry装饰器
- @Component装饰器:自定义组件
- @Builder/@BuilderParam
- @Styles/@Extend
- @State装饰器:组件内状态
- @Prop和@Link
- @Provide和@Consume
- @Observed和@ObjectLink
- LocalStorage和AppStorage
- UI描述
学习UI开发,一般关注以下三个问题:
1)界面和控件如何编写;
2)状态管理和组件间的数据传递如何实现;
3)用户交互时,如何做到界面渲染控制。
在展开讲界面和控件编写前,本文先介绍两个UI相关的核心概念:装饰器、UI描述。
装饰器:
用于装饰类、结构、方法以及变量,并赋予其特殊的含义。
@Entry、@Component和@State都是装饰器
@Entry装饰器
@Entry表示该自定义组件为入口组件
@Entry 关键字装饰,表示当前组件是UI页面的入口;
@Component装饰器:自定义组件
@Component表示自定义组件,
@Builder/@BuilderParam
特殊的封装UI描述的方法,细粒度的封装和复用UI描述;
@Builder修饰的方法叫 自定义构建函数
@Builder修饰的方法方法传参有两种方式:值传递和引用传递
一、@Builder
class Tmp {
paramA1: string = ''
}
@Entry
@Component
struct Day3 {
// 状态变量
@State label: string = 'Hello';
// 值传递
@Builder
overBuilder(param: string) {
Row() {
Text(`值传递: ${param}`)
}
}
// 引用传递,需要是对象类型
// 只能传一个参数,传两个参数当数据改变时不会自定刷新UI
@Builder
overBuilder2(param:Tmp){
Row() {
Text(`引用传递: ${param.paramA1}`)
}
}
// 可以使用$$来表明这是一个引用传递
@Builder
overBuilder3($$:Tmp){
Row() {
Text(`引用传递: ${$$.paramA1}`)
}
}
build() {
Column({space:5}) {
Text(`Lable:${this.label}`)
Text('点击修改Label的值').onClick(()=>{
// 当label的值发生改变时,引用传递的自定义构建函数的UI会自动刷新
this.label = '你好'
})
.width(200).height(60)
.backgroundColor(Color.Orange)
.textAlign(TextAlign.Center)
Divider()
.width('100%').height(2)
.backgroundColor(Color.Red)
// 值传递的UI不会自动刷新
this.overBuilder(this.label)
// 引用传递的UI会自动刷新
this.overBuilder2({paramA1:this.label})
this.overBuilder3({paramA1:this.label})
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Start)
.padding(20)
}
}
二、@BuilderParam
BuilderParam允许在自定义组件外部传入UI逻辑,类似slot占位.
1、参数初始化组件
@Component
struct Child {
label: string = `Child`
@Builder customBuilder() {}
@Builder customChangeThisBuilder() {}
// 设置默认值
@BuilderParam customBuilderParam: () => void = this.customBuilder;
// 设置默认值
@BuilderParam customChangeThisBuilderParam: () => void = this.customChangeThisBuilder;
build() {
Column() {
this.customBuilderParam()
this.customChangeThisBuilderParam()
}
}
}
@Entry
@Component
struct Parent {
label: string = `Parent`
@Builder componentBuilder() {
Text(`${this.label}`)
}
build() {
Column() {
this.componentBuilder()
Child({ customBuilderParam: this.componentBuilder, customChangeThisBuilderParam: ():void=>{this.componentBuilder()} })
}
}
}
注意this指向,普通函数的this指向和箭头函数的this指向是不同的
2、尾随闭包初始化组件
此场景下自定义组件内有且仅有一个使用@BuilderParam装饰的属性。(如果有两个BuilderParam装饰的属性,会报错)
此场景下自定义组件不支持使用通用属性。
@Component
struct Child2 {
label: string = `Child`
@Builder customBuilder() {}
@BuilderParam customBuilderParam: () => void = this.customBuilder;
build() {
Column() {
this.customBuilderParam()
}
}
}
// 初始化
Child2(){
this.componentBuilder()
}
@Styles/@Extend
@Styles
@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。
通过@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式。
@Styles方法不支持参数
组件内@Styles的优先级高于全局@Styles。
// 定义在全局的@Styles封装的样式
// 全局要加function关键字
// 全局【只能在当前文件内使用,不支持export。】
@Styles function globalFancy () {
.width(150)
.height(100)
.backgroundColor(Color.Pink)
}
@Entry
@Component
struct FancyUse {
@State heightValue: number = 100
// 定义在组件内的@Styles封装的样式
@Styles fancy() {
.width(200)
.height(this.heightValue)
.backgroundColor(Color.Yellow)
.onClick(() => {
this.heightValue = 200
})
}
build() {
Column({ space: 10 }) {
// 使用全局的@Styles封装的样式
Text('FancyA')
.globalFancy()
.fontSize(30)
// 使用组件内的@Styles封装的样式
Text('FancyB')
.fancy()
.fontSize(30)
}
}
}
@Extend
扩展已有组件(三方或者内置组件)样式;
我们尝试将公用的属性设置抽离,@Extend将样式组合复用,示例如下。
@Extend(Text) function fancyText(weightValue: number, color: Color) {
.fontStyle(FontStyle.Italic)
.fontWeight(weightValue)
.backgroundColor(color)
}
@Entry
@Component
struct FancyUse {
@State label: string = 'Hello World'
build() {
Row({ space: 10 }) {
Text(`${this.label}`)
.fancyText(100, Color.Blue)
Text(`${this.label}`)
.fancyText(200, Color.Pink)
Text(`${this.label}`)
.fancyText(300, Color.Orange)
}.margin('20%')
}
}
可以看到,之前重复的fontStyle、fontWeight、backgroundColor都被fancyText一个属性替换了。
@State装饰器:组件内状态
一、基本用法
@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。
@State装饰器的作用范围仅仅在当前组件,当状态改变时,UI会发生对应的渲染改变。
只能从组件内部访问,在声明时必须指定其类型和本地初始化。
可以作为@prop和@Link的数据源
可以观察到哪些变化:
1、当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
2、当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性,无法观察到属性中的嵌套属性的变化。
3、当装饰的对象是array时,可以观察到数组本身的赋值和添加、删除、更新数组的变化。
二、本质
1、说明
左边是被@State修饰的person变量
右边是没有@State修饰的person变量
从图片可以看出被@State修饰的person被包装成一个proxy对象,绑定了这个对象,就能感知变化刷新UI
而没有被@State修饰的person只是一个普通的对象


2、在父子组件中
// 父组件中
struct PageView {
@State
person:Person = new Person('小明', 16)
build(){
Column(){
Text(`${this.person.name} | ${this.person.age}`)
PersonView({p:this.person})
}
}
}
// 子组件中
struct PersonView {
@State
p:Person = new Person('xx',0)
build(){
Column(){
Text(`${this.p.name} | ${this.p.age}`)
}
}
}
当父组件将person传递给子组件中的p时,父子组件中使用的是同一个proxy对象,所以当person对象的属性发生变化时,父子组件都会刷新。
当父组件重新赋值时,子组件是监听不到的。重新赋值后父组件person的属性发生变化,子组件也就监听不到了,因为已经不是同一proxy对象。
问题
当子组件不使用@State时,父组件中person属性发生变化,子组件UI是否刷新?
不刷新,虽然父子组件绑定的是同一个proxy对象,但子组件没有使用@State
子组件中p属性发生变化,父组件UI是否刷新?
会刷新,父组件使用的是@State,又因为父子组件绑定的是同一个proxy对象,且proxy对象的属性发生变化,会触发父组件的UI刷新
3、箭头函数
Person对象下有两个方法 changeName 和 changName2
export class Person {
name:string
age:number
constructor(name: string = '', age:number = 0) {
this.name = name
this.age = age
}
changeName = ()=>{
this.name = "塔罗"
}
changeName2(){
this.name = "塔罗2"
}
}
组件内代码如下
DayCell({title:"箭头函数"}){
Text(`${this.person.name} | ${this.person.age}`)
Row(){
// 没有触发UI刷新
DayButton({text:'修改属性(箭头函数)',clickEvent:()=>{
this.person.changeName()
}})
// 会触发UI刷新
DayButton({text:'修改属性(正常函数)',clickEvent:()=>{
this.person.changeName2()
}})
}
.width('100%').margin({top:10})
.justifyContent(FlexAlign.SpaceAround)
}
官方解释:
箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。
所以在该场景下, changeName中的this指向person对象,而不是被装饰器@State代理的状态变量(proxy对象)。
我们通过调试也会发现当断点到changeName方法内时,this是一个普通的person实例对象
当我们断点到changeName2时,this是一个proxy对象
箭头函数的 this 是固定的(定义时决定),普通方法的 this 是动态的(调用时决定)。
@Prop和@Link
一、@Props装饰器
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
1、当父组件中的数据源更改时,与之相关的@Prop装饰的变量都会自动更新。
2、@Prop变量允许在本地修改和初始化,但修改后的变化不会同步回父组件
3、@Prop装饰器不能在@Entry装饰的自定义组件中使用
4、虽然说@Props装饰的变量可由父组件初始化,但使用范围依旧只能在组件内访问。
二、@Link装饰器
@Link装饰器与@Props装饰器的用法几乎一样,区别只是@Props装饰器是父组件向子组件单向同步,而@Link装饰器是子组件和父组件双向同步。
我们平时使用@Link装饰器就和@Props差不多,有一个区别:@Link装饰器的属性不能本地初始化。
@Provide和@Consume
@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。
@Provide和@Consume之间是双向数据同步的
@Component
struct CompD {
// @Consume装饰的变量通过相同的属性名绑定其祖先组件CompA内的@Provide装饰的变量
@Consume reviewVotes: number;
build() {
Column() {
Text(`reviewVotes(${this.reviewVotes})`)
Button(`reviewVotes(${this.reviewVotes}), give +1`)
.onClick(() => this.reviewVotes += 1)
}
.width('50%')
}
}
@Component
struct CompC {
build() {
Row({ space: 5 }) {
CompD()
CompD()
}
}
}
@Component
struct CompB {
build() {
CompC()
}
}
@Entry
@Component
struct CompA {
// @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件
@Provide reviewVotes: number = 0;
build() {
Column() {
Button(`reviewVotes(${this.reviewVotes}), give +1`)
.onClick(() => this.reviewVotes += 1)
CompB()
}
}
}
@Observed和@ObjectLink
@Observed 用于装饰类
@ObjectLink 用于装饰子组件内变量,这个变量是@Observed装饰的类的实例
可以观察到嵌套类的属性的变化
// @Observed修饰类
@Observed
class ClassA {
public name: string;
constructor(name:string) {
this.name = name;
}
}
@Observed
class ClassB {
public a: ClassA;
constructor(a: ClassA) {
this.a = a;
}
}
@Component
struct ViewA {
// @ObjectLink 修饰使用@Observe修饰的类的实例
@ObjectLink a: ClassA
build() {
Column(){
Text(`ViewA内部显示:${this.a.name}`)
Text('ViewA内部修改')
.onClick(()=>{
this.a.name = 'helloA'
})
}
}
}
@Entry
struct TestPage {
@State b:ClassB = new ClassB(new ClassA('tom'))
build() {
Column(){
// 不会刷新,只能观察到this.b属性的变化
Text(`外部显示:${this.b.a.name}`)
// 会刷新
ViewA({a:this.b.a})
Text('修改')
.onClick(()=>{
this.b.a.name = 'jack';
})
}
}
}
LocalStorage和AppStorage
一、LocalStorage
UI描述
注意:
1、属性方法
2、容器组件的尾随闭包
lxy:尾随闭包本质是方法,尾随闭包内放置的子组件,就是子组件的构造方法调用
tips:这里推荐使用箭头函数来作为事件的执行回调函数。
如果使用 function 匿名函数,或者使用组件成员变量,需要bind(this)保证传入的回调函数与当前组件一致
// case1: 匿名function,需要bind(this)
Button('add counter')
.onClick(function(){
this.counter += 2;
}.bind(this))
// case2: 成员函数,需要.bind(this)
myClickHandler(): void {
this.counter += 2;
}
...
Button('add counter')
.onClick(this.myClickHandler.bind(this))
行者常至,为者常成!