未验证 提交 247f0ef8 编写于 作者: O openharmony_ci 提交者: Gitee

!10386 eTS语言

Merge pull request !10386 from 田雨/master
...@@ -5,8 +5,19 @@ ...@@ -5,8 +5,19 @@
- [使用eTS语言开发(Stage模型)](start-with-ets-stage.md) - [使用eTS语言开发(Stage模型)](start-with-ets-stage.md)
- [使用eTS语言开发(FA模型)](start-with-ets-fa.md) - [使用eTS语言开发(FA模型)](start-with-ets-fa.md)
- [使用JS语言开发(FA模型)](start-with-js-fa.md) - [使用JS语言开发(FA模型)](start-with-js-fa.md)
- 开发基础知识 - 开发基础知识
- [应用包结构说明(FA模型)](package-structure.md) - [应用包结构说明(FA模型)](package-structure.md)
- [应用包结构说明(Stage模型)](stage-structure.md) - [应用包结构说明(Stage模型)](stage-structure.md)
- [初始ets语言](ets-language.md)
- eTS语法(声明式UI)
- [基本UI描述](ets-ui.md)
- 状态管理
- [基本概念](ets-state-management-concepts.md)
- [页面级变量的状态管理](ets-managing-component-states.md)
- [应用级变量的状态管理](ets-managing-application-states.md)
- [动态构建UI元素](ets-dynamically-build-ui-elememts.md)
- [渲染控制](ets-syntactic-usage.md)
- [使用限制与扩展](ets-usage-restrictions-and-extensions.md)
- [SysCap说明](syscap.md) - [SysCap说明](syscap.md)
- [HarmonyAppProvision配置文件](app-provision-structure.md) - [HarmonyAppProvision配置文件](app-provision-structure.md)
\ No newline at end of file
# 动态构建UI元素
前面章节介绍的是如何创建一个组件内部UI结构固定的自定义组件,为了满足开发者自定义组件内部UI结构的需求,eTS同时提供了动态构建UI元素的能力。
## @Builder
可通过@Builder装饰器进行描述,该装饰器可以修饰一个函数,此函数可以在build函数之外声明,并在build函数中或其他@Builder修饰的函数中使用,在一个自定义组件内快速生成多个布局内容。使用方式如下面示例所示。
```ts
// xxx.ets
@Component
struct CompB {
@State CompValue: string = '';
aboutToAppear() {
console.info('CompB aboutToAppear.');
}
aboutToDisappear() {
console.info('CompB aboutToDisappear.');
}
build() {
Column() {
Button(this.CompValue);
}
}
}
@Entry
@Component
struct CompA {
size1: number = 100;
@State CompValue1: string = "Hello,CompValue1";
@State CompValue2: string = "Hello,CompValue2";
@State CompValue3: string = "Hello,CompValue3";
// @Builder装饰的函数内使用自定义组件
@Builder CompC(value: string) {
CompB({ CompValue: value });
}
@Builder SquareText(label: string) {
Text(label)
.width(1 * this.size1)
.height(1 * this.size1)
}
@Builder RowOfSquareTexts(label1: string, label2: string) {
Row() {
this.SquareText(label1)
this.SquareText(label2)
}
.width(2 * this.size1)
.height(1 * this.size1)
}
build() {
Column() {
Row() {
this.SquareText("A")
this.SquareText("B")
// or as long as tsc is used
}
.width(2 * this.size1)
.height(1 * this.size1)
this.RowOfSquareTexts("C", "D")
Column() {
// 使用三次@Builder装饰的自定义组件
this.CompC(this.CompValue1);
this.CompC(this.CompValue2);
this.CompC(this.CompValue3);
}
.width(2 * this.size1)
.height(2 * this.size1)
}
.width(2 * this.size1)
.height(2 * this.size1)
}
}
```
## @BuilderParam<sup>8+<sup>
@BuilderParam装饰器用于修饰自定义组件内函数类型的属性(例如:`@BuilderParam content: () => any;`),并且在初始化自定义组件时被@BuilderParam修饰的属性必须赋值。
### 引入动机
当开发者创建自定义组件,想对该组件添加特定功能时(如:仅对自定义组件添加一个点击跳转操作)。若直接在组件内嵌入事件方法,将会导致所有初始化该组件的地方均增加了该功能。为解决此问题,引入了@BuilderParam装饰器,此装饰器修饰的属性值可为@Builder修饰的方法,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的的功能。
### 参数初始化组件
通过参数初始化组件时,将@Builder装饰的方法赋值给@BuilderParam修饰的属性,并在自定义组件内调用content属性值。对@BuilderParam修饰的属性进行赋值时不带参数(如:`content: this.specificParam`),则此属性的类型需定义成无返回值的函数(如:`@BuilderParam content: () => void`)。若带参数(如:`callContent: this.specificParam1("111")`),则此属性的类型需定义成any(如:`@BuilderParam callContent: any;`)。
```ts
// xxx.ets
@Component
struct CustomContainer {
header: string = "";
@BuilderParam noParam: () => void;
@BuilderParam withParam: any;
footer: string = "";
build() {
Column() {
Text(this.header)
.fontSize(50)
this.noParam()
this.withParam()
Text(this.footer)
.fontSize(50)
}
}
}
@Entry
@Component
struct CustomContainerUser {
@Builder specificNoParam() {
Column() {
Text("noParam").fontSize(50)
}
}
@Builder SpecificWithParam(label: string) {
Column() {
Text(label).fontSize(50)
}
}
build() {
Column() {
CustomContainer({
header: "Header",
noParam: this.specificNoParam,
withParam: this.SpecificWithParam("WithParam"),
footer: "Footer",
})
}
}
}
```
### 尾随闭包初始化组件
在自定义组件中使用@BuilderParam修饰的属性接收尾随闭包(在初始化自定义组件时,组件名称紧跟一个大括号“{}”形成尾随闭包场景(`CustomComponent(){}`)。开发者可把尾随闭包看做一个容器,向其填充内容,如在闭包内增加组件(`{Column(){Text("content")}`),闭包内语法规范与build函数一致。此场景下自定义组件内有且仅有一个使用@BuilderParam修饰的属性。
示例:在闭包内增加Column组件并添加点击事件,在新增的Column组件内调用@Builder修饰的specificParam方法,点击Column组件后该改变自定义组件中header的属性值为“changeHeader”。并且在初始化自定义组件时会把尾随闭包的内容赋值给使用@BuilderParam修饰的closer属性。
```ts
// xxx.ets
@Component
struct CustomContainer {
header: string = "";
@BuilderParam closer: () => void;
build() {
Column() {
Text(this.header)
.fontSize(50)
this.closer()
}
}
}
@Builder function specificParam(label1: string, label2: string) {
Column() {
Text(label1)
.fontSize(50)
Text(label2)
.fontSize(50)
}
}
@Entry
@Component
struct CustomContainerUser {
@State text: string = "header"
build() {
Column() {
CustomContainer({
header: this.text,
}){
Column(){
specificParam("111", "22")
}.onClick(()=>{
this.text = "changeHeader"
})
}
}
}
}
```
## @Styles
eTS为了避免开发者对重复样式的设置,通过@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置使用。@Styles装饰器将新的属性函数添加到基本组件上,如Text、Column、Button等。当前@Styles仅支持通用属性。通过@Styles装饰器可以快速定义并复用组件的自定义样式。
@Styles可以定义在组件内或组件外,在组件外定义时需在方法前添加function关键字,组件内定义时不需要添加function关键字。
```ts
// xxx.ets
@Styles function globalFancy() {
.backgroundColor(Color.Red)
}
@Entry
@Component
struct FancyUse {
@Styles componentFancy() {
.backgroundColor(Color.Blue)
}
build() {
Column({ space: 10 }) {
Text("Fancy")
.globalFancy()
.width(100)
.height(100)
.fontSize(30)
Text("Fancy")
.componentFancy()
.width(100)
.height(100)
.fontSize(30)
}
}
}
```
@Styles还可以在[StateStyles](../reference/arkui-ts/ts-universal-attributes-polymorphic-style.md)属性内部使用,在组件处于不同的状态时赋予相应的属性。
在StateStyles内可以直接调用组件外定义的Styles,但需要通过this关键字调用组件内定义的Styles。
```ts
// xxx.ets
@Styles function globalFancy() {
.width(100)
.height(100)
}
@Entry
@Component
struct FancyUse {
@Styles componentFancy() {
.width(50)
.height(50)
}
build() {
Row({ space: 10 }) {
Button() {
Text("Fancy")
}
.stateStyles({
normal: {
.width(80)
.height(80)
},
disabled: this.componentFancy,
pressed: globalFancy
})
}
}
}
```
## @Extend
@Extend装饰器将新的属性函数添加到内置组件上,如Text、Column、Button等。通过@Extend装饰器可以快速的扩展原生组件。@Extend装饰器不能用在自定义组件struct定义框内。
```ts
// xxx.ets
@Extend(Text) function fancy(fontSize: number) {
.fontColor(Color.Red)
.fontSize(fontSize)
.fontStyle(FontStyle.Italic)
}
@Entry
@Component
struct FancyUse {
build() {
Row({ space: 10 }) {
Text("Fancy")
.fancy(16)
Text("Fancy")
.fancy(24)
}
}
}
```
> **说明:**
>
> - @Extend装饰器不能用在自定义组件struct定义框内。
> - @Extend装饰器内仅支持属性函数语句。
## @CustomDialog
@CustomDialog装饰器用于装饰自定义弹窗,使得弹窗可以动态设置样式。
```ts
// custom-dialog-demo.ets
@CustomDialog
struct DialogExample {
controller: CustomDialogController;
action: () => void;
build() {
Row() {
Button ("Close CustomDialog")
.onClick(() => {
this.controller.close();
this.action();
})
}.padding(20)
}
}
@Entry
@Component
struct CustomDialogUser {
dialogController : CustomDialogController = new CustomDialogController({
builder: DialogExample({action: this.onAccept}),
cancel: this.existApp,
autoCancel: true
});
onAccept() {
console.log("onAccept");
}
existApp() {
console.log("Cancel dialog!");
}
build() {
Column() {
Button("Click to open Dialog")
.onClick(() => {
this.dialogController.open()
})
}
}
}
```
![custom-dialog-demo](figures/custom-dialog-demo.gif)
\ No newline at end of file
# 初始eTs语言
eTS(extended TypeScript)是OpenHarmony优选的主力应用开发语言。eTS基于TypeScript(简称TS)语言扩展而来,是TS的超集。
- eTS继承了TS的所有特性。
- 当前,eTS在ts基础上主要扩展了[声明式UI](ets-ui.md)能力,让开发者以更简洁、更自然的方式开发高性能应用。
当前扩展的声明式UI包括如下特性。
- [基本UI描述](ets-state-management-concepts.md):eTS定义了各种装饰器、自定义组件、UI描述机制,再配合UI开发框架中的UI内置组件、事件方法、属性方法等共同构成了UI开发的主体。
- [状态管理](ets-managing-component-states.md):eTS提供了多维度的状态管理机制,在UI开发框架中,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,也可以是全局范围内的传递,还可以是 跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和UI的联动。
- [动态构建UI元素](ets-dynamically-build-ui-elememts.md):eTS提供了动态构建UI元素的能力,不仅可以自定义组件内部的UI结构,还可复用组件样式,扩展原生组件。
- [渲染控制](ets-syntactic-usage.md):eTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的部分内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。
- [使用限制与扩展](ets-usage-restrictions-and-extensions.md):eTS在使用过程中存在限制与约束,同时也扩展了双向绑定等能力。
- 未来,eTS会结合应用开发/运行的需求持续演进,逐步提供并行和并发能力增强、类型系统增强、分布式开发范式等更多特性。
下面我们以一个具体的示例来说明eTS的基本组成。如下图所示的代码示例,UI界面会两段文本和一个按钮,当开发者点击按钮时,文本内容会从'Hello World'变为 'Hello ArkUI'。
这个示例中所包含的eTS声明式开发范式的基本组成说明如下:
- 装饰器: 用于装饰类、结构、方法以及变量,赋予其特殊的含义,如上述示例中@Entry、@Component和@State都是装饰器。 具体而言,@Component表示这是个自定义组件;@Entry则表示这是个入口组件;@State表示组件中的状态变量,这个状态变换会引起UI变更。
- 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。
- UI描述:声明式的方法来描述UI的结构,例如build()方法中的代码块。
- 内置组件:eTS中默认内置的基本组件和布局组件,开发者可以直接调用,如Column、Text、Divider、Button等。
- 属性方法:用于组件属性的配置,如fontSize()、width()、height()、color()等,可通过链式调用的方式设置多项属性。
- 事件方法:用于添加组件对事件的响应逻辑,统一通过事件方法进行设置,如跟随在Button后面的onClick()。
\ No newline at end of file
# 页面级变量的状态管理
@State、@Prop、@Link、@Provide、Consume、@ObjectLink、@Observed和@Watch用于管理页面级变量的状态。
## @State
@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。
@State状态数据具有以下特征:
- 支持多种类型:允许class、number、boolean、string强类型的按值和按引用类型。允许这些强类型构成的数组,即Array&lt;class&gt;、Array&lt;string&gt;、Array&lt;boolean&gt;、Array&lt;number&gt;。不允许object和any。
- 支持多实例:组件不同实例的内部状态数据独立。
- 内部私有:标记为@State的属性是私有变量,只能在组件内访问。
- 需要本地初始化:必须为所有@State变量分配初始值,将变量保持未初始化可能导致框架行为未定义。
- 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定@State状态属性的初始值。
**示例:**
在下面的示例中:
- 用户定义的组件MyComponent定义了@State状态变量count和title。如果count或title的值发生变化,则执行MyComponent的build方法来重新渲染组件;
- EntryComponent中有多个MyComponent组件实例,第一个MyComponent内部状态的更改不会影响第二个MyComponent;
- 创建MyComponent实例时通过变量名给组件内的变量进行初始化,如:
```ts
MyComponent({title: {value: 'Hello, World 2'}, count: 7})
```
```ts
// xxx.ets
class Model {
value: string
constructor(value: string) {
this.value = value
}
}
@Entry
@Component
struct EntryComponent {
build() {
Column() {
MyComponent({count: 1, increaseBy: 2}) // MyComponent1 in this document
MyComponent({title: {value: 'Hello, World 2'}, count: 7}) //MyComponent2 in this document
}
}
}
@Component
struct MyComponent {
@State title: Model = {value: 'Hello World'}
@State count: number = 0
private toggle: string = 'Hello World'
private increaseBy: number = 1
build() {
Column() {
Text(`${this.title.value}`).fontSize(30)
Button() {
Text(`Click to change title`).fontSize(20).fontColor(Color.White)
}.onClick(() => {
this.title.value = (this.toggle == this.title.value) ? 'Hello World' : 'Hello UI'
}) // Modify the internal state of MyComponent using the anonymous method.
Button() {
Text(`Click to increase count=${this.count}`).fontSize(20).fontColor(Color.White)
}.onClick(() => {
this.count += this.increaseBy
}) // Modify the internal state of MyComponent using the anonymous method.
}
}
}
```
![@state1](figures/@state.png)
## @Prop
@Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但更改不会通知给父组件,即@Prop属于单向数据绑定。
@Prop状态数据具有以下特征:
- 支持简单类型:仅支持number、string、boolean简单类型;
- 私有:仅在组件内访问;
- 支持多个实例:一个组件中可以定义多个标有@Prop的属性;
- 创建自定义组件时将值传递给@Prop变量进行初始化:在创建组件的新实例时,必须初始化所有@Prop变量,不支持在组件内部进行初始化。
**示例:**
在下面的示例中,当按“+1”或“-1”按钮时,父组件状态发生变化,重新执行build方法,此时将创建一个新的CountDownComponent组件。父组件的countDownStartValue状态属性被用于初始化子组件的@Prop变量,当按下子组件的“Try again”按钮时,其@Prop变量count将被更改,CountDownComponent重新渲染。但是count值的更改不会影响父组件的countDownStartValue值。
```ts
// xxx.ets
@Entry
@Component
struct ParentComponent {
@State countDownStartValue: number = 10 // 10 Nuggets default start value in a Game
build() {
Column() {
Text(`Grant ${this.countDownStartValue} nuggets to play.`)
Button() {
Text('+1 - Nuggets in New Game')
}.onClick(() => {
this.countDownStartValue += 1
})
Button() {
Text('-1 - Nuggets in New Game')
}.onClick(() => {
this.countDownStartValue -= 1
})
// 创建子组件时,必须在构造函数参数中提供其@Prop变量的初始值,同时初始化常规变量CostOfOneAttump(非Prop)
CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2})
}
}
}
@Component
struct CountDownComponent {
@Prop count: number
private costOfOneAttempt: number
build() {
Column() {
if (this.count > 0) {
Text(`You have ${this.count} Nuggets left`)
} else {
Text('Game over!')
}
Button() {
Text('Try again')
}.onClick(() => {
this.count -= this.costOfOneAttempt
})
}
}
}
```
> **说明:** 创建新组件实例时,必须初始化其所有@Prop变量。
## @Link
@Link装饰的变量可以和父组件的@State变量建立双向数据绑定:
- 支持多种类型:@Link变量的值与@State变量的类型相同,即class、number、string、boolean或这些类型的数组;
- 私有:仅在组件内访问;
- 单个数据源:初始化@Link变量的父组件的变量必须是@State变量;
- 双向通信:子组件对@Link变量的更改将同步修改父组件的@State变量;
- 创建自定义组件时需要将变量的引用传递给@Link变量,在创建组件的新实例时,必须使用命名参数初始化所有@Link变量。@Link变量可以使用@State变量或@Link变量的引用进行初始化,@State变量可以通过`'$'`操作符创建引用。
> 说明: @Link变量不能在组件内部进行初始化
**简单类型示例:**
@Link语义是从`'$'`操作符引出,即`$isPlaying``this.isPlaying`内部状态的双向数据绑定。当单击PlayButton时,PlayButton的Image组件和Text组件将同时进行刷新。
```ts
// xxx.ets
@Entry
@Component
struct Player {
@State isPlaying: boolean = false
build() {
Column() {
PlayButton({buttonPlaying: $isPlaying})
Text(`Player is ${this.isPlaying? '':'not'} playing`)
}
}
}
@Component
struct PlayButton {
@Link buttonPlaying: boolean
build() {
Column() {
Button() {
Image(this.buttonPlaying? 'play.png' : 'pause.png')
}.onClick(() => {
this.buttonPlaying = !this.buttonPlaying
})
}
}
}
```
**复杂类型示例:**
```ts
// xxx.ets
@Entry
@Component
struct Parent {
@State arr: number[] = [1, 2, 3]
build() {
Column() {
Child({items: $arr})
ForEach(this.arr,
item => Text(`${item}`),
item => item.toString())
}
}
}
@Component
struct Child {
@Link items: number[]
build() {
Column() {
Button() {
Text('Button1: push')
}.onClick(() => {
this.items.push(100)
})
Button() {
Text('Button2: replace whole item')
}.onClick(() => {
this.items = [100, 200, 300]
})
}
}
}
```
## @Link、@State和@Prop结合使用示例
下面示例中,ParentView包含ChildA和ChildB两个子组件,ParentView的状态变量counter分别初始化ChildA和ChildB。
- ChildB使用@Link建立双向状态绑定。当ChildB修改counterRef状态变量值时,该更改将同步到ParentView和ChildA共享;
- ChildA使用@Prop建立从ParentView到自身的单向状态绑定。当ChildA修改状态时,ChildA将重新渲染,但该更改不会传达给ParentView和ChildB。
```ts
// xxx.ets
@Entry
@Component
struct ParentView {
@State counter: number = 0
build() {
Column() {
ChildA({counterVal: this.counter}) // pass by value
ChildB({counterRef: $counter}) // $ creates a Reference that can be bound to counterRef
}
}
}
@Component
struct ChildA {
@Prop counterVal: number
build() {
Button() {
Text(`ChildA: (${this.counterVal}) + 1`)
}.onClick(() => {this.counterVal+= 1})
}
}
@Component
struct ChildB {
@Link counterRef: number
build() {
Button() {
Text(`ChildB: (${this.counterRef}) + 1`)
}.onClick(() => {this.counterRef+= 1})
}
}
```
## Observed和ObjectLink数据管理
当开发者需要在子组件中针对父组件的一个变量(parent_a)设置双向同步时,开发者可以在父组件中使用\@State装饰变量(parent_a),并在子组件中使用@Link装饰相应的变量(child_a)。这样的话,不仅可以实现父组件与单个子组件之间的数据同步,也可以实现父组件与多个子组件之间的数据同步。如下图所示,可以看到,父子组件针对ClassA类型的变量设置了双向同步,那么当子组件1中变量的属性c的值变化时,会通知父组件同步变化,而当父组件中属性c的值变化时,会通知所有子组件同步变化。
![zh-cn_image_0000001251090821](figures/zh-cn_image_0000001251090821.png)
然而,上述例子是针对某个数据对象进行的整体同步,而当开发者只想针对父组件中某个数据对象的部分信息进行同步时,使用@Link就不能满足要求。如果这些部分信息是一个类对象,就可以使用@ObjectLink配合@Observed来实现,如下图所示。
![zh-cn_image_0000001206450834](figures/zh-cn_image_0000001206450834.png)
### 设置要求
- @Observed 用于类,@ObjectLink 用于变量。
- @ObjectLink装饰的变量类型必须为类(class type)。
- 类要被\@Observed装饰器所装饰。
- 不支持简单类型参数,可以使用@Prop进行单向同步。
- @ObjectLink装饰的变量是不可变的(immutable)。
- 属性的改动是被允许的,当改动发生时,如果同一个对象被多个@ObjectLink变量所引用,那么所有拥有这些变量的自定义组件都会被通知去重新渲染。
- @ObjectLink装饰的变量不可设置默认值。
- 必须让父组件中有一个由@State、@Link、@StorageLink、@Provide或@Consume所装饰变量参与的TS表达式进行初始化。
- @ObjectLink装饰的变量是私有变量,只能在组件内访问。
### 示例
```ts
// xxx.ets
// 父组件ViewB中的类对象ClassA与子组件ViewA保持数据同步时,可以使用@ObjectLink和@Observed,绑定该数据对象的父组件和其他子组件同步更新
var nextID: number = 0;
@Observed
class ClassA {
public name : string;
public c: number;
public id : number;
constructor(c: number, name: string = 'OK') {
this.name = name;
this.c = c;
this.id = nextID++;
}
}
@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;
})
}.margin({ top: 10 })
}
}
@Entry
@Component
struct ViewB {
@State arrA : ClassA[] = [ new ClassA(0), new ClassA(0) ];
build() {
Column() {
ForEach (this.arrA, (item) => {
ViewA({label: `#${item.id}`, a: item})
},
(item) => item.id.toString()
)
ViewA({label: `ViewA this.arrA[first]`, a: this.arrA[0]})
ViewA({label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1]})
Button(`ViewB: reset array`)
.margin({ top: 10 })
.onClick(() => {
this.arrA = [ new ClassA(0), new ClassA(0) ];
})
Button(`ViewB: push`)
.margin({ top: 10 })
.onClick(() => {
this.arrA.push(new ClassA(0))
})
Button(`ViewB: shift`)
.margin({ top: 10 })
.onClick(() => {
this.arrA.shift()
})
}
}
}
```
![Observed](figures/Observed.gif)
## @Consume和@Provide
Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。Consume在感知到Provide数据的更新后,会触发当前view的重新渲染。
> **说明:** 使用@Provide 和@Consume时避免循环引用导致死循环。
表1 @Provide
| 名称 | 说明 |
| -------------- | ------------------------------------------------------------ |
| 装饰器参数 | 是一个string类型的常量,用于给装饰的变量起别名。如果规定别名,则提供对应别名的数据更新。如果没有,则使用变量名作为别名。推荐使用@Provide("alias")这种形式。 |
| 同步机制 | @Provide的变量类似@State,可以修改对应变量进行页面重新渲染。也可以修改@Consume装饰的变量,反向修改@State变量。 |
| 初始值 | 必须制定初始值。 |
| 页面重渲染场景 | 触发页面渲染的修改: <br/>- 基础类型(boolean,string,number)变量的改变; <br/>- @Observed class类型变量及其属性的修改; <br/>- 添加,删除,更新数组中的元素。 |
表2 @Consume
| 名称 | 说明 |
| -------------- | ------------------------------------------------------------ |
| 装饰器参数 | 是一个string类型的常量,用于给装饰的变量起别名。如果规定别名,则提供对应别名的数据更新。如果没有,则使用变量名作为别名。推荐使用@Provide("alias")这种形式。 |
| 同步机制 | @Provide的变量类似@State,可以修改对应变量进行页面重新渲染。也可以修改@Consume装饰的变量,反向修改@State变量。 |
| 初始值 | 不可设置默认值。 |
| 页面重渲染场景 | 触发页面渲染的修改: <br/>- 基础类型(boolean,string,number)变量的改变; <br/>- @Observed class类型变量及其属性的修改; <br/>- 添加,删除,更新数组中的元素。 |
### 示例
```ts
// xxx.ets
@Entry
@Component
struct CompA {
@Provide("reviewVote") reviewVotes : number = 0;
build() {
Column() {
CompB()
Button() {
Text(`${this.reviewVotes}`)
.fontSize(30)
}
.onClick(() => {
this.reviewVotes += 1;
})
}
}
}
@Component
struct CompB {
build() {
Column() {
CompC()
}
}
}
@Component
struct CompC {
@Consume("reviewVote") reviewVotes : number;
build() {
Column() {
Button() {
Text(`${this.reviewVotes}`)
.fontSize(30)
}
.onClick(() => {
this.reviewVotes += 1;
})
}.margin({ left:10, top: 10 })
}
}
```
## @Watch
@Watch用于监听状态变量的变化,语法结构为:
```
@State @Watch("onChanged") count : number = 0
```
如上给状态变量增加一个@Watch装饰器,通过@Watch注册一个回调方法onChanged, 当状态变量count被改变时, 触发onChanged回调。
装饰器@State、@Prop、@Link、@ObjectLink、@Provide、@Consume、@StorageProp以及@StorageLink装饰的变量可以监听其变化。
```
// xxx.ets
@Entry
@Component
struct CompA {
@State @Watch("onBasketUpdated") shopBasket : Array<number> = [ 7, 12, 47, 3 ];
@State totalPurchase : number = 0;
updateTotal() : number {
let sum = 0;
this.shopBasket.forEach((i) => { sum += i; });
// 计算新的购物篮总价值,如果超过100RMB,则适用折扣
this.totalPurchase = (sum < 100) ? sum : 0.9 * sum;
return this.totalPurchase;
}
// @Watch cb
onBasketUpdated(propName: string) : void {
this.updateTotal();
}
build() {
Column() {
Button("add to basket").onClick(() => { this.shopBasket.push(Math.round(100 * Math.random())) })
Text(`${this.totalPurchase}`)
.fontSize(30)
}
}
}
```
\ No newline at end of file
# 基本概念
eTS提供了多维度的状态管理机制,在UI开发框架中,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间、爷孙组件之前,也可以时全局范围内的传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和UI的联动。
![](figures/CoreSpec_figures_state-mgmt-overview.png)
## 页面级变量的状态管理
| 装饰器 | 装饰内容 | 说明 |
| ----------- | ------------------------- | ------------------------------------------------------------ |
| @State | 基本数据类型,类,数组 | 修饰的状态数据被修改时会触发组件的build方法进行UI界面更新。 |
| @Prop | 基本数据类型 | 修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,更新当前组件的UI。 |
| @Link | 基本数据类型,类,数组 | 父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。 |
| @Observed | 类 | @Observed应用于类,表示该类中的数据变更被UI页面管理。 |
| @ObjectLink | 被@Observed所装饰类的对象 | 装饰的状态数据被修改时,在父组件或者其他兄弟组件内与它关联的状态数据所在的组件都会更新UI。 |
| @Consume | 基本数据类型,类,数组 | @Consume装饰的变量在感知到@Provide装饰的变量更新后,会触发当前自定义组件的重新渲染。 |
| @Provide | 基本数据类型,类,数组 | @Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。 |
## 应用级变量的状态管理
AppStorage是整个UI应用程序状态的中心“数据库”,UI框架会针对应用程序创建单例AppStorage对象,并提供相应的装饰器和接口供应用程序使用。
- @StorageLink:@StorageLink(name)的原理类似于@Consume(name),不同的是,该给定名称的链接对象是从AppStorage中获得的,在UI组件和AppStorage之间建立双向绑定同步数据。
- @StorageProp:@StorageProp(name)将UI组件属性与AppStorage进行单向同步,AppStorage中值的更改会更新组件中的属性,但UI组件无法更改AppStorage中的属性值。
- AppStorage还提供用于业务逻辑实现的API,用于添加、读取、修改和删除应用程序的状态属性,此API所做的更改会导致修改的状态数据同步到UI组件上进行UI更新。
- PersistentStorage类提供了一些静态方法用来管理应用持久化数据,可以将特定标记的持久化数据链接到AppStorage中,并由AppStorage接口访问对应持久化数据,或者通过@StorageLink装饰器来访问对应key的变量。
- Environment是框架在应用程序启动时创建的单例对象,它为AppStorage提供了一系列应用程序需要的环境状态属性,这些属性描述了应用程序运行的设备环境。
# 数据懒加载 # 渲染控制
开发框架提供数据懒加载(LazyForEach组件)从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。LazyForEach定义如下: eTS也提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的部分内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。
## 条件渲染
```ts 使用if/else进行条件渲染。
LazyForEach(
dataSource: IDataSource, // Data source to be iterated
itemGenerator: (item: any) => void, // child component generator
keyGenerator?: (item: any) => string // (optional) Unique key generator, which is recommended.
): void
interface IDataSource {
totalCount(): number; // Get total count of data
getData(index: number): any; // Get single data by index
registerDataChangeListener(listener: DataChangeListener): void; // Register listener to listening data changes
unregisterDataChangeListener(listener: DataChangeListener): void; // Unregister listener
}
interface DataChangeListener { > **说明:**
onDataReloaded(): void; // Called while data reloaded >
onDataAdd(index: number): void; // Called while single data added > - if条件语句可以使用状态变量。
onDataMove(from: number, to: number): void; // Called while single data moved >
onDataDelete(index: number): void; // Called while single data deleted > - 使用if可以使子组件的渲染依赖条件语句。
onDataChange(index: number): void; // Called while single data changed >
> - 必须在容器组件内使用。
>
> - 某些容器组件限制子组件的类型或数量。将if放置在这些组件内时,这些限制将应用于if和else语句内创建的组件。例如,Grid组件的子组件仅支持GridItem组件,在Grid组件内使用if时,则if条件语句内仅允许使用GridItem组件。
```ts
Column() {
if (this.count < 0) {
Text('count is negative')
} else if (this.count % 2 === 0) {
Divider()
Text('even')
} else {
Divider()
Text('odd')
}
} }
``` ```
## 循环渲染
## 接口 开发框架提供循环渲染(ForEach组件)来迭代数组,并为每个数组项创建相应的组件。当循环渲染的元素较多时,会出现页面加载变慢的情况,出于性能考虑,建议使用[LazyForEach](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/ts-rending-control-syntax-lazyforeach.md)代替。ForEach定义如下:
```
ForEach(
arr: any[], // Array to be iterated
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
```
### LazyForEach **参数:**
LazyForEach(dataSource: IDataSource, itemGenerator: (item: any) =&gt; void, keyGenerator?: (item: any) =&gt; string):void | 参数名 | 参数类型 | 必填 | 参数描述 |
| ------------- | ------------------------------------- | ---- | ------------------------------------------------------------ |
| arr | any[] | 是 | 必须是数组,允许空数组,空数组场景下不会创建子组件。同时允许设置返回值为数组类型的函数,例如arr.slice(1, 3),设置的函数不得改变包括数组本身在内的任何状态变量,如Array.splice、Array.sort或Array.reverse这些改变原数组的函数。 |
| itemGenerator | (item: any, index?: number) => void | 是 | 生成子组件的lambda函数,为给定数组项生成一个或多个子组件,单个组件和子组件列表必须括在大括号“{....}”中。 |
| keyGenerator | (item: any, index?: number) => string | 否 | 匿名参数,用于给定数组项生成唯一且稳定的键值。当子项在数组中的位置更改时,子项的键值不得更改,当数组中的子项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则ForEach中的所有节点都将重建。 |
表1 参数说明 > **说明:**
>
> - ForEach组件必须在容器组件内使用;
>
> - 生成的子组件允许在ForEach的父容器组件中,允许子组件生成器函数中包含if/else条件渲染,同时也允许ForEach包含在if/else条件渲染语句中;
>
> - 子项生成器函数的调用顺序不一定和数组中的数据项相同,在开发过程中不要假设子项生成器和键值生成器函数是否执行以及执行顺序。如下示例可能无法正常工作:
>
> ```
> ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }),
> item => Text(`${item.i}. item.data.label`),
> ```
| 参数名 | 参数类型 | 必填 | 默认值 | 参数描述 | ## 示例
| ------------- | --------------------------------------- | ---- | ---- | ---------------------------------------- |
| dataSource | IDataSource | 是 | - | 实现IDataSource接口的对象,需要开发者实现相关接口。 |
| itemGenerator | (item:&nbsp;any)&nbsp;=&gt;&nbsp;void | 是 | - | 生成子组件的lambda函数,为给定数组项生成一个或多个子组件,单个组件和子组件列表必须括在大括号“{....}”中。 |
| keyGenerator | (item:&nbsp;any)&nbsp;=&gt;&nbsp;string | 否 | - | 匿名函数,用于键值生成,为给定数组项生成唯一且稳定的键值。当子项在数组中的位置更改时,子项的键值不得更改,当数组中的子项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。 |
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
@State arr: number[] = [10, 20, 30]
表2 IDataSource类型说明 build() {
Column() {
Button() {
Text('Reverse Array')
}.onClick(() => {
this.arr.reverse()
})
| 名称 | 描述 | ForEach(this.arr,
| ---------------------------------------- | ----------- | (item: number) => {
| totalCount():&nbsp;number | 获取数据总数。 | Text(`item value: ${item}`)
| getData(index:&nbsp;number):&nbsp;any | 获取索引对应的数据。 | Divider()
| registerDataChangeListener(listener:DataChangeListener):&nbsp;void | 注册改变数据的控制器。 | },
| unregisterDataChangeListener(listener:DataChangeListener):&nbsp;void | 注销改变数据的控制器。 | (item: number) => item.toString()
)
}
}
}
```
## 数据懒加载
表3 DataChangeListener类型说明 通过数据懒加载(LazyForEach组件)从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
| 名称 | 描述 | ```ts
| ---------------------------------------- | ---------------------- | LazyForEach(
| onDataReloaded():&nbsp;void | 重新加载所有数据。 | dataSource: IDataSource,
| onDataAdded(index:&nbsp;number):&nbsp;void <sup>(deprecated) </sup> | 通知组件index的位置有数据添加。 | itemGenerator: (item: any) => void,
| onDataMoved(from:&nbsp;number,&nbsp;to:&nbsp;number):&nbsp;void <sup>(deprecated) </sup> | 通知组件数据从from的位置移到to的位置。 | keyGenerator?: (item: any) => string
| onDataDeleted(index:&nbsp;number):&nbsp;void <sup>(deprecated) </sup> | 通知组件index的位置有数据删除。 | ): void
| onDataChanged(index:&nbsp;number):&nbsp;void <sup>(deprecated) </sup> | 通知组件index的位置有数据变化。 |
| onDataAdd(index:&nbsp;number):&nbsp;void <sup>8+</sup> | 通知组件index的位置有数据添加。 |
| onDataMove(from:&nbsp;number,&nbsp;to:&nbsp;number):&nbsp;void <sup>8+</sup> | 通知组件数据从from的位置移到to的位置。 |
| onDataDelete(index:&nbsp;number):&nbsp;void <sup>8+</sup> | 通知组件index的位置有数据删除。 |
| onDataChange(index:&nbsp;number):&nbsp;void <sup>8+</sup> | 通知组件index的位置有数据变化。 |
interface IDataSource {
totalCount(): number;
getData(index: number): any;
registerDataChangeListener(listener: DataChangeListener): void;
unregisterDataChangeListener(listener: DataChangeListener): void;
}
> **说明:** interface DataChangeListener {
> - 数据懒加载必须在容器组件内使用,且仅有List、Grid以及Swiper组件支持数据的懒加载(即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次加载所有的数据; onDataReloaded(): void;
> onDataAdd(index: number): void;
> - LazyForEach在每次迭代中,必须且只允许创建一个子组件; onDataMove(from: number, to: number): void;
> onDataDelete(index: number): void;
> - 生成的子组件必须允许在LazyForEach的父容器组件中; onDataChange(index: number): void;
> }
> - 允许LazyForEach包含在if/else条件渲染语句中,不允许LazyForEach中出现if/else条件渲染语句; ```
>
> - 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,仅itemGenerator中的UI描述的组件内使用了状态变量时,才会触发组件刷新; **参数:**
>
> - 子项生成器函数的调用顺序不一定和数据源中的数据项相同,在开发过程中不要假设子项生成器和键值生成器函数是否执行以及执行顺序。如下示例可能无法正常工作: | 参数名 | 参数类型 | 必填 | 参数描述 |
> ```ts | ------------- | --------------------- | ---- | ------------------------------------------------------------ |
> LazyForEach(dataSource, item => {Text(`${++counter}. item.label`)}) | dataSource | IDataSource | 是 | 实现IDataSource接口的对象,需要开发者实现相关接口。 |
> ``` | itemGenerator | (item: any) => void | 是 | 生成子组件的lambda函数,为给定数组项生成一个或多个子组件,单个组件和子组件列表必须括在大括号“{....}”中。 |
> | keyGenerator | (item: any) => string | 否 | 匿名函数,用于键值生成,为给定数组项生成唯一且稳定的键值。当子项在数组中的位置更改时,子项的键值不得更改,当数组中的子项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。 |
> 正确的示例如下:
> 表2 IDataSource类型说明
> ```ts
> LazyForEach(dataSource,
> item => Text(`${item.i}. item.data.label`)),
> item => item.data.id.toString())
> ```
| 名称 | 描述 |
| ------------------------------------------------------------ | ---------------------- |
| totalCount(): number | 获取数据总数。 |
| getData(index: number): any | 获取索引对应的数据。 |
| registerDataChangeListener(listener:DataChangeListener): void | 注册改变数据的控制器。 |
| unregisterDataChangeListener(listener:DataChangeListener): void | 注销改变数据的控制器。 |
表3 DataChangeListener类型说明
| 名称 | 描述 |
| -------------------------------------------------------- | -------------------------------------- |
| onDataReloaded(): void | 重新加载所有数据。 |
| onDataAdded(index: number): void (deprecated) | 通知组件index的位置有数据添加。 |
| onDataMoved(from: number, to: number): void (deprecated) | 通知组件数据从from的位置移到to的位置。 |
| onDataDeleted(index: number): void (deprecated) | 通知组件index的位置有数据删除。 |
| onDataChanged(index: number): void (deprecated) | 通知组件index的位置有数据变化。 |
| onDataAdd(index: number): void 8+ | 通知组件index的位置有数据添加。 |
| onDataMove(from: number, to: number): void 8+ | 通知组件数据从from的位置移到to的位置。 |
| onDataDelete(index: number): void 8+ | 通知组件index的位置有数据删除。 |
| onDataChange(index: number): void 8+ | 通知组件index的位置有数据变化。 |
## 示例 ## 示例
```ts ```ts
// Basic implementation of IDataSource to handle data listener
class BasicDataSource implements IDataSource { class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [] private listeners: DataChangeListener[] = []
...@@ -189,3 +247,23 @@ struct MyComponent { ...@@ -189,3 +247,23 @@ struct MyComponent {
} }
} }
``` ```
> **说明:**
>
> - 数据懒加载必须在容器组件内使用,且仅有List、Grid以及Swiper组件支持数据的懒加载(即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次加载所有的数据;
>
> - LazyForEach在每次迭代中,必须且只允许创建一个子组件;
>
> - 生成的子组件必须允许在LazyForEach的父容器组件中;
>
> - 允许LazyForEach包含在if/else条件渲染语句中,不允许LazyForEach中出现if/else条件渲染语句;
>
> - 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,仅itemGenerator中的UI描述的组件内使用了状态变量时,才会触发组件刷新;
>
> - 子项生成器函数的调用顺序不一定和数据源中的数据项相同,在开发过程中不要假设子项生成器和键值生成器函数是否执行以及执行顺序。如下示例可能无法正常工作:
>
> ```ts
> LazyForEach(dataSource,
> item => Text(`${item.i}. item.data.label`)),
> item => item.data.id.toString())
> ```
\ No newline at end of file
# 基本UI描述
eTS通过装饰器@Component和@Entry装饰struct关键字声明的数据结构,构成一个自定义组件。自定义组件中提供了一个build函数,开发者需在该函数内以链式调用的方式进行基本的UI描述,UI描述的方法请参考[UI描述规范](#ui描述规范)
## 基本概念
- struct:组件可以基于struct实现,不能有继承关系,对于struct的实例化,可以省略new。
- 装饰器:装饰器给被装饰的对象赋予某一种能力,其不仅可以装饰类或结构体,还可以装饰类的属性。多个装饰器可以叠加到目标元素,定义在同一行上或者多行上,推荐定义在多行上。
```ts
@Entry
@Component
struct MyComponent {
}
```
- build函数:自定义组件必须定义build函数,并且自定义组件禁止自定义构造函数。build函数满足Builder构造器接口定义,用于定义组件的声明式UI描述。
```ts
interface Builder {
build: () => void
}
```
- @Component:装饰struct,结构体在装饰后具有基于组件的能力,需要实现build方法来更新UI。
- @Entry: 装饰struct,组件被装饰后作为页面的入口,页面加载时将被渲染显示。
- @Preview:装饰struct, 用@Preview装饰的自定义组件可以在DevEco Studio的预览器上进行实时预览,加载页面时,将创建并呈现@Preview装饰的自定义组件。
> **说明:** 在单个源文件中,最多可以使用10个@Preview装饰自定义组件,更多说明请参考[查看eTS组件预览效果](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ohos-previewing-app-service-0000001218760596#section146052489820)。
- 链式调用:以 "." 链式调用的方式配置UI结构及其属性、事件等。
## UI描述规范
### 无参数构造配置
组件的接口定义不包含必选构造参数,组件后面的“()”中不需要配置任何内容。例如,Divider组件不包含构造参数:
```ts
Column() {
Text('item 1')
Divider()
Text('item 2')
}
```
### 必选参数构造配置
如果组件的接口定义中包含必选构造参数,则在组件后面的“()”中必须配置参数,参数可以使用常量进行赋值。
例如:
- Image组件的必选参数src:
```ts
Image('http://xyz/a.jpg')
```
- Text组件的必选参数content:
```ts
Text('123')
```
变量或表达式也可以用于参数赋值,其中表达式返回的结果类型必须满足参数类型要求。例如,传递变量或表达式来构造Image和Text组件的参数:
```ts
Image(this.imagePath)
Image('http://' + this.imageUrl)
Text(`count: ${this.count}`)
```
### 属性配置
使用属性方法配置组件的属性,属性方法紧随组件,并用"."运算符连接。
- 配置Text组件的字体大小属性:
```ts
Text('123')
.fontSize(12)
```
- 使用"."操作进行链式调用并同时配置组件的多个属性,如下所示:
```ts
Image('a.jpg')
.alt('error.jpg')
.width(100)
.height(100)
```
- 除了直接传递常量参数外,还可以传递变量或表达式,如下所示:
```ts
Text('hello')
.fontSize(this.size)
Image('a.jpg')
.width(this.count % 2 === 0 ? 100 : 200)
.height(this.offset + 100)
```
- 对于系统内置组件,框架还为其属性预定义了一些[枚举类型](../reference/arkui-ts/ts-appendix-enums.md)供开发人员调用,枚举类型必须满足参数类型要求,枚举值可以作为参数传递。可以按以下方式配置Text组件的颜色和字体属性:
```ts
Text('hello')
.fontSize(20)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
```
### 事件配置
通过事件方法可以配置组件支持的事件。
- 使用lambda表达式配置组件的事件方法:
```ts
Button('add counter')
.onClick(() => {
this.counter += 2
})
```
- 使用匿名函数表达式配置组件的事件方法,要求使用bind,以确保函数体中的this引用包含的组件:
```ts
Button('add counter')
.onClick(function () {
this.counter += 2
}.bind(this))
```
- 使用组件的成员函数配置组件的事件方法:
```ts
myClickHandler(): void {
}
...
Button('add counter')
.onClick(this.myClickHandler)
```
### 子组件配置
对于支持子组件配置的组件,例如容器组件,在"{ ... }"里为组件添加子组件的UI描述。Column、Row、Stack、Button、Grid和List组件都是容器组件。
- 以下是简单的Column示例:
```ts
Column() {
Text('Hello')
.fontSize(100)
Divider()
Text(this.myText)
.fontSize(100)
.fontColor(Color.Red)
}
```
- 可以嵌套多个子组件:
```ts
Column() {
Column() {
Button() {
Text('+ 1')
}.type(ButtonType.Capsule)
.onClick(() => console.log ('+1 clicked!'))
Image('1.jpg')
}
Divider()
Column() {
Button() {
Text('+ 2')
}.type(ButtonType.Capsule)
.onClick(() => console.log ('+2 clicked!'))
Image('2.jpg')
}
Divider()
Column() {
Button() {
Text('+ 3')
}.type(ButtonType.Capsule)
.onClick(() => console.log('+3 clicked!'))
Image('3.jpg')
}
}.alignItems(HorizontalAlign.Center)
```
\ No newline at end of file
# 使用限制与扩展
## 在生成器函数中的使用限制
eTS语言的使用在生成器函数中存在一定的限制:
- 表达式仅允许在字符串(${expression})、if条件、ForEach的参数和组件的参数中使用;
- 任何表达式都不能导致任何应用程序状态变量(@State、@Link、@Prop)的改变,否则会导致未定义和潜在不稳定的框架行为;
- 生成器函数内部不能有局部变量。
上述限制都不适用于事件处理函数(例如onClick)的匿函数实现。
## 变量的双向绑定
eTS支持通过$$双向绑定变量,通常应用于状态值频繁改变的变量。
- 当前$$支持基础类型变量,以及@State、@Link和@Prop装饰的变量。
- 当前$$仅支持[bindPopup](../reference/arkui-ts/ts-universal-attributes-popup.md)属性的show参数和@State变量之间的渲染,Radio组件的checked属性。
- $$绑定的变量变更时,仅渲染当前组件,提高渲染速度。
```ts
// xxx.ets
@Entry
@Component
struct bindPopup {
@State customPopup: boolean = false
build() {
column() {
button() {
Text('Popup')
}
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup(
$$this.customPopup, {
mesage: "showPopup"
}
)
}
}
}
```
## 状态变量多种数据类型声明使用限制
@State、@Provide、 @Link和@Consume四种状态变量的多种数据类型只能同时由简单数据类型或引用数据类型其中一种构成。
示例:
```ts
// xxx.ets
@Entry
@Component
struct Index {
//错误写法: @State message: string | Resource = 'Hello World'
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(`${ this.message }`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
```
\ No newline at end of file
...@@ -29,19 +29,20 @@ ...@@ -29,19 +29,20 @@
```js ```js
// xxx.js // xxx.js
export default { export default {
onShow(){ onShow() {
const el =this.$refs.canvas; const el = this.$refs.canvas;
var ctx =el.getContext('2d'); var ctx = el.getContext('2d');
var img = new Image(); var img = new Image();
img.src = 'common/images/example.jpg'; // 图片路径建议放在common目录下
img.onload = function() { img.src = 'common/images/example.jpg';
console.log('Image load success'); img.onload = function () {
ctx.drawImage(img, 0, 0, 360, 250); console.log('Image load success');
}; ctx.drawImage(img, 0, 0, 360, 250);
img.onerror = function() { };
console.log('Image load fail'); img.onerror = function () {
}; console.log('Image load fail');
} };
}
} }
``` ```
......
# UI开发 # UI开发
- [方舟开发框架(ArkUI)概述](arkui-overview.md) - [方舟开发框架(ArkUI)概述](arkui-overview.md)
- UI开发(基于eTS的声明式开发范式) - UI开发(基于eTS的声明式开发范式)
- [概述](ui-ts-overview.md) - [概述](ui-ts-overview.md)
- 框架说明 - 框架说明
...@@ -12,48 +13,10 @@ ...@@ -12,48 +13,10 @@
- [资源访问](ts-resource-access.md) - [资源访问](ts-resource-access.md)
- [像素单位](ts-pixel-units.md) - [像素单位](ts-pixel-units.md)
- 声明式语法 - 声明式语法
- [描述规范使用说明](ts-syntax-intro.md)
- 通用UI描述规范
- [基本概念](ts-general-ui-concepts.md)
- 声明式UI描述规范
- [无构造参数配置](ts-parameterless-configuration.md)
- [必选参数构造配置](ts-configuration-with-mandatory-parameters.md)
- [属性配置](ts-attribution-configuration.md)
- [事件配置](ts-event-configuration.md)
- [子组件配置](ts-child-component-configuration.md)
- 组件化
- [@Component](ts-component-based-component.md)
- [@Entry](ts-component-based-entry.md)
- [@Preview](ts-component-based-preview.md)
- [@Builder](ts-component-based-builder.md)
- [@Extend](ts-component-based-extend.md)
- [@CustomDialog](ts-component-based-customdialog.md)
- [@Styles](ts-component-based-styles.md)
- UI状态管理
- [基本概念](ts-ui-state-mgmt-concepts.md)
- 管理组件拥有的状态
- [@State](ts-component-states-state.md)
- [@Prop](ts-component-states-prop.md)
- [@Link](ts-component-states-link.md)
- 管理应用程序的状态
- [应用程序的数据存储](ts-application-states-appstorage.md)
- [Ability数据存储](ui-ts-local-storage.md)
- [持久化数据管理](ts-application-states-apis-persistentstorage.md)
- [环境变量](ts-application-states-apis-environment.md)
- 其他类目的状态管理
- [Observed和ObjectLink数据管理](ts-other-states-observed-objectlink.md)
- [@Consume和@Provide数据管理](ts-other-states-consume-provide.md)
- [@Watch](ts-other-states-watch.md)
- 渲染控制语法
- [条件渲染](ts-rending-control-syntax-if-else.md)
- [循环渲染](ts-rending-control-syntax-foreach.md)
- [数据懒加载](ts-rending-control-syntax-lazyforeach.md)
- 深入理解组件化 - 深入理解组件化
- [build函数](ts-function-build.md)
- [自定义组件初始化](ts-custom-component-initialization.md) - [自定义组件初始化](ts-custom-component-initialization.md)
- [自定义组件生命周期回调函数](ts-custom-component-lifecycle-callbacks.md) - [自定义组件生命周期回调函数](ts-custom-component-lifecycle-callbacks.md)
- [组件创建和重新初始化示例](ts-component-creation-re-initialization.md) - [组件创建和重新初始化示例](ts-component-creation-re-initialization.md)
- [语法糖](ts-syntactic-sugar.md)
- 常见组件开发指导 - 常见组件开发指导
- [Button开发指导](ui-ts-basic-components-button.md) - [Button开发指导](ui-ts-basic-components-button.md)
- [Web开发指导](ui-ts-components-web.md) - [Web开发指导](ui-ts-components-web.md)
......
# 环境变量
Environment是框架在应用程序启动时创建的单例对象,它为AppStorage提供了一系列应用程序需要的环境状态属性,这些属性描述了应用程序运行的设备环境。Environment及其属性是不可变的,所有属性值类型均为简单类型。如下示例展示了从Environment获取语音环境:
```ts
Environment.EnvProp("accessibilityEnabled", "default");
var enable = AppStorage.Get("accessibilityEnabled");
```
accessibilityEnabled是Environment提供默认系统变量识别符。首先需要将对应系统属性绑定到AppStorage中,再通过AppStorage中的方法或者装饰器访问对应系统的属性数据。
## Environment接口
| key | 参数 | 返回值 | 说明 |
| -------- | ---------------------------------------- | ------- | ---------------------------------------- |
| EnvProp | key:&nbsp;string,<br/>defaultValue:&nbsp;any | boolean | 关联此系统项到AppStorage中,建议在app启动时使用此接口。如果该属性在AppStorage已存在,返回false。请勿使用AppStorage中的变量,在调用此方法关联环境变量。 |
| EnvProps | keys:&nbsp;{<br/>key:&nbsp;string,<br/>defaultValue:&nbsp;any<br/>}[] | void | 关联此系统项数组到AppStorage中。 |
| Keys | Array&lt;string&gt; | number | 返回关联的系统项。 |
## Environment内置的环境变量
| key | 类型 | 说明 |
| -------------------- | --------------- | ---------------------------------------- |
| accessibilityEnabled | boolean | 无障碍屏幕朗读是否启用。 |
| colorMode | ColorMode | 深浅色模式,可选值为:<br/>-&nbsp;ColorMode.LIGHT:浅色模式;<br/>-&nbsp;ColorMode.DARK:深色模式。 |
| fontScale | number | 字体大小比例,取值范围为[0.85,&nbsp;1.45]。 |
| fontWeightScale | number | 字体权重比例,取值范围为[0.6,&nbsp;1.6]。 |
| layoutDirection | LayoutDirection | 布局方向类型,可选值为:<br/>-&nbsp;LayoutDirection.LTR:从左到右;<br/>-&nbsp;LayoutDirection.RTL:从右到左。 |
| languageCode | string | 设置当前系统的语言,小写字母,例如zh。 |
# 持久化数据管理
方舟开发框架通过PersistentStorage类提供了一些静态方法用来管理应用持久化数据,可以将特定标记的持久化数据链接到AppStorage中,并由AppStorage接口访问对应持久化数据,或者通过@StorageLink装饰器来访问对应key的变量。
| 方法 | 参数说明 | 返回值 | 定义 |
| ------------ | ---------------------------------------- | ------------------- | ---------------------------------------- |
| PersistProp | key&nbsp;:&nbsp;string<br/>defaultValue:&nbsp;T | void | 关联命名的属性在AppStorage变为持久化数据,赋值覆盖顺序如下:<br/>-&nbsp;首先,如果该属性存在于AppStorage,将Persistent中的数据复写为AppStorage中的属性值。<br/>-&nbsp;其次,Persistent中有此命名的属性,使用Persistent中的属性值。<br/>-&nbsp;最后,以上条件均不满足,则使用defaultValue,不支持null和undefined。 |
| DeleteProp | key:&nbsp;string | void | 取消双向数据绑定,该属性值将从持久存储中删除。 |
| PersistProps | keys:&nbsp;{<br/>key:&nbsp;string,<br/>defaultValue:&nbsp;any<br/>}[] | void | 关联多个命名的属性绑定。 |
| Keys | void | Array&lt;string&gt; | 返回所有持久化属性的标记。 |
> **说明:**
> - PersistProp接口使用时,需要保证输入对应的key应当在AppStorage存在。
>
> - DeleteProp接口使用时,只能对本次启动已经link过的数据生效。
```ts
// xxx.ets
PersistentStorage.PersistProp("highScore", "0");
@Entry
@Component
struct PersistentComponent {
@StorageLink('highScore') highScore: string = '0'
@State currentScore: number = 0
build() {
Column() {
if (this.currentScore === Number(this.highScore)) {
Text(`new highScore : ${this.highScore}`)
}
Button() {
Text(`goal!, currentScore : ${this.currentScore}`)
.fontSize(10)
}.onClick(() => {
this.currentScore++
if (this.currentScore > Number(this.highScore)) {
this.highScore = this.currentScore.toString()
}
})
}
}
}
```
# 应用程序的数据存储
AppStorage是应用程序中的单例对象,由UI框架在应用程序启动时创建,在应用程序退出时销毁,为应用程序范围内的可变状态属性提供中央存储。AppStorage包含整个应用程序中需要访问的所有状态属性,只要应用程序保持运行,AppStorage就会保存所有属性及属性值,属性值可以通过唯一的键值进行访问。
UI组件可以通过装饰器将应用程序状态数据与AppStorage进行同步,应用业务逻辑的实现也可以通过接口访问AppStorage。
AppStorage的选择状态属性可以与不同的数据源或数据接收器同步,这些数据源和接收器可以是设备上的本地或远程,并具有不同的功能,如数据持久性。这样的数据源和接收器可以独立于UI在业务逻辑中实现。
默认情况下,AppStorage中的属性是可变的,AppStorage还可使用不可变(只读)属性。
## AppStorage接口
| 方法 | 参数说明 | 返回值 | 定义 |
| ----------- | ---------------------------------------- | ------------------- | ---------------------------------------- |
| SetAndLink | key:&nbsp;string,<br/>defaultValue:&nbsp;T | @Link | 与Link接口类似,如果当前的key保存于AppStorage,则返回该key对应的value。如果该key未被创建,则创建一个对应default值的Link返回。 |
| Set | key:&nbsp;string,<br/>newValue:&nbsp;T | void | 对已保存的key值,替换其value值。 |
| Link | key:&nbsp;string | @Link | 如果存在具有给定键的数据,则返回到此属性的双向数据绑定,该双向绑定意味着变量或者组件对数据的更改将同步到AppStorage,通过AppStorage对数据的修改将同步到变量或者组件。如果具有此键的属性不存在或属性为只读,则返回undefined。 |
| SetAndProp | propName:&nbsp;string,<br/>defaultValue:&nbsp;S | @Prop | 与Prop接口类似,如果当前的key保存于AppStorage,则返回该key对应的value。如果该key未被创建,则创建一个对应default值的Prop返回。 |
| Prop | key:&nbsp;string | @Prop | 如果存在具有给定键的属性,则返回此属性的单向数据绑定。该单向绑定意味着只能通过AppStorage将属性的更改同步到变量或者组件。该方法返回的变量为不可变变量,适用于可变和不可变的状态属性,如果具有此键的属性不存在则返回undefined。<br/>>&nbsp;![icon-note.gif](public_sys-resources/icon-note.gif)&nbsp;**说明:**<br/>>&nbsp;prop方法对应的属性值类型为简单类型。 |
| SetOrCreate | key:&nbsp;string,<br/>newValue:&nbsp;T | boolean | 如果已存在与给定键名字相同的属性,如果此属性可以被更改则替换其value值且返回true,否则不替换且返回false。<br/>如果不存在存在与给定键名字相同的属性,&nbsp;则创建一个键为key,&nbsp;值为newValue的属性,属性值不支持null和undefined。 |
| Get | key:&nbsp;string | T或undefined | 通过此接口获取对应key值的value。 |
| Has | propName:&nbsp;string | boolean | 判断对应键值的属性是否存在。 |
| Keys | void | array&lt;string&gt; | 返回包含所有键的字符串数组。 |
| Delete | key:&nbsp;string | boolean | 删除key指定的键值对,如果存在且删除成功返回true,不存在或删除失败返回false。 |
| Clear | void | boolean | 删除所有的属性,如果当前有状态变量依旧引用此属性,则返回false。 |
| IsMutable | key:&nbsp;string | boolean | 返回此属性是否存在并且是否可以改变。 |
## AppStorage与组件同步
[管理组件拥有的状态](ts-component-states-state.md)中,已经定义了如何将组件的状态变量与父组件或祖先组件中的@State装饰的状态变量同步,主要包括@Prop、@Link、@Consume。
本章节定义如何将组件变量与AppStorage同步,主要提供@StorageLink和@StorageProp装饰器。
### @StorageLink装饰器
组件通过使用@StorageLink(key)装饰的状态变量,与AppStorage建立双向数据绑定,key为AppStorage中的属性键值。当创建包含@StorageLink的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。在UI组件中对@StorageLink的状态变量所做的更改将同步到AppStorage,并从AppStorage同步到任何其他绑定实例中,如PersistentStorage或其他绑定的UI组件。
### @StorageProp装饰器
组件通过使用@StorageProp(key)装饰的状态变量,将与AppStorage建立单向数据绑定,key标识AppStorage中的属性键值。当创建包含@StoageProp的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。AppStorage中的属性值的更改会导致绑定的UI组件进行状态更新。
## 示例
```ts
// xxx.ets
@Entry
@Component
struct ComponentA {
@StorageLink('varA') varA: number = 2
@StorageProp('languageCode') lang: string = 'en'
private label: string = 'count'
aboutToAppear() {
this.label = (this.lang === 'zh') ? '' : 'Count'
}
build() {
Column(){
Row({ space: 20 }) {
Button(`${this.label}: ${this.varA}`)
.onClick(() => {
AppStorage.Set<number>('varA', AppStorage.Get<number>('varA') + 1)
})
Button(`lang: ${this.lang}`)
.onClick(() => {
if (this.lang === 'zh') {
AppStorage.Set<string>('languageCode', 'en')
} else {
AppStorage.Set<string>('languageCode', 'zh')
}
this.label = (this.lang === 'zh') ? '' : 'Count'
})
}
.margin({ bottom: 50 })
Row(){
Button(`更改@StorageLink修饰的变量:${this.varA}`).fontSize(10)
.onClick(() => {
this.varA++
})
}.margin({ bottom: 50 })
Row(){
Button(`更改@StorageProp修饰的变量:${this.lang}`).fontSize(10)
.onClick(() => {
this.lang = 'test'
})
}
}
}
}
```
每次用户单击Count按钮时,this.varA变量值都会增加,此变量与AppStorage中的varA同步。每次用户单击当前语言按钮时,修改AppStorage中的languageCode,此修改会同步给this.lang变量。
# 属性配置
使用属性方法配置组件的属性,属性方法紧随组件,并用“.”运算符连接。
- 配置Text组件的字体大小属性:
```ts
Text('123')
.fontSize(12)
```
- 使用“.”操作进行链式调用并同时配置组件的多个属性,如下所示:
```ts
Image('a.jpg')
.alt('error.jpg')
.width(100)
.height(100)
```
- 除了直接传递常量参数外,还可以传递变量或表达式,如下所示:
```ts
// Size, count, and offset are private variables defined in the component.
Text('hello')
.fontSize(this.size)
Image('a.jpg')
.width(this.count % 2 === 0 ? 100 : 200)
.height(this.offset + 100)
```
- 对于内置组件,框架还为其属性预定义了一些枚举类型,供开发人员调用,枚举值可以作为参数传递。枚举类型必须满足参数类型要求,有关特定属性的枚举类型定义的详细信息。可以按以下方式配置Text组件的颜色和字体属性:
```ts
Text('hello')
.fontSize(20)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
```
# 子组件配置
对于支持子组件配置的组件,例如容器组件,在“{ ... }”里为组件添加子组件的UI描述。Column、Row、Stack、Button、Grid和List组件都是容器组件。
- 以下是简单的Column示例:
```ts
Column() {
Text('Hello')
.fontSize(100)
Divider()
Text(this.myText)
.fontSize(100)
.fontColor(Color.Red)
}
```
- 可以嵌套多个子组件:
```ts
Column() {
Column() {
Button() {
Text('+ 1')
}.type(ButtonType.Capsule)
.onClick(() => console.log ('+1 clicked!'))
Image('1.jpg')
}
Divider()
Column() {
Button() {
Text('+ 2')
}.type(ButtonType.Capsule)
.onClick(() => console.log ('+2 clicked!'))
Image('2.jpg')
}
Divider()
Column() {
Button() {
Text('+ 3')
}.type(ButtonType.Capsule)
.onClick(() => console.log('+3 clicked!'))
Image('3.jpg')
}
}.alignItems(HorizontalAlign.Center) // center align components inside Column
```
# @Component
@Component装饰的struct表示该结构体具有组件化能力,能够成为一个独立的组件,这种类型的组件也称为自定义组件,在build方法里描述UI结构。自定义组件具有以下特点:
- 可组合:允许开发人员组合使用内置组件、其他组件、公共属性和方法;
- 链式调用<sup>9+</sup>:通过链式调用通用属性改变组件样式;
- 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用;
- 生命周期:生命周期的回调方法可以在组件中配置,用于业务逻辑处理;
- 数据驱动更新:由状态变量的数据驱动,实现UI自动更新。
对组件化的深入描述,请参考[深入理解组件化](ts-custom-component-initialization.md)
> **说明:**
>
> - 自定义组件必须定义build方法。
>- 自定义组件禁止自定义构造函数。
如下代码定义了MyComponent组件:
```ts
@Component
struct MyComponent {
build() {
Column() {
Text('my component')
.fontColor(Color.Red)
}.alignItems(HorizontalAlign.Center) // center align Text inside Column
}
}
```
MyComponent的build方法会在初始渲染时执行,此外,当组件中的状态发生变化时,build方法将再次执行。
以下代码使用了MyComponent组件:
```ts
@Component
struct ParentComponent {
build() {
Column() {
MyComponent()
Text('we use component')
.fontSize(20)
}
}
}
```
可以多次使用MyComponent,并在不同的组件中进行重用:
```ts
@Component
struct ParentComponent {
build() {
Row() {
Column() {
MyComponent()
Text('first column')
.fontSize(20)
}
Column() {
MyComponent()
Text('second column')
.fontSize(20)
}
}
}
aboutToAppear() {
console.log('ParentComponent: Just created, about to become rendered first time.')
}
aboutToDisappear() {
console.log('ParentComponent: About to be removed from the UI.')
}
}
```
可链式调用通用属性,使组件样式多样化:
> **说明:** 从API version 9开始支持。
>
> 自定义组件链式调用暂不支持尾随闭包写法(在初始化自定义组件时,组件名称紧跟一个大括号“{}”形成尾随闭包场景`(Inedx(){})`。开发者可把尾随闭包看做一个容器,向其填充内容,如在闭包内增加组件`{Column(){Text("content")}` )。
```ts
@Entry
@Component
struct Index {
@State bannerValue: string = 'Hello,world';
build() {
Column() {
Chind({ ChindBannerValue:$bannerValue })
.height(60)
.width(250)
.border({ width:5, color:Color.Red, radius:10, style: BorderStyle.Dotted })
}
}
}
@Component
struct Chind {
@Link ChindBannerValue: string;
build() {
Column() {
Text(this.ChindBannerValue)
.fontSize(30)
}
}
}
```
# @CustomDialog
@CustomDialog装饰器用于装饰自定义弹窗。
```ts
// custom-dialog-demo.ets
@CustomDialog
struct DialogExample {
controller: CustomDialogController;
action: () => void;
build() {
Row() {
Button ("Close CustomDialog")
.onClick(() => {
this.controller.close();
this.action();
})
}.padding(20)
}
}
@Entry
@Component
struct CustomDialogUser {
dialogController : CustomDialogController = new CustomDialogController({
builder: DialogExample({action: this.onAccept}),
cancel: this.existApp,
autoCancel: true
});
onAccept() {
console.log("onAccept");
}
existApp() {
console.log("Cancel dialog!");
}
build() {
Column() {
Button("Click to open Dialog")
.onClick(() => {
this.dialogController.open()
})
}
}
}
```
![custom-dialog-demo](figures/custom-dialog-demo.gif)
\ No newline at end of file
# @Entry
用@Entry装饰的自定义组件用作页面的默认入口组件,加载页面时,将首先创建并呈现@Entry装饰的自定义组件。
> **说明:**
> 在单个源文件中,最多可以使用@Entry装饰一个自定义组件。
@Entry的用法如下:
```ts
// Only MyComponent decorated by @Entry is rendered and displayed. "hello world" is displayed, but "goodbye" is not displayed.
@Entry
@Component
struct MyComponent {
build() {
Column() {
Text('hello world')
.fontColor(Color.Red)
}
}
}
@Component
struct HideComponent {
build() {
Column() {
Text('goodbye')
.fontColor(Color.Blue)
}
}
}
```
# @Extend
@Extend装饰器将新的属性函数添加到内置组件上,如Text、Column、Button等。通过@Extend装饰器可以快速定义并复用组件的自定义样式。
```ts
// xxx.ets
@Extend(Text) function fancy(fontSize: number) {
.fontColor(Color.Red)
.fontSize(fontSize)
.fontStyle(FontStyle.Italic)
}
@Entry
@Component
struct FancyUse {
build() {
Row({ space: 10 }) {
Text("Fancy")
.fancy(16)
Text("Fancy")
.fancy(24)
}
}
}
```
> **说明:**
>
> - @Extend装饰器不能用在自定义组件struct定义框内。
> - @Extend装饰器内仅支持属性函数语句。
# @Preview
用@Preview装饰的自定义组件可以在DevEco Studio的预览器上进行实时预览,不支持动态图和动态预览,加载页面时,将创建并呈现@Preview装饰的自定义组件。
> **说明:**
> 在单个源文件中,最多可以使用10个@Preview装饰自定义组件,更多说明请参考[查看eTS组件预览效果]( https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ohos-previewing-app-service-0000001218760596#section146052489820 )。
@Preview的用法如下:
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
build() {
Column() {
Row() {
Text('Hello World!')
.fontSize("50lpx")
.fontWeight(FontWeight.Bold)
}
Row() {
Component1()
}
Row() {
Component2()
}
}
}
}
@Preview
@Component
struct Component1 {
build() {
Column() {
Row() {
Text('Hello Component1')
.fontSize("50lpx")
.fontWeight(FontWeight.Bold)
}
}
}
}
@Component
struct Component2 {
build() {
Column() {
Row() {
Text('Hello Component2')
.fontSize("50lpx")
.fontWeight(FontWeight.Bold)
}
}
}
}
```
# @Styles
@Styles装饰器将新的属性函数添加到基本组件上,如Text、Column、Button等。当前@Styles仅支持通用属性。通过@Styles装饰器可以快速定义并复用组件的自定义样式。
@Styles可以定义在组件内或组件外,在组件外定义时需在方法前添加function关键字,组件内定义时不需要添加function关键字。
```ts
// xxx.ets
@Styles function globalFancy() {
.backgroundColor(Color.Red)
}
@Entry
@Component
struct FancyUse {
@Styles componentFancy() {
.backgroundColor(Color.Blue)
}
build() {
Column({ space: 10 }) {
Text("Fancy")
.globalFancy()
.width(100)
.height(100)
.fontSize(30)
Text("Fancy")
.componentFancy()
.width(100)
.height(100)
.fontSize(30)
}
}
}
```
@Styles还可以在[StateStyles](../reference/arkui-ts/ts-universal-attributes-polymorphic-style.md)属性内部使用,在组件处于不同的状态时赋予相应的属性。
在StateStyles内可以直接调用组件外定义的Styles,但需要通过this关键字调用组件内定义的Styles。
```ts
// xxx.ets
@Styles function globalFancy() {
.width(100)
.height(100)
}
@Entry
@Component
struct FancyUse {
@Styles componentFancy() {
.width(50)
.height(50)
}
build() {
Row({ space: 10 }) {
Button() {
Text("Fancy")
}
.stateStyles({
normal: {
.width(80)
.height(80)
},
disabled: this.componentFancy,
pressed: globalFancy
})
}
}
}
```
# @Link
@Link装饰的变量可以和父组件的@State变量建立双向数据绑定:
- 支持多种类型:@Link变量的值与@State变量的类型相同,即class、number、string、boolean或这些类型的数组;
- 私有:仅在组件内访问;
- 单个数据源:初始化@Link变量的父组件的变量必须是@State变量;
- 双向通信:子组件对@Link变量的更改将同步修改父组件的@State变量;
- 创建自定义组件时需要将变量的引用传递给@Link变量,在创建组件的新实例时,必须使用命名参数初始化所有@Link变量。@Link变量可以使用@State变量或@Link变量的引用进行初始化,@State变量可以通过`'$'`操作符创建引用。
> 说明:
> @Link变量不能在组件内部进行初始化。
## 简单类型示例
```ts
// xxx.ets
@Entry
@Component
struct Player {
@State isPlaying: boolean = false
build() {
Column() {
PlayButton({buttonPlaying: $isPlaying})
Text(`Player is ${this.isPlaying? '':'not'} playing`)
}
}
}
@Component
struct PlayButton {
@Link buttonPlaying: boolean
build() {
Column() {
Button() {
Image(this.buttonPlaying? 'play.png' : 'pause.png')
}.onClick(() => {
this.buttonPlaying = !this.buttonPlaying
})
}
}
}
```
@Link语义是从`'$'`操作符引出,即`$isPlaying``this.isPlaying`内部状态的双向数据绑定。当单击PlayButton时,PlayButton的Image组件和Text组件将同时进行刷新。
## 复杂类型示例
```ts
// xxx.ets
@Entry
@Component
struct Parent {
@State arr: number[] = [1, 2, 3]
build() {
Column() {
Child({items: $arr})
ForEach(this.arr,
item => Text(`${item}`),
item => item.toString())
}
}
}
@Component
struct Child {
@Link items: number[]
build() {
Column() {
Button() {
Text('Button1: push')
}.onClick(() => {
this.items.push(100)
})
Button() {
Text('Button2: replace whole item')
}.onClick(() => {
this.items = [100, 200, 300]
})
}
}
}
```
在上面的示例中,点击Button1和Button2更改父组件中显示的文本项目列表。
## @Link、@State和@Prop结合使用示例
```ts
// xxx.ets
@Entry
@Component
struct ParentView {
@State counter: number = 0
build() {
Column() {
ChildA({counterVal: this.counter}) // pass by value
ChildB({counterRef: $counter}) // $ creates a Reference that can be bound to counterRef
}
}
}
@Component
struct ChildA {
@Prop counterVal: number
build() {
Button() {
Text(`ChildA: (${this.counterVal}) + 1`)
}.onClick(() => {this.counterVal+= 1})
}
}
@Component
struct ChildB {
@Link counterRef: number
build() {
Button() {
Text(`ChildB: (${this.counterRef}) + 1`)
}.onClick(() => {this.counterRef+= 1})
}
}
```
上述示例中,ParentView包含ChildA和ChildB两个子组件,ParentView的状态变量counter分别初始化ChildA和ChildB。
- ChildB使用@Link建立双向状态绑定。当ChildB修改counterRef状态变量值时,该更改将同步到ParentView和ChildA共享;
- ChildA使用@Prop建立从ParentView到自身的单向状态绑定。当ChildA修改状态时,ChildA将重新渲染,但该更改不会传达给ParentView和ChildB。
# @Prop
@Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但更改不会通知给父组件,即@Prop属于单向数据绑定。
@Prop状态数据具有以下特征:
- 支持简单类型:仅支持number、string、boolean简单类型;
- 私有:仅在组件内访问;
- 支持多个实例:一个组件中可以定义多个标有@Prop的属性;
- 创建自定义组件时将值传递给@Prop变量进行初始化:在创建组件的新实例时,必须初始化所有@Prop变量,不支持在组件内部进行初始化。
## 示例
```ts
// xxx.ets
@Entry
@Component
struct ParentComponent {
@State countDownStartValue: number = 10 // 10 Nuggets default start value in a Game
build() {
Column() {
Text(`Grant ${this.countDownStartValue} nuggets to play.`)
Button() {
Text('+1 - Nuggets in New Game')
}.onClick(() => {
this.countDownStartValue += 1
})
Button() {
Text('-1 - Nuggets in New Game')
}.onClick(() => {
this.countDownStartValue -= 1
})
// 创建子组件时,必须在构造函数参数中提供其@Prop变量的初始值,同时初始化常规变量CostOfOneAttump(非Prop)
CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2})
}
}
}
@Component
struct CountDownComponent {
@Prop count: number
private costOfOneAttempt: number
build() {
Column() {
if (this.count > 0) {
Text(`You have ${this.count} Nuggets left`)
} else {
Text('Game over!')
}
Button() {
Text('Try again')
}.onClick(() => {
this.count -= this.costOfOneAttempt
})
}
}
}
```
在上述示例中,当按“+1”或“-1”按钮时,父组件状态发生变化,重新执行build方法,此时将创建一个新的CountDownComponent组件。父组件的countDownStartValue状态属性被用于初始化子组件的@Prop变量,当按下子组件的“Try again”按钮时,其@Prop变量count将被更改,CountDownComponent重新渲染。但是count值的更改不会影响父组件的countDownStartValue值。
> **说明:**
> 创建新组件实例时,必须初始化其所有@Prop变量。
# @State
@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新,只能监听第一层数据的改变,内层数据的改变@State监听不到,无法触发build生命周期。
@State状态数据具有以下特征:
- 支持多种类型:允许class、number、boolean、string强类型的按值和按引用类型。允许这些强类型构成的数组,即Array&lt;class&gt;、Array&lt;string&gt;、Array&lt;boolean&gt;、Array&lt;number&gt;。不允许object和any。
- 支持多实例:组件不同实例的内部状态数据独立。
- 内部私有:标记为@State的属性是私有变量,只能在组件内访问。
- 需要本地初始化:必须为所有@State变量分配初始值,将变量保持未初始化可能导致框架行为未定义。
- 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定@State状态属性的初始值。
## 简单类型的状态属性示例
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
@State count: number = 0
// MyComponent provides a method for modifying the @State status data member.
private toggleClick() {
this.count += 1
}
build() {
Column() {
Button() {
Text(`click times: ${this.count}`)
.fontSize(10)
}.onClick(this.toggleClick.bind(this))
}
}
}
```
![@state1](figures/@state1.gif)
## 复杂类型的状态变量示例
```ts
// Customize the status data class.
// xxx.ets
class Model {
value: string
constructor(value: string) {
this.value = value
}
}
@Entry
@Component
struct EntryComponent {
build() {
Column() {
MyComponent({count: 1, increaseBy: 2}) // MyComponent1 in this document
MyComponent({title: {value: 'Hello, World 2'}, count: 7}) //MyComponent2 in this document
}
}
}
@Component
struct MyComponent {
@State title: Model = {value: 'Hello World'}
@State count: number = 0
private toggle: string = 'Hello World'
private increaseBy: number = 1
build() {
Column() {
Text(`${this.title.value}`).fontSize(30)
Button() {
Text(`Click to change title`).fontSize(20).fontColor(Color.White)
}.onClick(() => {
this.title.value = (this.toggle == this.title.value) ? 'Hello World' : 'Hello UI'
}) // Modify the internal state of MyComponent using the anonymous method.
Button() {
Text(`Click to increase count=${this.count}`).fontSize(20).fontColor(Color.White)
}.onClick(() => {
this.count += this.increaseBy
}) // Modify the internal state of MyComponent using the anonymous method.
}
}
}
```
![@state1](figures/@state.png)
在上述示例中:
- 用户定义的组件MyComponent定义了@State状态变量count和title。如果count或title的值发生变化,则执行MyComponent的build方法来重新渲染组件;
- EntryComponent中有多个MyComponent组件实例,第一个MyComponent内部状态的更改不会影响第二个MyComponent;
- 创建MyComponent实例时通过变量名给组件内的变量进行初始化,如:
```ts
MyComponent({title: {value: 'Hello, World 2'}, count: 7})
```
# 必选参数构造配置
如果组件的接口定义中包含必选构造参数,则在组件后面的“()”中必须配置参数,参数可以使用常量进行赋值。
例如:
- Image组件的必选参数src:
```ts
Image('http://xyz/a.jpg')
```
- Text组件的必选参数content:
```ts
Text('123')
```
变量或表达式也可以用于参数赋值,其中表达式返回的结果类型必须满足参数类型要求。例如,传递变量或表达式来构造Image和Text组件的参数:
```ts
// imagePath, where imageUrl is a private data variable defined in the component.
Image(this.imagePath)
Image('http://' + this.imageUrl)
// count is a private data variable defined in the component.
// (``) and (${}) are the template character string features supported by the TS language and comply with the
// features of the corresponding language. This specification is not limited.
Text(`count: ${this.count}`)
```
# 事件配置
通过事件方法可以配置组件支持的事件。从API Version 9开始,可以在回调函数中获取当前component关联的Context,具体用法请参考[在eTS页面中访问Context](../ability/context-userguide.md)
- 使用lambda表达式配置组件的事件方法:
```ts
// Counter is a private data variable defined in the component.
Button('add counter')
.onClick(() => {
this.counter += 2
})
```
- 使用匿名函数表达式配置组件的事件方法,要求使用bind,以确保函数体中的this引用包含的组件:
```ts
// Counter is a private data variable defined in the component.
Button('add counter')
.onClick(function () {
this.counter += 2
}.bind(this))
```
- 使用组件的成员函数配置组件的事件方法:
```ts
myClickHandler(): void {
// do something
}
...
Button('add counter')
.onClick(this.myClickHandler)
```
# build函数
build函数满足Builder构造器接口定义,用于定义组件的声明式UI描述。必须遵循上述Builder接口约束,在build方法中以声明式方式进行组合自定义组件或系统内置组件,在组件创建和更新场景中都会调用build方法。
```ts
interface Builder {
build: () => void
}
```
> **说明:**
> build方法仅支持组合组件,使用渲染控制语法。
# 基本概念
基于eTS的声明式开发范式提供了一系列基础组件,这些组件以声明方式进行组合和扩展来描述应用程序的UI界面,并且还提供了基本的数据绑定和事件处理机制,帮助开发者实现应用交互逻辑。
## HelloWorld基本示例
```ts
// An example of displaying Hello World. After you click the button, Hello UI is displayed.
// xxx.ets
@Entry
@Component
struct Hello {
@State myText: string = 'World'
build() {
Column() {
Text('Hello')
.fontSize(30)
Text(this.myText)
.fontSize(32)
Divider()
Button() {
Text('Click me')
.fontColor(Color.Red)
}.onClick(() => {
this.myText = 'UI'
})
.width(500)
.height(200)
}
}
}
```
## 基本概念描述
上述示例代码描述了简单页面的结构,并介绍了以下基本概念:
- 装饰器:方舟开发框架定义了一些具有特殊含义的装饰器,用于装饰类、结构、方法和变量。例如,上例中的@Entry、@Component和@State都是装饰器。
- 自定义组件:可重用的UI单元,可以与其他组件组合,如@Component装饰的struct Hello。
- UI描述:声明性描述UI结构,例如build()方法中的代码块。
- 内置组件:框架中默认内置的基本组件和布局组件,开发者可以直接调用,如Column、Text、Divider、Button等。
- 属性方法:用于配置组件属性,如fontSize()、width()、height()、color()等。
- 事件方法:在事件方法的回调中添加组件响应逻辑。例如,为Button组件添加onClick方法,在onClick方法的回调中添加点击响应逻辑。
# @Consume和@Provide数据管理
Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。Consume在感知到Provide数据的更新后,会触发当前view的重新渲染。
表1 @Provide
| 名称 | 说明 |
| ------- | ---------------------------------------- |
| 装饰器参数 | 是一个string类型的常量,用于给装饰的变量起别名。如果规定别名,则提供对应别名的数据更新。如果没有,则使用变量名作为别名。推荐使用@Provide("alias")这种形式。 |
| 同步机制 | @Provide的变量类似@state,可以修改对应变量进行页面重新渲染。也可以修改@Consume装饰的变量,反向修改@State变量。 |
| 初始值 | 必须制定初始值。 |
| 页面重渲染场景 | 触发页面渲染的修改:<br/>-&nbsp;基础类型(boolean,string,number)变量的改变;<br/>-&nbsp;@Observed&nbsp;class类型变量及其属性的修改;<br/>-&nbsp;添加,删除,更新数组中的元素。 |
表2 @Consume
| 类型 | 说明 |
| ---- | -------- |
| 初始值 | 不可设置默认值。 |
> **说明:**
> 使用@Provide 和@Consume时避免循环引用导致死循环。
其他属性说明与Provide一致。
```ts
// xxx.ets
@Entry
@Component
struct CompA {
@Provide("reviewVote") reviewVotes : number = 0;
build() {
Column() {
CompB()
Button() {
Text(`${this.reviewVotes}`)
.fontSize(30)
}
.onClick(() => {
this.reviewVotes += 1;
})
}
}
}
@Component
struct CompB {
build() {
Column() {
CompC()
}
}
}
@Component
struct CompC {
@Consume("reviewVote") reviewVotes : number;
build() {
Column() {
Button() {
Text(`${this.reviewVotes}`)
.fontSize(30)
}
.onClick(() => {
this.reviewVotes += 1;
})
}
}
}
```
# Observed和ObjectLink数据管理
本章将引入两个新的装饰符@Observed和@ObjectLink:
- @Observed应用于类,表示该类中的数据变更被UI页面管理,例如:@Observed class ClassA {}。
- @ObjectLink应用于被@Observed所装饰类的对象,例如:@ObjectLink a: ClassA。
## 引入动机
当开发者需要在子组件中针对父组件的一个变量(parent_a)设置双向同步时,开发者可以在父组件中使用\@State装饰变量(parent_a),并在子组件中使用@Link装饰相应的变量(child_a)。这样的话,不仅可以实现父组件与单个子组件之间的数据同步,也可以实现父组件与多个子组件之间的数据同步。如下图所示,可以看到,父子组件针对ClassA类型的变量设置了双向同步,那么当子组件1中变量的属性c的值变化时,会通知父组件同步变化,而当父组件中属性c的值变化时,会通知所有子组件同步变化。
![](figures/zh-cn_image_0000001251090821.png)
然而,上述例子是针对某个数据对象进行的整体同步,而当开发者只想针对父组件中某个数据对象的部分信息进行同步时,使用@Link就不能满足要求。如果这些部分信息是一个类对象,就可以使用@ObjectLink配合@Observed来实现,如下图所示。
![](figures/zh-cn_image_0000001206450834.png)
## 设置要求
- @Observed 用于类,@ObjectLink 用于变量。
- @ObjectLink装饰的变量类型必须为类(class type)。
- 类要被\@Observed装饰器所装饰。
- 不支持简单类型参数,可以使用@Prop进行单向同步。
- @ObjectLink装饰的变量是不可变的(immutable)。
- 属性的改动是被允许的,当改动发生时,如果同一个对象被多个@ObjectLink变量所引用,那么所有拥有这些变量的自定义组件都会被通知去重新渲染。
- @ObjectLink装饰的变量不可设置默认值。
- 必须让父组件中有一个由@State、@Link、@StorageLink、@Provide或@Consume所装饰变量参与的TS表达式进行初始化。
- @ObjectLink装饰的变量是私有变量,只能在组件内访问。
## 示例
### 案例1
```ts
//父组件ViewB中的类对象ClassB,其包含的对象ClassA与子组件ViewA数据同步时,通过ObjectLink将数据c值的变化状态通知给父组件同步变化。
@Observed
class ClassA {
public name : string;
public c: number;
constructor(c: number, name: string = 'OK') {
this.name = name;
this.c = c;
}
}
class ClassB {
public a: ClassA;
constructor(a: ClassA) {
this.a = a;
}
}
@Component
struct ViewA {
label : string = "ep1";
@ObjectLink a : ClassA;
build() {
Column() {
Text(`ViewA [${this.label}]: a.c=${this.a.c}`)
.fontSize(20)
Button(`+1`)
.width(100)
.margin(2)
.onClick(() => {
this.a.c += 1;
})
Button(`reset`)
.width(100)
.margin(2)
.onClick(() => {
this.a = new ClassA(0); // 错误:ObjectLink装饰的变量a是不可变的
})
}
}
}
@Entry
@Component
struct ViewB {
@State b : ClassB = new ClassB(new ClassA(10));
build() {
Flex({direction: FlexDirection.Column, alignItems: ItemAlign.Center}) {
ViewA({label: "ViewA #1", a: this.b.a})
ViewA({label: "ViewA #2", a: this.b.a})
Button(`ViewB: this.b.a.c += 1` )
.width(320)
.margin(4)
.onClick(() => {
this.b.a.c += 1;
})
Button(`ViewB: this.b.a = new ClassA(0)`)
.width(240)
.margin(4)
.onClick(() => {
this.b.a = new ClassA(0);
})
Button(`ViewB: this.b = new ClassB(ClassA(0))`)
.width(240)
.margin(4)
.onClick(() => {
this.b = new ClassB(new ClassA(0));
})
}
}
}
```
### 案例2
```ts
// 父组件ViewB中的类对象ClassA与子组件ViewA保持数据同步时,可以使用@ObjectLink和@Observed,绑定该数据对象的父组件和其他子组件同步更新
var nextID: number = 0;
@Observed
class ClassA {
public name : string;
public c: number;
public id : number;
constructor(c: number, name: string = 'OK') {
this.name = name;
this.c = c;
this.id = nextID++;
}
}
@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;
})
}.margin({ top: 10 })
}
}
@Entry
@Component
struct ViewB {
@State arrA : ClassA[] = [ new ClassA(0), new ClassA(0) ];
build() {
Column() {
ForEach (this.arrA, (item) => {
ViewA({label: `#${item.id}`, a: item})
},
(item) => item.id.toString()
)
ViewA({label: `ViewA this.arrA[first]`, a: this.arrA[0]})
ViewA({label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1]})
Button(`ViewB: reset array`)
.margin({ top: 10 })
.onClick(() => {
this.arrA = [ new ClassA(0), new ClassA(0) ];
})
Button(`ViewB: push`)
.margin({ top: 10 })
.onClick(() => {
this.arrA.push(new ClassA(0))
})
Button(`ViewB: shift`)
.margin({ top: 10 })
.onClick(() => {
this.arrA.shift()
})
}
}
}
```
# @Watch
@Watch用于监听状态变量的变化,语法结构为:
```ts
@State @Watch("onChanged") count : number = 0
```
如上给状态变量增加一个@Watch装饰器,通过@Watch注册一个回调方法onChanged, 当状态变量count被改变时, 触发onChanged回调。
装饰器@State、@Prop、@Link、@ObjectLink、@Provide、@Consume、@StorageProp以及@StorageLink装饰的变量可以监听其变化。
```ts
// xxx.ets
@Entry
@Component
struct CompA {
@State @Watch("onBasketUpdated") shopBasket : Array<number> = [ 7, 12, 47, 3 ];
@State totalPurchase : number = 0;
updateTotal() : number {
let sum = 0;
this.shopBasket.forEach((i) => { sum += i; });
// 计算新的购物篮总价值,如果超过100RMB,则适用折扣
this.totalPurchase = (sum < 100) ? sum : 0.9 * sum;
return this.totalPurchase;
}
// @Watch cb
onBasketUpdated(propName: string) : void {
this.updateTotal();
}
build() {
Column() {
Button("add to basket").onClick(() => { this.shopBasket.push(Math.round(100 * Math.random())) })
Text(`${this.totalPurchase}`)
.fontSize(30)
}
}
}
```
# 无参数构造配置
组件的接口定义不包含必选构造参数,组件后面的“()”中不需要配置任何内容。例如,Divider组件不包含构造参数:
```ts
Column() {
Text('item 1')
Divider() // No parameter configuration of the divider component
Text('item 2')
}
```
# 循环渲染
开发框架提供循环渲染(ForEach组件)来迭代数组,并为每个数组项创建相应的组件。当循环渲染的元素较多时,会出现页面加载变慢的情况,出于性能考虑,建议使用[LazyForEach](ts-rending-control-syntax-lazyforeach.md)代替。ForEach定义如下:
```ts
ForEach(
arr: any[], // Array to be iterated
itemGenerator: (item: any, index?: number) => void, // child component generator
keyGenerator?: (item: any, index?: number) => string // (optional) Unique key generator, which is recommended.
)
```
## ForEach
ForEach(arr: any[],itemGenerator: (item: any, index?: number) =&gt; void, keyGenerator?: (item: any, index?: number) =&gt; string):void
表1 参数说明
| 参数名 | 参数类型 | 必填 | 默认值 | 参数描述 |
| ------------- | ---------------------------------------- | ---- | ---- | ---------------------------------------- |
| arr | any[] | 是 | - | 必须是数组,允许空数组,空数组场景下不会创建子组件。同时允许设置返回值为数组类型的函数,例如arr.slice(1,&nbsp;3),设置的函数不得改变包括数组本身在内的任何状态变量,如Array.splice、Array.sort或Array.reverse这些改变原数组的函数。 |
| itemGenerator | (item:&nbsp;any,&nbsp;index?:&nbsp;number)&nbsp;=&gt;&nbsp;void | 是 | - | 生成子组件的lambda函数,为给定数组项生成一个或多个子组件,单个组件和子组件列表必须括在大括号“{....}”中。 |
| keyGenerator | (item:&nbsp;any,&nbsp;index?:&nbsp;number)&nbsp;=&gt;&nbsp;string | 否 | - | 匿名参数,用于给定数组项生成唯一且稳定的键值。当子项在数组中的位置更改时,子项的键值不得更改,当数组中的子项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则ForEach中的所有节点都将重建。 |
> **说明:**
> - 必须在容器组件内使用;
>
> - 生成的子组件允许在ForEach的父容器组件中,允许子组件生成器函数中包含if/else条件渲染,同时也允许ForEach包含在if/else条件渲染语句中;
>
> - 子项生成器函数的调用顺序不一定和数组中的数据项相同,在开发过程中不要假设子项生成器和键值生成器函数是否执行以及执行顺序。如下示例可能无法正常工作:
> ```ts
> ForEach(anArray, item => {Text(`${++counter}. item.label`)})
> ```
>
> 正确的示例如下:
>
> ```ts
> ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }),
> item => Text(`${item.i}. item.data.label`),
> item => item.data.id.toString())
> ```
## 示例
简单类型数组示例:
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
@State arr: number[] = [10, 20, 30]
build() {
Column() {
Button() {
Text('Reverse Array')
}.onClick(() => {
this.arr.reverse()
})
ForEach(this.arr, // Parameter 1: array to be iterated
(item: number) => { // Parameter 2: item generator
Text(`item value: ${item}`)
Divider()
},
(item: number) => item.toString() // Parameter 3: unique key generator, which is optional but recommended.
)
}
}
}
```
复杂类型数组示例:
```ts
// xxx.ets
class Month {
year: number
month: number
days: Array<number>
constructor(year, month, days) {
this.year = year;
this.month = month;
this.days = days;
}
}
@Entry
@Component
struct Calendar1 {
// simulate with 6 months
@State calendar: Month[] = [
new Month(2020, 1, [...Array(31).keys()]),
new Month(2020, 2, [...Array(28).keys()]),
new Month(2020, 3, [...Array(31).keys()]),
new Month(2020, 4, [...Array(30).keys()]),
new Month(2020, 5, [...Array(31).keys()]),
new Month(2020, 6, [...Array(30).keys()]),
]
build() {
Column() {
Button('next month')
.onClick(() => {
this.calendar.shift()
this.calendar.push({
year: 2020,
month: 7,
days: [...Array(31)
.keys()]
})
})
ForEach(this.calendar,
(item: Month) => {
Text('month:' + item.month)
.fontSize(30)
.padding(20)
Grid() {
ForEach(item.days,
(day: number) => {
GridItem() {
Text((day + 1).toString())
.fontSize(30)
}
},
(day: number) => day.toString())
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.rowsGap(20)
},
// field is used together with year and month as the unique ID of the month.
(item: Month) => (item.year * 12 + item.month).toString())
}
}
}
```
# 条件渲染
使用if/else进行条件渲染。
> **说明:**
> - if条件语句可以使用状态变量。
>
> - 使用if可以使子组件的渲染依赖条件语句。
>
> - 必须在容器组件内使用。
>
> - 某些容器组件限制子组件的类型或数量。将if放置在这些组件内时,这些限制将应用于if和else语句内创建的组件。例如,Grid组件的子组件仅支持GridItem组件,在Grid组件内使用if时,则if条件语句内仅允许使用GridItem组件。
## 示例
使用if条件语句:
```ts
Column() {
if (this.count > 0) {
Text('count is positive')
}
}
```
使用if、else if、else条件语句:
```ts
Column() {
if (this.count < 0) {
Text('count is negative')
} else if (this.count % 2 === 0) {
Divider()
Text('even')
} else {
Divider()
Text('odd')
}
}
```
# 语法糖
## 装饰器
装饰器@Decorator不仅可以装饰类或结构体,还可以装饰类的属性。多个装饰器可以叠加到目标元素,定义在同一行上或者在多行上,推荐定义在多行上。
如下示例为@Component和@State的使用,被 @Component装饰的元素具备了组件化的含义,使用@State装饰的变量具备了状态数据的含义。
```ts
@Component
struct MyComponent {
@State count: number = 0
}
```
装饰器定义在同一行上的描述如下:
```ts
@Entry @Component struct MyComponent {
}
```
但更推荐定义在多行上:
```ts
@Entry
@Component
struct MyComponent {
}
```
### 支持的装饰器列表
| 装饰器 | 装饰内容 | 说明 |
| ------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| @Component | struct | 结构体在装饰后具有基于组件的能力,需要实现build方法来更新UI。 |
| @Entry | struct | 组件被装饰后作为页面的入口,页面加载时将被渲染显示。 |
| @Preview | struct | 用@Preview装饰的自定义组件可以在DevEco&nbsp;Studio的预览器上进行预览,加载页面时,将创建并呈现@Preview装饰的自定义组件。 |
| @Builder | 方法 | 在@Builder装饰的方法里,通过声明式UI描述,可以在一个自定义组件内快速生成多个布局内容。 |
| @Extend | 方法 | @Extend装饰器将新的属性函数添加到内置组件上,通过@Extend装饰器可以快速定义并复用组件的自定义样式。 |
| @CustomDialog | struct | @CustomDialog装饰器用于装饰自定义弹窗。 |
| @State | 基本数据类型,类,数组 | 修饰的状态数据被修改时会触发组件的build方法进行UI界面更新。 |
| @Prop | 基本数据类型 | 修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,更新当前组件的UI。 |
| @Link | 基本数据类型,类,数组 | 父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。 |
| @Observed | 类 | @Observed应用于类,表示该类中的数据变更被UI页面管理。 |
| @ObjectLink | 被@Observed所装饰类的对象 | 装饰的状态数据被修改时,在父组件或者其他兄弟组件内与它关联的状态数据所在的组件都会更新UI。 |
| @Consume | 基本数据类型,类,数组 | @Consume装饰的变量在感知到@Provide装饰的变量更新后,会触发当前自定义组件的重新渲染。 |
| @Provide | 基本数据类型,类,数组 | @Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。 |
| @Watch | 被@State,&nbsp;@Prop,&nbsp;@Link,&nbsp;@ObjectLink,&nbsp;<br>@Provide,&nbsp;@Consume,&nbsp;@StorageProp,&nbsp;@StorageLink中任意一个装饰的变量 | @Watch用于监听状态变量的变化,应用可以注册回调方法。 |
## 链式调用
允许开发者以“.”链式调用的方式配置UI结构及其属性、事件等。
```ts
Column() {
Image('1.jpg')
.alt('error.jpg')
.width(100)
.height(100)
}.padding(10)
```
## struct对象
组件可以基于struct实现,组件不能有继承关系,struct可以比class更加快速的创建和销毁。
```ts
@Component
struct MyComponent {
@State data: string = ''
build() {
}
}
```
## 在实例化过程中省略"new"
对于struct的实例化,可以省略new。
```ts
// 定义
@Component
struct MyComponent {
build() {
}
}
// 使用
Column() {
MyComponent()
}
// 等价于
new Column() {
new MyComponent()
}
```
## TS语言在生成器函数中的使用限制
TS语言的使用在生成器函数中存在一定的限制:
- 表达式仅允许在字符串(${expression})、if条件、ForEach的参数和组件的参数中使用;
- 这些表达式中的任何一个都不能导致任何应用程序状态变量(@State、@Link、@Prop)的改变,否则会导致未定义和潜在不稳定的框架行为;
- 生成器函数内部不能有局部变量。
上述限制都不适用于事件处理函数(例如onClick)的匿名函数实现。
非法示例:
```ts
build() {
let a: number = 1 // invalid: variable declaration not allowed
Column() {
Text(`Hello ${this.myName.toUpperCase()}`) // ok.
ForEach(this.arr.reverse(), ..., ...) // invalid: Array.reverse modifies the @State array variable in place
}
buildSpecial() // invalid: no function calls
Text(this.calcTextValue()) // this function call is ok.
}
```
## 变量双向绑定
$$支持变量双向绑定,支持简单变量、@State、@Link、@Prop等类型。
当前$$仅支持[bindPopup](../reference/arkui-ts/ts-universal-attributes-popup.md)属性的show参数和@State变量之间的渲染,以及Radio组件的checked属性和Refresh组件的refreshing参数。
```ts
// xxx.ets
@Entry
@Component
struct bindPopup {
@State customPopup: boolean = false
build() {
Column() {
Button(){
Text('Popup')
}
.onClick(()=>{
this.customPopup = !this.customPopup
})
.bindPopup(
$$this.customPopup, {
message: "showPopup"
}
)
}
}
}
```
## 状态变量多种数据类型声明使用限制
@State、@Provide、 @Link和@Consume四种状态变量的多种数据类型只能同时由简单数据类型或引用数据类型其中一种构成。
示例:
```ts
@Entry
@Component
struct Index {
//错误写法: @State message: string | Resource = 'Hello World'
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(`${ this.message }`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
```
# 描述规范使用说明
本节定义了基于TypeScript (TS)扩展的声明式开发范式的核心机制和功能,讲述了声明式UI描述、组件化机制、UI状态管理、渲染控制语法和语法糖。
本节为应用开发人员开发UI提供了参考规范。有关组件的详细信息,请参考组件说明。
> **说明:**
> - 本节所有示例都以TS语言为例,请遵循相应语言的语法要求。
>
> - 示例中的Image、Button、Text、Divider、Row和Column等组件是UI框架中预置的组件控件,仅用于解释UI描述规范。
>
> - 通用属性方法和事件方法通常支持所有组件,而组件内的属性方法和事件方法仅对当前组件有效。
# 基本概念
在声明式UI编程范式中,UI是应用程序状态的函数,开发人员通过修改当前应用程序状态来更新相应的UI界面。如下图所示,开发框架提供了多种应用程序状态管理的能力。
![](figures/CoreSpec_figures_state-mgmt-overview.png)
## 状态变量装饰器
- @State:组件拥有的状态属性,当@State装饰的变量更改时,组件会重新渲染更新UI。
- @Link:组件依赖于其父组件拥有的某些状态属性,当任何一个组件中的数据更新时,另一个组件的状态都会更新,父子组件重新渲染。
- @Prop:原理类似@Link,但是子组件所做的更改不会同步到父组件上,属于单向传递。
> **说明:**
> 状态变量名称不能使用id,如@Prop id:number 。
## 应用程序状态数据
AppStorage是整个UI应用程序状态的中心“数据库”,UI框架会针对应用程序创建单例AppStorage对象,并提供相应的装饰器和接口供应用程序使用。
- @StorageLink:@StorageLink(name)的原理类似于@Consume(name),不同的是,该给定名称的链接对象是从AppStorage中获得的,在UI组件和AppStorage之间建立双向绑定同步数据。
- @StorageProp:@StorageProp(name)将UI组件属性与AppStorage进行单向同步,AppStorage中值的更改会更新组件中的属性,但UI组件无法更改AppStorage中的属性值。
- AppStorage还提供用于业务逻辑实现的API,用于添加、读取、修改和删除应用程序的状态属性,此API所做的更改会导致修改的状态数据同步到UI组件上进行UI更新。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册