diff --git a/zh-cn/application-dev/quick-start/Readme-CN.md b/zh-cn/application-dev/quick-start/Readme-CN.md index 8a17f7519768635cdb99c9b4b8a6d7005f0c328a..ba4fbe83f1460c2ebcb18e336b5b4c5cda11da99 100755 --- a/zh-cn/application-dev/quick-start/Readme-CN.md +++ b/zh-cn/application-dev/quick-start/Readme-CN.md @@ -74,8 +74,11 @@ - [其他状态管理概述](arkts-other-state-mgmt-functions-overview.md) - [\@Watch装饰器:状态变量更改通知](arkts-watch.md) - [$$语法:内置组件双向同步](arkts-two-way-sync.md) + - [MVVM模式](arkts-mvvm.md) + - [状态管理优秀实践](arkts-state-management-best-practices.md) - 渲染控制 - [渲染控制概述](arkts-rendering-control-overview.md) - [if/else:条件渲染](arkts-rendering-control-ifelse.md) - [ForEach:循环渲染](arkts-rendering-control-foreach.md) - [LazyForEach:数据懒加载](arkts-rendering-control-lazyforeach.md) + - [渲染控制优秀实践](arkts-rendering-control-best-practices.md) diff --git a/zh-cn/application-dev/quick-start/arkts-localstorage.md b/zh-cn/application-dev/quick-start/arkts-localstorage.md index f772506b80cfab021cc203442fbba05602e80184..7701bbcb38df88ccbb824ec576e615f5a07496fd 100644 --- a/zh-cn/application-dev/quick-start/arkts-localstorage.md +++ b/zh-cn/application-dev/quick-start/arkts-localstorage.md @@ -1,7 +1,7 @@ # LocalStorage:页面级UI状态存储 -LocalStorage是页面级的UI状态存储,通过\@Entry装饰器接受的参数可以在页面内共享同一个LocalStorage实例。LocalStorage也可以在UIAbility内,页面间共享状态。 +LocalStorage是页面级的UI状态存储,通过\@Entry装饰器接收的参数可以在页面内共享同一个LocalStorage实例。LocalStorage也可以在UIAbility内,页面间共享状态。 本文仅介绍LocalStorage使用场景和相关的装饰器:\@LocalStorageProp和\@LocalStorageLink。 diff --git a/zh-cn/application-dev/quick-start/arkts-mvvm.md b/zh-cn/application-dev/quick-start/arkts-mvvm.md new file mode 100644 index 0000000000000000000000000000000000000000..6185ed8d6574e2083605b4d2a4171ca768918106 --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-mvvm.md @@ -0,0 +1,1334 @@ +# MVVM模式 + + +应用通过状态去渲染更新UI是程序设计中相对复杂,但又十分重要的,往往决定了应用程序的性能。程序的状态数据通常包含了数组、对象,或者是嵌套对象组合而成。在这些情况下,ArkUI采取MVVM = Model + View + ViewModel模式,其中状态管理模块起到的就是ViewModel的作用,将数据与视图绑定在一起,更新数据的时候直接更新视图。 + + +- Model层:存储数据和相关逻辑的模型。它表示组件或其他相关业务逻辑之间传输的数据。Model是对原始数据的进一步处理。 + +- View层:在ArkUI中通常是\@Components修饰组件渲染的UI。 + +- ViewModel层:在ArkUI中,ViewModel是存储在自定义组件的状态变量、LocalStorage和AppStorage中的数据。 + - 自定义组件通过执行其build()方法或者\@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。 + - View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了\@Watch回调方法用于监听状态数据的改变。 + - 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。 + - ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。 + + +目前很多关于UI构造和更新的问题,都是由于ViewModel的设计并没有很好的支持自定义组件的渲染,或者试图去让自定义组件强行适配Model层,而中间没有用ViewModel来进行分离。例如,一个应用程序直接将SQL数据库中的数据读入内存,这种数据模型不能很好的直接适配自定义组件的渲染,所以在应用程序开发中需要适配ViewModel层。 + + +![zh-cn_image_0000001653986573](figures/zh-cn_image_0000001653986573.png) + + +根据上面涉及SQL数据库的示例,应用程序应设计为: + + +- Model:针对数据库高效操作的数据模型。 + +- ViewModel:针对ArkUI状态管理功能进行高效的UI更新的视图模型。 + +- 部署 converters/adapters: converters/adapters作用于Model和ViewModel的相互转换。 + - converters/adapters可以转换最初从数据库读取的Model,来创建并初始化ViewModel。 + - 在应用的使用场景中,UI会通过event handler改变ViewModel,此时converters/adapters需要将ViewModel的更新数据同步回Model。 + + +虽然与强制将UI拟合到SQL数据库模式(MV模式)相比,MVVM的设计比较复杂,但应用程序开发人员可以通过ViewModel层的隔离,来简化UI的设计和实现,以此来收获更好的UI性能。 + + +## ViewModel的数据源 + + +ViewModel通常包含多个顶层数据源。\@State和\@Provide装饰的变量以及LocalStorage和AppStorage都是顶层数据源,其余装饰器都是与数据源做同步的数据。装饰器的选择取决于状态需要在自定义组件之间的共享范围。共享范围从小到大的排序是: + + +- \@State:组件级别的共享,通过命名参数机制传递,例如:CompA: ({ aProp: this.aProp }),表示传递层级(共享范围)是父子之间的传递。 + +- \@Provide:组件级别的共享,可以通过key和\@Consume绑定,因此不用参数传递,实现多层级的数据共享,共享范围大于\@State。 + +- LocalStorage:页面级别的共享,可以通过\@Entry在当前组件树上共享LocalStorage实例。 + +- AppStorage:应用全局的UI状态存储,和应用进程绑定,在整个应用内的状态数据的共享。 + + +### \@State装饰的变量与一个或多个子组件共享状态数据 + + +\@State可以初始化多种状态变量,\@Prop、\@Link和\@ObjectLink可以和其建立单向或双向同步,详情见[@State使用规范](arkts-state.md)。 + + +1. 使用Parent根节点中\@State装饰的testNum作为ViewModel数据项。将testNum传递给其子组件LinkChild和Sibling。 + + ```ts + // xxx.ets + @Entry + @Component + struct Parent { + @State @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ testNum: $testNum }) + Sibling({ testNum: $testNum }) + } + } + } + ``` + +2. LinkChild和Sibling中用\@Link和父组件的数据源建立双向同步。其中LinkChild中创建了LinkLinkChild和PropLinkChild。 + + ```ts + @Component + struct Sibling { + @Link @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } + } + + @Component + struct LinkChild { + @Link @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ testNumGrand: $testNum }) + PropLinkChild({ testNumGrand: this.testNum }) + } + .height(200).width(200) + } + } + ``` + +3. LinkLinkChild和PropLinkChild声明如下,PropLinkChild中的\@Prop和其父组件建立单向同步关系。 + + ```ts + @Component + struct LinkLinkChild { + @Link @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNumGrand}`) + } + } + + + @Component + struct PropLinkChild { + @Prop @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } + } + ``` + + ![zh-cn_image_0000001638250945](figures/zh-cn_image_0000001638250945.png) + + 当LinkChild中的\@Link testNum更改时。 + + 1. 更改首先同步到其父组件Parent,然后更改从Parent同步到Siling。 + + 2. LinkChild中的\@Link testNum更改也同步给子组件LinkLinkChild和PropLinkChild。 + + \@State装饰器与\@Provide、LocalStorage、AppStorage的区别: + + - \@State如果想要将更改传递给孙子节点,需要先将更改传递给子组件,再从子节点传递给孙子节点。 + - 共享只能通过构造函数的参数传递,即命名参数机制CompA: ({ aProp: this.aProp })。 + + 完整的代码示例如下: + + + ```ts + @Component + struct LinkLinkChild { + @Link @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNumGrand}`) + } + } + + + @Component + struct PropLinkChild { + @Prop @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } + } + + + @Component + struct Sibling { + @Link @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } + } + + @Component + struct LinkChild { + @Link @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ testNumGrand: $testNum }) + PropLinkChild({ testNumGrand: this.testNum }) + } + .height(200).width(200) + } + } + + + @Entry + @Component + struct Parent { + @State @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ testNum: $testNum }) + Sibling({ testNum: $testNum }) + } + } + } + ``` + + +### \@Provide装饰的变量与任何后代组件共享状态数据 + +\@Provide装饰的变量可以与任何后代组件共享状态数据,其后代组件使用\@Consume创建双向同步,详情见[@Provide和@Consume](arkts-provide-and-consume.md)。 + +因此,\@Provide-\@Consume模式比使用\@State-\@Link-\@Link从父组件将更改传递到孙子组件更方便。\@Provide-\@Consume适合在单个页面UI组件树中共享状态数据。 + +使用\@Provide-\@Consume模式时,\@Consume和其祖先组件中的\@Provide通过绑定相同的key连接,而不是在组件的构造函数中通过参数来进行传递。 + +以下示例通过\@Provide-\@Consume模式,将更改从父组件传递到孙子组件。 + + +```ts +@Component +struct LinkLinkChild { + @Consume @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNum value ${this.testNum}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNum}`) + } +} + +@Component +struct PropLinkChild { + @Prop @Watch("testNumChange") testNumGrand: number; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } +} + +@Component +struct Sibling { + @Consume @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } +} + +@Component +struct LinkChild { + @Consume @Watch("testNumChange") testNum: number; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ /* empty */ }) + PropLinkChild({ testNumGrand: this.testNum }) + } + .height(200).width(200) + } +} + +@Entry +@Component +struct Parent { + @Provide @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ /* empty */ }) + Sibling({ /* empty */ }) + } + } +} +``` + + +### 给LocalStorage实例中对应的属性建立双向或单向同步 + +通过\@LocalStorageLink和\@LocalStorageProp,给LocalStorage实例中的属性建立双向或单向同步。可以将LocalStorage实例视为\@State变量的Map,使用详情参考LocalStorage。 + +LocalStorage对象可以在ArkUI应用程序的几个页面上共享。因此,使用\@LocalStorageLink、\@LocalStorageProp和LocalStorage可以在应用程序的多个页面上共享状态。 + +以下示例中: + +1. 创建一个LocalStorage实例,并通过\@Entry(storage)将其注入根节点。 + +2. 在Parent组件中初始化\@LocalStorageLink("testNum")变量时,将在LocalStorage实例中创建testNum属性,并设置指定的初始值为1,即\@LocalStorageLink("testNum") testNum: number = 1。 + +3. 在其子组件中,都使用\@LocalStorageLink或\@LocalStorageProp绑定同一个属性名key来传递数据。 + +LocalStorage可以被认为是\@State变量的Map,属性名作为Map中的key。 + +\@LocalStorageLink和LocalStorage中对应的属性的同步行为,和\@State和\@Link一致,都为双向数据同步。 + +以下为组件的状态更新图: + +![zh-cn_image_0000001588450934](figures/zh-cn_image_0000001588450934.png) + + +```ts +@Component +struct LinkLinkChild { + @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNum value ${this.testNum}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNum}`) + } +} + +@Component +struct PropLinkChild { + @LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } +} + +@Component +struct Sibling { + @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } +} + +@Component +struct LinkChild { + @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ /* empty */ }) + PropLinkChild({ /* empty */ }) + } + .height(200).width(200) + } +} + +// create LocalStorage object to hold the data +const storage = new LocalStorage(); +@Entry(storage) +@Component +struct Parent { + @LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ /* empty */ }) + Sibling({ /* empty */ }) + } + } +} +``` + + +### 给AppStorage中对应的属性建立双向或单向同步 + +AppStorage是LocalStorage的单例对象,ArkUI在应用程序启动时创建该对象,在页面中使用\@StorageLink和\@StorageProp为多个页面之间共享数据,具体使用方法和LocalStorage类似。 + +也可以使用PersistentStorage将AppStorage中的特定属性持久化到本地磁盘的文件中,再次启动的时候\@StorageLink和\@StorageProp会恢复上次应用退出的数据。详情请参考[PersistentStorage文档](arkts-persiststorage.md)。 + +示例如下: + + +```ts +@Component +struct LinkLinkChild { + @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`LinkLinkChild: testNum value ${this.testNum}`); + } + + build() { + Text(`LinkLinkChild: ${this.testNum}`) + } +} + +@Component +struct PropLinkChild { + @StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; + + testNumChange(propName: string): void { + console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); + } + + build() { + Text(`PropLinkChild: ${this.testNumGrand}`) + .height(70) + .backgroundColor(Color.Red) + .onClick(() => { + this.testNumGrand += 1; + }) + } +} + +@Component +struct Sibling { + @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`Sibling: testNumChange value ${this.testNum}`); + } + + build() { + Text(`Sibling: ${this.testNum}`) + } +} + +@Component +struct LinkChild { + @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; + + testNumChange(propName: string): void { + console.log(`LinkChild: testNumChange value ${this.testNum}`); + } + + build() { + Column() { + Button('incr testNum') + .onClick(() => { + console.log(`LinkChild: before value change value ${this.testNum}`); + this.testNum = this.testNum + 1 + console.log(`LinkChild: after value change value ${this.testNum}`); + }) + Text(`LinkChild: ${this.testNum}`) + LinkLinkChild({ /* empty */ + }) + PropLinkChild({ /* empty */ + }) + } + .height(200).width(200) + } +} + + +@Entry +@Component +struct Parent { + @StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; + + testNumChange1(propName: string): void { + console.log(`Parent: testNumChange value ${this.testNum}`) + } + + build() { + Column() { + LinkChild({ /* empty */ + }) + Sibling({ /* empty */ + }) + } + } +} +``` + + +## ViewModel的嵌套场景 + + +大多数情况下,ViewModel数据项都是复杂类型的,例如,对象数组、嵌套对象或者这些类型的组合。对于嵌套场景,可以使用\@Observed搭配\@Prop或者\@ObjectLink来观察变化。 + + +### \@Prop和\@ObjectLink嵌套数据结构 + +推荐设计单独的\@Component来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个\@Component,一个\@Component呈现外部数组/对象,另一个\@Component呈现嵌套在数组/对象内的类对象。 \@Prop、\@Link、\@ObjectLink修饰的变量只能观察到第一层的变化。 + +- 对于类: + - 可以观察到赋值的变化:this.obj=new ClassObj(...) + - 可以观察到对象属性的更改:this.obj.a=new ClassA(...) + - 不能观察更深层级的属性更改:this.obj.a.b = 47 + +- 对于数组: + - 可以观察到数组的整体赋值:this.arr=[...] + - 可以观察到数据项的删除、插入和替换:this.arr[1] = new ClassA(); this.arr.pop(); this.arr.push(new ClassA(...)))、this.arr.sort(...) + - 不能观察更深层级的数组变化:this.arr[1].b = 47 + +如果要观察嵌套类的内部对象的变化,可以使用\@ObjectLink或\@Prop。优先考虑\@ObjectLink,其通过嵌套对象内部属性的引用初始化自身。\@Prop会对嵌套在内部的对象的深度拷贝来进行初始化,以实现单向同步。在性能上\@Prop的深度拷贝比\@ObjectLink的引用拷贝慢很多。 + +\@ObjectLink或\@Prop可以用来存储嵌套内部的类对象,该类必须用\@Observed类装饰器装饰,否则类的属性改变并不会触发更新UI并不会刷新。\@Observed为其装饰的类实现自定义构造函数,此构造函数创建了一个类的实例,并使用ES6代理包装(由ArkUI框架实现),拦截修饰class属性的所有“get”和“set”。“set”观察属性值,当发生赋值操作时,通知ArkUI框架更新。“get”收集哪些UI组件依赖该状态变量,实现最小化UI更新。 + +如果嵌套场景中,嵌套数据内部是数组或者class时,需根据以下场景使用\@Observed类装饰器。 + +- 如果嵌套数据内部是class,直接被\@Observed装饰。 + +- 如果嵌套数据内部是数组,可以通过以下方式来观察数组变化。 + + ```ts + @Observed class ObservedArray extends Array { + constructor(args: any[]) { + super(...args); + } + /* otherwise empty */ + } + ``` + + ViewModel为外层class。 + + + ```ts + class Outer { + innerArrayProp : ObservedArray; + ... + } + ``` + + +### 嵌套数据结构中\@Prop和\@ObjectLink之的区别 + +以下示例中: + +- 父组件ViewB渲染\@State arrA:Array<ClassA>。\@State可以观察新数组的分配、数组项插入、删除和替换。 + +- 子组件ViewA渲染每一个ClassA的对象。 + +- 类装饰器\@Observed ClassA与\@ObjectLink a: ClassA。 + + - 可以观察嵌套在Array内的ClassA对象的变化。 + + - 不使用\@Observed时: + ViewB中的this.arrA[Math.floor(this.arrA.length/2)].c=10将不会被观察到,相应的ViewA组件也不会更新。 + + 对于数组中的第一个和第二个数组项,每个数组项都初始化了两个ViewA的对象,渲染了同一个ViewA实例。在一个ViewA中的属性赋值this.a.c += 1;时不会引发另外一个使用同一个ClassA初始化的ViewA的渲染更新。 + +![zh-cn_image_0000001588610894](figures/zh-cn_image_0000001588610894.png) + + +```ts +let NextID: number = 1; + +// 类装饰器@Observed装饰ClassA +@Observed +class ClassA { + public id: number; + public c: number; + + constructor(c: number) { + this.id = NextID++; + this.c = c; + } +} + +@Component +struct ViewA { + @ObjectLink a: ClassA; + label: string = "ViewA1"; + + build() { + Row() { + Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) + .onClick(() => { + // 改变对象属性 + this.a.c += 1; + }) + } + } +} + +@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() + ) + + Divider().height(10) + + if (this.arrA.length) { + ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) + ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) + } + + Divider().height(10) + + Button(`ViewB: reset array`) + .onClick(() => { + // 替换整个数组,会被@State this.arrA观察到 + this.arrA = [new ClassA(0), new ClassA(0)]; + }) + Button(`array push`) + .onClick(() => { + // 数组中插入数据,会被@State this.arrA观察到 + this.arrA.push(new ClassA(0)) + }) + Button(`array shift`) + .onClick(() => { + // 数组中移除数据,会被@State this.arrA观察到 + this.arrA.shift() + }) + Button(`ViewB: chg item property in middle`) + .onClick(() => { + // 替换数组中的某个元素,会被@State this.arrA观察到 + this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); + }) + Button(`ViewB: chg item property in middle`) + .onClick(() => { + // 改变数组中某个元素的属性c,会被ViewA中的@ObjectLink观察到 + this.arrA[Math.floor(this.arrA.length / 2)].c = 10; + }) + } + } +} +``` + +在ViewA中,将\@ObjectLink替换为\@Prop。 + + +```ts +@Component +struct ViewA { + + @Prop a: ClassA; + label : string = "ViewA1"; + + build() { + Row() { + Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) + .onClick(() => { + // change object property + this.a.c += 1; + }) + } + } +} +``` + +与用\@ObjectLink修饰不同,用\@ObjectLink修饰时,点击数组的第一个或第二个元素,后面两个ViewA会发生同步的变化。 + +\@Prop是单向数据同步,ViewA内的Button只会触发Button自身的刷新,不会传播到其他的ViewA实例中。在ViewA中的ClassA只是一个副本,并不是其父组件中\@State arrA : Array<ClassA>中的对象,也不是其他ViewA的ClassA,这使得数组的元素和ViewA中的元素表面是传入的同一个对象,实际上在UI上渲染使用的是两个互不相干的对象。 + +需要注意\@Prop和\@ObjectLink还有一个区别:\@ObjectLink装饰的变量是仅可读的,不能被赋值;\@Prop装饰的变量可以被赋值。 + +- \@ObjectLink实现双向同步,因为它是通过数据源的引用初始化的。 + +- \@Prop是单向同步,需要深拷贝数据源。 + +- 对于\@Prop赋值新的对象,就是简单地将本地的值覆写,但是对于实现双向数据同步的\@ObjectLink,覆写新的对象相当于要更新数据源中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。 + + +## MVVM应用示例 + + +以下示例深入探讨了嵌套ViewModel的应用程序设计,特别是自定义组件如何渲染一个嵌套的Object,该场景在实际的应用开发中十分常见。 + + +开发一个电话簿应用,实现功能如下: + + +- 显示联系人和本机("Me")电话号码 。 + +- 选中联系人时,进入可编辑态”Edit“,可以更新该联系人详细信息,包括电话号码,住址。 + +- 在更新联系人信息时,只有在单击保存“Save Changes”之后,才会保存更改。 + +- 可以点击删除联系人”Delete Contact“,可以在联系人列表删除该联系人。 + + +ViewModel需要包括: + + +- AddressBook(class) + - me (本机): 存储一个Person类。 + - contacts(本机联系人):存储一个Person类数组。 + + +AddressBook类声明如下: + + + +```ts +export class AddressBook { + me: Person; + contacts: ObservedArray; +​ + constructor(me: Person, contacts: Person[]) { + this.me = me; + this.contacts = new ObservedArray(contacts); + } +} +``` + + +- Person (class) + - name : string + - address : Address + - phones: ObservedArray<string> + - Address (class) + - street : string + - zip : number + - city : string + + +Address类声明如下: + + + +```ts +@Observed +export class Address { + street: string; + zip: number; + city: string; + + constructor(street: string, + zip: number, + city: string) { + this.street = street; + this.zip = zip; + this.city = city; + } +} +``` + + +Person类声明如下: + + + +```ts +@Observed +export class Person { + id_: string; + name: string; + address: Address; + phones: ObservedArray; + + constructor(name: string, + street: string, + zip: number, + city: string, + phones: string[]) { + this.id_ = `${nextId}`; + nextId++; + this.name = name; + this.address = new Address(street, zip, city); + this.phones = new ObservedArray(phones); + } +} +``` + + +需要注意的是,因为phones是嵌套属性,如果要观察到phones的变化,需要extends array,并用\@Observed修饰它。ObservedArray类的声明如下。 + + + +```ts +@Observed +export class ObservedArray extends Array { + constructor(args?: any[]) { + console.log(`ObservedArray: ${JSON.stringify(args)} `) + if (Array.isArray(args)) { + super(...args); + } else { + super(args) + } + } +} +``` + + +- selected : 对Person的引用。 + + +更新流程如下: + + +1. 在根节点PageEntry中初始化所有的数据,将me和contacts和其子组件AddressBookView建立双向数据同步,selectedPerson默认为me,需要注意,selectedPerson并不是PageEntry数据源中的数据,而是数据源中,对某一个Person的引用。 + PageEntry和AddressBookView声明如下: + + + ```ts + @Component + struct AddressBookView { + + @ObjectLink me : Person; + @ObjectLink contacts : ObservedArray; + @State selectedPerson: Person = undefined; + + aboutToAppear() { + this.selectedPerson = this.me; + } + + build() { + Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start}) { + Text("Me:") + PersonView({person: this.me, phones: this.me.phones, selectedPerson: this.$selectedPerson}) + + Divider().height(8) + + ForEach(this.contacts, + contact => { + PersonView({person: contact, phones: contact.phones, selectedPerson: this.$selectedPerson}) + }, + contact => contact.id_ + ) + + Divider().height(8) + + Text("Edit:") + PersonEditView({ selectedPerson: this.$selectedPerson, name: this.selectedPerson.name, address: this.selectedPerson.address, phones: this.selectedPerson.phones }) + } + .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) + } + } + + @Entry + @Component + struct PageEntry { + @Provide addrBook: AddressBook = new AddressBook( + new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["+358441234567", "+35891234567", "+49621234567889"]), + [ + new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["+358449876543", "+3589456789"]), + new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["+358509876543", "+358910101010"]), + new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["+358400908070", "+35894445555"]), + ]); + + build() { + Column() { + AddressBookView({ me: this.addrBook.me, contacts: this.addrBook.contacts, selectedPerson: this.addrBook.me }) + } + } + } + ``` + +2. PersonView,即电话簿中联系人姓名和首选电话的View,当用户选中,即高亮当前Person,需要同步回其父组件AddressBookView的selectedPerson,所以需要通过\@Link建立双向同步。 + PersonView声明如下: + + + ```ts + // 显示联系人姓名和首选电话 + // 为了更新电话号码,这里需要@ObjectLink person和@ObjectLink phones, + // 显示首选号码不能使用this.person.phones[0],因为@ObjectLink person只代理了Person的属性,数组内部的变化观察不到 + // 触发onClick事件更新selectedPerson + @Component + struct PersonView { + + @ObjectLink person : Person; + @ObjectLink phones : ObservedArray; + + @Link selectedPerson : Person; + + build() { + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + Text(this.person.name) + if (this.phones.length) { + Text(this.phones[0]) + } + } + .height(55) + .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") + .onClick(() => { + this.selectedPerson = this.person; + }) + } + } + ``` + +3. 选中的Person会在PersonEditView中显示详细信息,对于PersonEditView的数据同步分为以下三种方式: + + - 在Edit状态通过Input.onChange回调事件接受用户的键盘输入时,在点击“Save Changes”之前,这个修改是不希望同步会数据源的,但又希望刷新在当前的PersonEditView中,所以\@Prop深拷贝当前Person的详细信息; + + - PersonEditView通过\@Link seletedPerson: Person和AddressBookView的``selectedPerson建立双向同步,当用户点击“Save Changes”的时候,\@Prop的修改将被赋值给\@Link seletedPerson: Person,这就意味这,数据将被同步回数据源。 + + - PersonEditView中通过\@Consume addrBook: AddressBook和根节点PageEntry建立跨组件层级的直接的双向同步关系,当用户在PersonEditView界面删除某一个联系人时,会直接同步回PageEntry,PageEntry的更新会通知AddressBookView刷新contracts的列表页。 PersonEditView声明如下: + + ```ts + // 渲染Person的详细信息 + // @Prop装饰的变量从父组件AddressBookView深拷贝数据,将变化保留在本地, TextInput的变化只会在本地副本上进行修改。 + // 点击 "Save Changes" 会将所有数据的复制通过@Prop到@Link, 同步到其他组件 + @Component + struct PersonEditView { + + @Consume addrBook : AddressBook; + + /* 指向父组件selectedPerson的引用 */ + @Link selectedPerson: Person; + + /*在本地副本上编辑,直到点击保存*/ + @Prop name: string; + @Prop address : Address; + @Prop phones : ObservedArray; + + selectedPersonIndex() : number { + return this.addrBook.contacts.findIndex((person) => person.id_ == this.selectedPerson.id_); + } + + build() { + Column() { + TextInput({ text: this.name}) + .onChange((value) => { + this.name = value; + }) + TextInput({text: this.address.street}) + .onChange((value) => { + this.address.street = value; + }) + + TextInput({text: this.address.city}) + .onChange((value) => { + this.address.city = value; + }) + + TextInput({text: this.address.zip.toString()}) + .onChange((value) => { + const result = parseInt(value); + this.address.zip= isNaN(result) ? 0 : result; + }) + + if(this.phones.length>0) { + ForEach(this.phones, + (phone, index) => { + TextInput({text: phone}) + .width(150) + .onChange((value) => { + console.log(`${index}. ${value} value has changed`) + this.phones[index] = value; + }) + }, + (phone, index) => `${index}-${phone}` + ) + } + + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + Text("Save Changes") + .onClick(() => { + // 将本地副本更新的值赋值给指向父组件selectedPerson的引用 + // 避免创建新对象,在现有属性上进行修改 + this.selectedPerson.name = this.name; + this.selectedPerson.address.street = this.address.street + this.selectedPerson.address.city = this.address.city + this.selectedPerson.address.zip = this.address.zip + this.phones.forEach((phone : string, index : number) => { this.selectedPerson.phones[index] = phone } ); + }) + if (this.selectedPersonIndex()!=-1) { + Text("Delete Contact") + .onClick(() => { + let index = this.selectedPersonIndex(); + console.log(`delete contact at index ${index}`); + + // 删除当前联系人 + this.addrBook.contacts.splice(index, 1); + + // 删除当前selectedPerson,选中态前移一位 + index = (index < this.addrBook.contacts.length) ? index : index-1; + + // 如果contract被删除完,则设置me为选中态 + this.selectedPerson = (index>=0) ? this.addrBook.contacts[index] : this.addrBook.me; + }) + } + } + + } + } + } + ``` + + 其中在关于\@ObjectLink和\@Link的区别要注意以下几点: + + 1. 在AddressBookView中实现和父组件PageView的双向同步,需要用\@ObjectLink me : Person和\@ObjectLink contacts : ObservedArray<Person>,而不能用\@Link,原因如下: + - \@Link需要和其数据源类型完全相同,且仅能观察到第一层的变化; + - \@ObjectLink可以被数据源的属性初始化,且代理了\@Observed装饰类的属性,可以观察到被装饰类属性的变化。 + 2. 当 联系人姓名 (Person.name) 或者首选电话号码 (Person.phones[0]) 发生更新时,PersonView也需要同步刷新,其中Person.phones[0]属于第二层的更新,如果使用\@Link将无法观察到,而且\@Link需要和其数据源类型完全相同。所以在PersonView中也需要使用\@ObjectLink,即\@ObjectLink person : Person和\@ObjectLink phones : ObservedArray<string>。 + + ![zh-cn_image_0000001605293914](figures/zh-cn_image_0000001605293914.png) + + 在这个例子中,我们可以大概了解到如何构建ViewModel,在应用的根节点中,ViewModel的数据可能是可以巨大的嵌套数据,但是在ViewModel和View的适配和渲染中,我们尽可能将ViewModel的数据项和View相适配,这样的话在针对每一层的View,都是一个相对“扁平”的数据,仅观察当前层就可以了。 + + 在应用实际开发中,也许我们无法避免去构建一个十分庞大的Model,但是我们可以在UI树状结构中合理地去拆分数据,使得ViewModel和View更好的适配,从而搭配最小化更新来实现高性能开发。 + + 完整应用代码如下: + + + ```ts + // ViewModel classes + let nextId = 0; + + @Observed + export class ObservedArray extends Array { + constructor(args?: any[]) { + console.log(`ObservedArray: ${JSON.stringify(args)} `) + if (Array.isArray(args)) { + super(...args); + } else { + super(args) + } + } + } + + @Observed + export class Address { + street: string; + zip: number; + city: string; + + constructor(street: string, + zip: number, + city: string) { + this.street = street; + this.zip = zip; + this.city = city; + } + } + + @Observed + export class Person { + id_: string; + name: string; + address: Address; + phones: ObservedArray; + + constructor(name: string, + street: string, + zip: number, + city: string, + phones: string[]) { + this.id_ = `${nextId}`; + nextId++; + this.name = name; + this.address = new Address(street, zip, city); + this.phones = new ObservedArray(phones); + } + } + + + export class AddressBook { + me: Person; + contacts: ObservedArray; + + constructor(me: Person, contacts: Person[]) { + this.me = me; + this.contacts = new ObservedArray(contacts); + } + } + + //渲染出Person对象的名称和手机Observed数组中的第一个号码 + //为了更新电话号码,这里需要@ObjectLink person和@ObjectLink phones, + //不能使用this.person.phones,内部数组的更改不会被观察到。 + // 在AddressBookView、PersonEditView中的onClick更新selectedPerson + @Component + struct PersonView { + @ObjectLink person: Person; + @ObjectLink phones: ObservedArray; + @Link selectedPerson: Person; + + build() { + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + Text(this.person.name) + if (this.phones.length) { + Text(this.phones[0]) + } + } + .height(55) + .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") + .onClick(() => { + this.selectedPerson = this.person; + }) + } + } + + // 渲染Person的详细信息 + // @Prop装饰的变量从父组件AddressBookView深拷贝数据,将变化保留在本地, TextInput的变化只会在本地副本上进行修改。 + // 点击 "Save Changes" 会将所有数据的复制通过@Prop到@Link, 同步到其他组件 + @Component + struct PersonEditView { + @Consume addrBook: AddressBook; + + /* 指向父组件selectedPerson的引用 */ + @Link selectedPerson: Person; + + /*在本地副本上编辑,直到点击保存*/ + @Prop name: string; + @Prop address: Address; + @Prop phones: ObservedArray; + + selectedPersonIndex(): number { + return this.addrBook.contacts.findIndex((person) => person.id_ == this.selectedPerson.id_); + } + + build() { + Column() { + TextInput({ text: this.name }) + .onChange((value) => { + this.name = value; + }) + TextInput({ text: this.address.street }) + .onChange((value) => { + this.address.street = value; + }) + + TextInput({ text: this.address.city }) + .onChange((value) => { + this.address.city = value; + }) + + TextInput({ text: this.address.zip.toString() }) + .onChange((value) => { + const result = parseInt(value); + this.address.zip = isNaN(result) ? 0 : result; + }) + + if (this.phones.length > 0) { + ForEach(this.phones, + (phone, index) => { + TextInput({ text: phone }) + .width(150) + .onChange((value) => { + console.log(`${index}. ${value} value has changed`) + this.phones[index] = value; + }) + }, + (phone, index) => `${index}-${phone}` + ) + } + + Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { + Text("Save Changes") + .onClick(() => { + // 将本地副本更新的值赋值给指向父组件selectedPerson的引用 + // 避免创建新对象,在现有属性上进行修改 + this.selectedPerson.name = this.name; + this.selectedPerson.address.street = this.address.street + this.selectedPerson.address.city = this.address.city + this.selectedPerson.address.zip = this.address.zip + this.phones.forEach((phone: string, index: number) => { + this.selectedPerson.phones[index] = phone + }); + }) + if (this.selectedPersonIndex() != -1) { + Text("Delete Contact") + .onClick(() => { + let index = this.selectedPersonIndex(); + console.log(`delete contact at index ${index}`); + + // 删除当前联系人 + this.addrBook.contacts.splice(index, 1); + + // 删除当前selectedPerson,选中态前移一位 + index = (index < this.addrBook.contacts.length) ? index : index - 1; + + // 如果contract被删除完,则设置me为选中态 + this.selectedPerson = (index >= 0) ? this.addrBook.contacts[index] : this.addrBook.me; + }) + } + } + + } + } + } + + @Component + struct AddressBookView { + @ObjectLink me: Person; + @ObjectLink contacts: ObservedArray; + @State selectedPerson: Person = undefined; + + aboutToAppear() { + this.selectedPerson = this.me; + } + + build() { + Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) { + Text("Me:") + PersonView({ person: this.me, phones: this.me.phones, selectedPerson: this.$selectedPerson }) + + Divider().height(8) + + ForEach(this.contacts, + contact => { + PersonView({ person: contact, phones: contact.phones, selectedPerson: this.$selectedPerson }) + }, + contact => contact.id_ + ) + + Divider().height(8) + + Text("Edit:") + PersonEditView({ + selectedPerson: this.$selectedPerson, + name: this.selectedPerson.name, + address: this.selectedPerson.address, + phones: this.selectedPerson.phones + }) + } + .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) + } + } + + @Entry + @Component + struct PageEntry { + @Provide addrBook: AddressBook = new AddressBook( + new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["+358441234567", "+35891234567", "+49621234567889"]), + [ + new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["+358449876543", "+3589456789"]), + new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["+358509876543", "+358910101010"]), + new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["+358400908070", "+35894445555"]), + ]); + + build() { + Column() { + AddressBookView({ me: this.addrBook.me, contacts: this.addrBook.contacts, selectedPerson: this.addrBook.me }) + } + } + } + ``` \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/arkts-rendering-control-best-practices.md b/zh-cn/application-dev/quick-start/arkts-rendering-control-best-practices.md new file mode 100644 index 0000000000000000000000000000000000000000..5afba0367e9bf5c41c6d9c168faa95083e9d9faf --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-rendering-control-best-practices.md @@ -0,0 +1,110 @@ +# 渲染控制优秀实践 + + +为了帮助应用程序开发人员提高其应用程序质量,本章节面向开发者提供了多个在开发ArkUI应用中常见场景和易错问题,并给出了对应的解决方案。此外,还提供了同一场景下,推荐用法和不推荐用法的对比和解释说明,更直观地展示两者区别,从而帮助开发者学习如果正确地在应用开发中使用渲染控制,进行高性能开发。 + + +## 在ForEach数据源中添加元素 + +在ForEach数据源中添加元素导致数组项ID重复。 + + +### 不推荐用法 + + 下面示例使用ForEach方法迭代数组this.arr的每个元素,在Text组件进行显示,并在单击Text('Add arr element')时添加新的数组元素。 + +```ts +@Entry +@Component +struct Index { + @State arr: number[] = [1,2,3]; + + build() { + Column() { + ForEach(this.arr, + (item) => { + Text(`Item ${item}`) + }, + item => item.toString()) + Text('Add arr element') + .fontSize(20) + .onClick(()=>{ + this.arr.push(4); // arr新增的元素,其在ForEach内的键值均为'4' + console.log("Arr elements: ", this.arr); + }) + } + } +} +``` + +点击两次Text('Add arr element')时,数组this.arr每次都会添加新元素 4。但是在ForEach循环渲染中,第三个参数(item)=> item.toString()需要生成Array每一个item对应的id值。该Array Id被要求是唯一的和稳定的。 + +- 唯一性:键值生成函数生成的每个数组项的id是不同的。 + +- 稳定性:当数组项ID发生变化时,ArkUI框架认为该数组项被替换或更改。 + +- ArkUI框架会对重复的ID告警,这种情况下框架的行为是未知的,特别是UI的更新在该场景下可能不起作用。 + +因此上述示例中,框架不会显示第二次及以后新添加的文本元素。因为这个元素不再是唯一的,他们都含有相同的id4。 但是如果删除ForEach第三个键值生成函数(item) => item.toString(),则触发onClick事件后每一个新添加的Text元素都会得到更新。这是因为框架使用了默认的Array id生成函数,即(item: any, index : number) => '${index}__${JSON.stringify(item)}'。它的兼容性更好但可能会导致不必要的UI更新,因此仍建议应用定义自己的键值生成函数。 + + +## ForEach数据源更新 + +ForEach数据源更新时,数组项ID与原数组项ID重复不会重新创建该数组项。 + + +### 不推荐用法 + +下面的示例定义了Index和Child两个组件。父组件Index有arr数组成员变量,初始值包含数字1、2、3。Child定义\@Prop value,接收父组件中arr数组中的一个元素。 + + +```ts +@Component +struct Child { + @Prop value: number; + build() { + Text(`${this.value}`) + .fontSize(50) + .onClick(() => { + this.value++ // 点击改变@Prop的值 + }) + } +} +@Entry +@Component +struct Index { + @State arr: number[] = [1, 2, 3]; + build() { + Row() { + Column() { + // 对照组 + Child({ value: this.arr[0] }) + Child({ value: this.arr[1] }) + Child({ value: this.arr[2] }) + Divider().height(5) + ForEach(this.arr, + item => { + Child({ value: item }) + }, + item => item.toString() // 键值,标识id + ) + Text('Parent: replace entire arr') + .fontSize(50) + .onClick(() => { + // 两个数组项内均含有'3',ForEach内的id没有发生变化 + // 意味着ForEach不会更新该Child实例,@Prop也不会在父组件处被更新 + this.arr = (this.arr[0] == 1) ? [3, 4, 5] : [1, 2, 3]; + }) + } + } + } +} +``` + +当触发文本组件Parent: replace entire arr的onClick事件时,状态变量数组arr根据自身第一个元素的值将被[3, 4, 5]或[1, 2, 3]替换,但是ForEach里初始被创建的\@Prop传入值为3的Child组件并不会更新。 + +因为,老数组和新数组初始均含有同一个值的元素(数字3),而且该元素生成的标识id在父组件里没有变化。因此ForEach没有识别出对应的Child实例需要被新的输入值更新,对应的子组件内\@Prop也没有更新。 + +![zh-cn_image_0000001604900446](figures/zh-cn_image_0000001604900446.png) + +可以在arr中用一个唯一的元素代替重复的元素3来观察本事件的行为表现。当恰当的替换掉数组时,接下来应用的表现才是符合预期的。 \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/arkts-state-management-best-practices.md b/zh-cn/application-dev/quick-start/arkts-state-management-best-practices.md new file mode 100644 index 0000000000000000000000000000000000000000..24b1660cb6eb7175cd5c5a0ea2e01b1752f16ece --- /dev/null +++ b/zh-cn/application-dev/quick-start/arkts-state-management-best-practices.md @@ -0,0 +1,1025 @@ +# 状态管理优秀实践 + + +为了帮助应用程序开发人员提高其应用程序质量,特别是在高效的状态管理方面。本章节面向开发者提供了多个在开发ArkUI应用中常见场景和易错问题,并给出了对应的解决方案。此外,还提供了同一场景下,推荐用法和不推荐用法的对比和解释说明,更直观地展示两者区别,从而帮助开发者学习如果正确地在应用开发中使用状态变量,进行高性能开发。 + + +## 基础示例 + +下面的例子是关于\@Prop,\@Link,\@ObjectLink的初始化规则的,在学习下面这个例子前,我们首先需要了解: + +- \@Prop:可以被父组件的\@State初始化,或者\@State是复杂类型Object和class时的属性,或者是数组时的数组项。 + +- \@ObjectLink:初始化规则和\@Prop相同,但需要被\@Observed装饰class的实例初始化。 + +- \@Link:必须和\@State或其他数据源类型完全相同。 + + +### 不推荐用法 + + + +```ts +@Observed +class ClassA { + public c: number = 0; + + constructor(c: number) { + this.c = c; + } +} + +@Component +struct LinkChild { + @Link testNum: number; + + build() { + Text(`LinkChild testNum ${this.testNum}`) + } +} + + +@Component +struct PropChild2 { + @Prop testNum: ClassA; + + build() { + Text(`PropChild2 testNum ${this.testNum.c}`) + .onClick(() => { + this.testNum.c += 1; + }) + } +} + +@Component +struct PropChild3 { + @Prop testNum: ClassA; + + build() { + Text(`PropChild3 testNum ${this.testNum.c}`) + } +} + +@Component +struct ObjectLinkChild { + @ObjectLink testNum: ClassA; + + build() { + Text(`ObjectLinkChild testNum ${this.testNum.c}`) + .onClick(() => { + // 问题4:ObjectLink不能被赋值 + this.testNum = new ClassA(47); + }) + } +} + +@Entry +@Component +struct Parent { + @State testNum: ClassA[] = [new ClassA(1)]; + + build() { + Column() { + Text(`Parent testNum ${this.testNum.c}`) + .onClick(() => { + this.testNum[0].c += 1; + }) + // 问题1:@Link装饰的变量需要和数据源@State类型一致 + LinkChild({ testNum: this.testNum.c }) + + // 问题2:@Prop本地没有初始化,也没有从父组件初始化 + PropChild2() + + // 问题3:PropChild3没有改变@Prop testNum: ClassA的值,所以这时最优的选择是使用@ObjectLink + PropChild3({ testNum: this.testNum[0] }) + + ObjectLinkChild({ testNum: this.testNum[0] }) + } + } +} +``` + + +上面的例子有以下几个错误: + + +1. \@Component LinkChild:\@Link testNum: number从父组件的LinkChild({testNum:this.testNum.c})。\@Link的数据源必须是装饰器装饰的状态变量,简而言之,\@Link装饰的数据必须和数据源类型相同,比如\@Link: T和\@State : T。所以,这里应该改为\@Link testNum: ClassA,从父组件初始化的方式为LinkChild({testNum: $testNum})。 + +2. \@Component PropChild2:\@Prop可以本地初始化,也可以从父组件初始化,但是必须初始化,对于\@Prop testNum: ClassA没有本地初始化,所以必须从父组件初始化PropChild1({testNum: this.testNum})。 + +3. \@Component PropChild3:没有改变\@Prop testNum: ClassA的值,所以这时较优的选择是使用\@ObjectLink,因为\@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候\@ObjectLink是比\@Link和\@Prop更优的选择。 + +4. 点击ObjectLinkChild给\@ObjectLink装饰的变量赋值:this.testNum = new ClassA(47); 也是不允许的,对于实现双向数据同步的\@ObjectLink,赋值相当于要更新父组件中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。框架对于这种行为会发生运行时报错。 + +5. 如果是非嵌套场景,比如Parent里声明的变量为 \@State testNum: ClassA = new ClassA(1),ClassA就不需要被\@Observed修饰,因为\@State已经具备了观察第一层变化的能力,不需要再使用\@Observed来加一层代理。 + + +### 推荐用法 + + + +```ts +@Observed +class ClassA { + public c: number = 0; + + constructor(c: number) { + this.c = c; + } +} + +@Component +struct LinkChild { + @Link testNum: ClassA; + + build() { + Text(`LinkChild testNum ${this.testNum?.c}`) + } +} + +@Component +struct PropChild1 { + @Prop testNum: ClassA = new ClassA(1); + + build() { + Text(`PropChild1 testNum ${this.testNum?.c}`) + .onClick(() => { + this.testNum = new ClassA(48); + }) + } +} + +@Component +struct ObjectLinkChild { + @ObjectLink testNum: ClassA; + + build() { + Text(`ObjectLinkChild testNum ${this.testNum.c}`) + // @ObjectLink装饰的变量可以更新属性 + .onClick(() => { + this.testNum.c += 1; + }) + } +} + +@Entry +@Component +struct Parent { + @State testNum: ClassA[] = [new ClassA(1)]; + + build() { + Column() { + Text(`Parent testNum ${this.testNum.c}`) + .onClick(() => { + this.testNum[0].c += 1; + }) + // @Link装饰的变量需要和数据源@State类型一致 + LinkChild({ testNum: this.testNum[0] }) + + // @Prop本地有初始化,不需要再从父组件初始化 + PropChild1() + + // 当子组件不需要发生本地改变时,优先使用@ObjectLink,因为@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候@ObjectLink是比@Link和@Prop更优的选择 + ObjectLinkChild({ testNum: this.testNum[0] }) + } + } +} +``` + + + +## 基础嵌套对象属性更改失效 + +在应用开发中,有很多嵌套对象场景,例如,开发者更新了某个属性,但UI没有进行对应的更新。 + +每个装饰器都有自己可以观察的能力,并不是所有的改变都可以被观察到,只有可以被观察到的变化才会进行UI更新。\@Observed装饰器可以观察到嵌套对象的属性变化,其他装饰器仅能观察到第二层的变化。 + + +### 不推荐用法 + +下面的例子中,一些UI组件并不会更新。 + + +```ts +class ClassA { + a: number; + + constructor(a: number) { + this.a = a; + } + + getA(): number { + return this.a; + } + + setA(a: number): void { + this.a = a; + } +} + +class ClassC { + c: number; + + constructor(c: number) { + this.c = c; + } + + getC(): number { + return this.c; + } + + setC(c: number): void { + this.c = c; + } +} + +class ClassB extends ClassA { + b: number = 47; + c: ClassC; + + constructor(a: number, b: number, c: number) { + super(a); + this.b = b; + this.c = new ClassC(c); + } + + getB(): number { + return this.b; + } + + setB(b: number): void { + this.b = b; + } + + getC(): number { + return this.c.getC(); + } + + setC(c: number): void { + return this.c.setC(c); + } +} + + +@Entry +@Component +struct MyView { + @State b: ClassB = new ClassB(10, 20, 30); + + build() { + Column({ space: 10 }) { + Text(`a: ${this.b.a}`) + Button("Change ClassA.a") + .onClick(() => { + this.b.a += 1; + }) + + Text(`b: ${this.b.b}`) + Button("Change ClassB.b") + .onClick(() => { + this.b.b += 1; + }) + + Text(`c: ${this.b.c.c}`) + Button("Change ClassB.ClassC.c") + .onClick(() => { + // 点击时上面的Text组件不会刷新 + this.b.c.c += 1; + }) + } + } +} +``` + +- 最后一个Text组件Text('c: ${this.b.c.c}'),当点击该组件时UI不会刷新。 因为,\@State b : ClassB 只能观察到this.b属性的变化,比如this.b.a, this.b.b 和this.b.c的变化,但是无法观察嵌套在属性中的属性,即this.b.c.c(属性c是内嵌在b中的对象classC的属性)。 + +- 为了观察到嵌套与内部的ClassC的属性,需要做如下改变: + - 构造一个子组件,用于单独渲染ClassC的实例。 该子组件可以使用\@ObjectLink c : ClassC或\@Prop c : ClassC。通常会使用\@ObjectLink,除非子组件需要对其ClassC对象进行本地修改。 + - 嵌套的ClassC必须用\@Observed修饰。当在ClassB中创建ClassC对象时(本示例中的ClassB(10, 20, 30)),它将被包装在ES6代理中,当ClassC属性更改时(this.b.c.c += 1),该代码将修改通知到\@ObjectLink变量。 + + +### 推荐用法 + +以下示例使用\@Observed/\@ObjectLink来观察嵌套对象的属性更改。 + + +```ts +class ClassA { + a: number; + constructor(a: number) { + this.a = a; + } + getA() : number { + return this.a; } + setA( a: number ) : void { + this.a = a; } +} + +@Observed +class ClassC { + c: number; + constructor(c: number) { + this.c = c; + } + getC() : number { + return this.c; } + setC(c : number) : void { + this.c = c; } +} + +class ClassB extends ClassA { + b: number = 47; + c: ClassC; + + constructor(a: number, b: number, c: number) { + super(a); + this.b = b; + this.c = new ClassC(c); + } + + getB() : number { + return this.b; } + setB(b : number) : void { + this.b = b; } + getC() : number { + return this.c.getC(); } + setC(c : number) : void { + return this.c.setC(c); } +} + +@Component +struct ViewClassC { + + @ObjectLink c : ClassC; + build() { + Column({space:10}) { + Text(`c: ${this.c.getC()}`) + Button("Change C") + .onClick(() => { + this.c.setC(this.c.getC()+1); + }) + } + } +} + +@Entry +@Component +struct MyView { + @State b : ClassB = new ClassB(10, 20, 30); + + build() { + Column({space:10}) { + Text(`a: ${this.b.a}`) + Button("Change ClassA.a") + .onClick(() => { + this.b.a +=1; + }) + + Text(`b: ${this.b.b}`) + Button("Change ClassB.b") + .onClick(() => { + this.b.b += 1; + }) + + ViewClassC({c: this.b.c}) // Text(`c: ${this.b.c.c}`)的替代写法 + Button("Change ClassB.ClassC.c") + .onClick(() => { + this.b.c.c += 1; + }) + } + } +} +``` + + + +## 复杂嵌套对象属性更改失效 + + +### 不推荐用法 + +以下示例创建了一个带有\@ObjectLink装饰变量的子组件,用于渲染一个含有嵌套属性的ParentCounter,用\@Observed装饰嵌套在ParentCounter中的SubCounter。 + + +```ts +let nextId = 1; +@Observed +class SubCounter { + counter: number; + constructor(c: number) { + this.counter = c; + } +} +@Observed +class ParentCounter { + id: number; + counter: number; + subCounter: SubCounter; + incrCounter() { + this.counter++; + } + incrSubCounter(c: number) { + this.subCounter.counter += c; + } + setSubCounter(c: number): void { + this.subCounter.counter = c; + } + constructor(c: number) { + this.id = nextId++; + this.counter = c; + this.subCounter = new SubCounter(c); + } +} +@Component +struct CounterComp { + @ObjectLink value: ParentCounter; + build() { + Column({ space: 10 }) { + Text(`${this.value.counter}`) + .fontSize(25) + .onClick(() => { + this.value.incrCounter(); + }) + Text(`${this.value.subCounter.counter}`) + .onClick(() => { + this.value.incrSubCounter(1); + }) + Divider().height(2) + } + } +} +@Entry +@Component +struct ParentComp { + @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; + build() { + Row() { + Column() { + CounterComp({ value: this.counter[0] }) + CounterComp({ value: this.counter[1] }) + CounterComp({ value: this.counter[2] }) + Divider().height(5) + ForEach(this.counter, + item => { + CounterComp({ value: item }) + }, + item => item.id.toString() + ) + Divider().height(5) + // 第一个点击事件 + Text('Parent: incr counter[0].counter') + .fontSize(20).height(50) + .onClick(() => { + this.counter[0].incrCounter(); + // 每次触发时自增10 + this.counter[0].incrSubCounter(10); + }) + // 第二个点击事件 + Text('Parent: set.counter to 10') + .fontSize(20).height(50) + .onClick(() => { + // 无法将value设置为10,UI不会刷新 + this.counter[0].setSubCounter(10); + }) + Text('Parent: reset entire counter') + .fontSize(20).height(50) + .onClick(() => { + this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; + }) + } + } + } +} +``` + +对于Text('Parent: incr counter[0].counter')的onClick事件,this.counter[0].incrSubCounter(10)调用incrSubCounter方法使SubCounter的counter值增加10,UI同步刷新。 + +但是,在Text('Parent: set.counter to 10')的onClick中调用this.counter[0].setSubCounter(10),SubCounter的counter值却无法重置为10。 + +incrSubCounter和setSubCounter都是同一个SubCounter的函数。在第一个点击处理时调用incrSubCounter可以正确更新UI,而第二个点击处理调用setSubCounter时却没有更新UI。实际上incrSubCounter和setSubCounter两个函数都不能触发Text('${this.value.subCounter.counter}')的更新,因为\@ObjectLink value : ParentCounter仅能观察其代理ParentCounter的属性,对于this.value.subCounter.counter是SubCounter的属性,无法观察到嵌套类的属性。 + +但是,第一个click事件调用this.counter[0].incrCounter()将CounterComp自定义组件中\@ObjectLink value: ParentCounter标记为已更改。此时触发Text('${this.value.subCounter.counter}')的更新。 如果在第一个点击事件中删除this.counter[0].incrCounter(),也无法更新UI。 + + +### 推荐用法 + +对于上述问题,为了直接观察SubCounter中的属性,以便this.counter[0].setSubCounter(10)操作有效,可以利用下面的方法: + + +```ts +@ObjectLink value:ParentCounter; +@ObjectLink subValue:SubCounter; +``` + +该方法使得\@ObjectLink分别代理了ParentCounter和SubCounter的属性,这样对于这两个类的属性的变化都可以观察到,即都会对UI视图进行刷新。即使删除了上面所说的this.counter[0].incrCounter(),UI也会进行正确的刷新。 + +该方法可用于实现“两个层级”的观察,即外部对象和内部嵌套对象的观察。但是该方法只能用于\@ObjectLink装饰器,无法作用于\@Prop(\@Prop通过深拷贝传入对象)。详情参考@Prop与@ObjectLink的差异。 + + +```ts +let nextId = 1; +@Observed +class SubCounter { + counter: number; + constructor(c: number) { + this.counter = c; + } +} +@Observed +class ParentCounter { + id: number; + counter: number; + subCounter: SubCounter; + incrCounter() { + this.counter++; + } + incrSubCounter(c: number) { + this.subCounter.counter += c; + } + setSubCounter(c: number): void { + this.subCounter.counter = c; + } + constructor(c: number) { + this.id = nextId++; + this.counter = c; + this.subCounter = new SubCounter(c); + } +} +@Component +struct CounterComp { + @ObjectLink value: ParentCounter; + @ObjectLink subValue: SubCounter; + build() { + Column({ space: 10 }) { + Text(`${this.value.counter}`) + .fontSize(25) + .onClick(() => { + this.value.incrCounter(); + }) + Text(`${this.subValue.counter}`) + .onClick(() => { + this.subValue.counter += 1; + }) + Divider().height(2) + } + } +} +@Entry +@Component +struct ParentComp { + @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; + build() { + Row() { + Column() { + CounterComp({ value: this.counter[0], subValue: this.counter[0].subCounter }) + CounterComp({ value: this.counter[1], subValue: this.counter[1].subCounter }) + CounterComp({ value: this.counter[2], subValue: this.counter[2].subCounter }) + Divider().height(5) + ForEach(this.counter, + item => { + CounterComp({ value: item, subValue: item.subCounter }) + }, + item => item.id.toString() + ) + Divider().height(5) + Text('Parent: reset entire counter') + .fontSize(20).height(50) + .onClick(() => { + this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; + }) + Text('Parent: incr counter[0].counter') + .fontSize(20).height(50) + .onClick(() => { + this.counter[0].incrCounter(); + this.counter[0].incrSubCounter(10); + }) + Text('Parent: set.counter to 10') + .fontSize(20).height(50) + .onClick(() => { + this.counter[0].setSubCounter(10); + }) + } + } + } +} +``` + + +## \@Prop与\@ObjectLink的差异 + +在下面的示例代码中,\@ObjectLink修饰的变量是对数据源的引用,即在this.value.subValue和this.subValue都是同一个对象的不同引用,所以在点击CounterComp的click handler,改变this.value.subCounter.counter,this.subValue.counter也会改变,对应的组件Text(`this.subValue.counter: ${this.subValue.counter}`)会刷新。 + + +```ts +let nextId = 1; + +@Observed +class SubCounter { + counter: number; + constructor(c: number) { + this.counter = c; + } +} + +@Observed +class ParentCounter { + id: number; + counter: number; + subCounter: SubCounter; + incrCounter() { + this.counter++; + } + incrSubCounter(c: number) { + this.subCounter.counter += c; + } + setSubCounter(c: number): void { + this.subCounter.counter = c; + } + constructor(c: number) { + this.id = nextId++; + this.counter = c; + this.subCounter = new SubCounter(c); + } +} + +@Component +struct CounterComp { + @ObjectLink value: ParentCounter; + @ObjectLink subValue: SubCounter; + build() { + Column({ space: 10 }) { + Text(`this.subValue.counter: ${this.subValue.counter}`) + .fontSize(30) + Text(`this.value.counter:increase 7 `) + .fontSize(30) + .onClick(() => { + // click handler, Text(`this.subValue.counter: ${this.subValue.counter}`) will update + this.value.incrSubCounter(7); + }) + Divider().height(2) + } + } +} + +@Entry +@Component +struct ParentComp { + @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; + build() { + Row() { + Column() { + CounterComp({ value: this.counter[0], subValue: this.counter[0].subCounter }) + CounterComp({ value: this.counter[1], subValue: this.counter[1].subCounter }) + CounterComp({ value: this.counter[2], subValue: this.counter[2].subCounter }) + Divider().height(5) + ForEach(this.counter, + item => { + CounterComp({ value: item, subValue: item.subCounter }) + }, + item => item.id.toString() + ) + Divider().height(5) + Text('Parent: reset entire counter') + .fontSize(20).height(50) + .onClick(() => { + this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; + }) + Text('Parent: incr counter[0].counter') + .fontSize(20).height(50) + .onClick(() => { + this.counter[0].incrCounter(); + this.counter[0].incrSubCounter(10); + }) + Text('Parent: set.counter to 10') + .fontSize(20).height(50) + .onClick(() => { + this.counter[0].setSubCounter(10); + }) + } + } + } +} +``` + +\@ObjectLink图示如下: + +![zh-cn_image_0000001651665921](figures/zh-cn_image_0000001651665921.png) + + +### 不推荐用法 + +如果用\@Prop替代\@ObjectLink。点击第一个click handler,UI刷新正常。但是点击第二个onClick事件,\@Prop 对变量做了一个本地拷贝,CounterComp的第一个Text并不会刷新。 + + this.value.subCounter和this.subValue并不是同一个对象。所以this.value.subCounter的改变,并没有改变this.subValue的拷贝对象,Text(`this.subValue.counter: ${this.subValue.counter}`)不会刷新。 + +```ts +@Component +struct CounterComp { + @Prop value: ParentCounter; + @Prop subValue: SubCounter; + build() { + Column({ space: 10 }) { + Text(`this.subValue.counter: ${this.subValue.counter}`) + .fontSize(20) + .onClick(() => { + // 1st click handler + this.subValue.counter += 7; + }) + Text(`this.value.counter:increase 7 `) + .fontSize(20) + .onClick(() => { + // 2nd click handler + this.value.incrSubCounter(7); + }) + Divider().height(2) + } + } +} +``` + +\@Prop拷贝的关系图示如下: + +![zh-cn_image_0000001602146116](figures/zh-cn_image_0000001602146116.png) + + +### 推荐用法 + +可以通过从ParentComp到CounterComp仅拷贝一份\@Prop value: ParentCounter,同时必须避免再多拷贝一份SubCounter。 + +- 在CounterComp组件中只使用一个\@Prop counter:Counter。 + +- 添加另一个子组件SubCounterComp,其中包含\@ObjectLink subCounter: SubCounter。此\@ObjectLink可确保观察到SubCounter对象属性更改,并且UI更新正常。 + +- \@ObjectLink subCounter: SubCounter与CounterComp中的\@Prop counter:Counter的this.counter.subCounter共享相同的SubCounter对象。 + + +```ts +@Component +struct SubCounterComp { + @ObjectLink subValue: SubCounter; + build() { + Text(`SubCounterComp: this.subValue.counter: ${this.subValue.counter}`) + .onClick(() => { + // 2nd click handler + this.subValue.incrSubCounter(7); + }) + } +} +@Component +struct CounterComp { + @Prop value: ParentCounter; + build() { + Column({ space: 10 }) { + Text(`this.value.incrCounter(): this.value.counter: ${this.value.counter}`) + .fontSize(20) + .onClick(() => { + // 1st click handler + this.value.incrCounter(); + }) + SubCounterComp({ subValue: this.value.subCounter }) + Text(`this.value.incrSubCounter()`) + .onClick(() => { + // 3rd click handler + this.value.incrSubCounter(77); + }) + Divider().height(2) + } + } +} +@Entry +@Component +struct ParentComp { + @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; + build() { + Row() { + Column() { + CounterComp({ value: this.counter[0] }) + CounterComp({ value: this.counter[1] }) + CounterComp({ value: this.counter[2] }) + Divider().height(5) + ForEach(this.counter, + item => { + CounterComp({ value: item }) + }, + item => item.id.toString() + ) + Divider().height(5) + Text('Parent: reset entire counter') + .fontSize(20).height(50) + .onClick(() => { + this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; + }) + Text('Parent: incr counter[0].counter') + .fontSize(20).height(50) + .onClick(() => { + this.counter[0].incrCounter(); + this.counter[0].incrSubCounter(10); + }) + Text('Parent: set.counter to 10') + .fontSize(20).height(50) + .onClick(() => { + this.counter[0].setSubCounter(10); + }) + } + } + } +} +``` + + +拷贝关系图示如下: + + +![zh-cn_image_0000001653949465](figures/zh-cn_image_0000001653949465.png) + + +## 应用在渲染期间禁止改变状态变量 + +在学习本示例之前,我们要先明确一个概念,在ArkUI状态管理中,状态驱动UI更新。 + +![zh-cn_image_0000001651365257](figures/zh-cn_image_0000001651365257.png) + +所以,不能在自定义组件的build()或\@Builder方法里直接改变状态变量,这可能会造成循环渲染的风险,下面以build()方法举例示意。 + + +### 不推荐用法 + +在下面的示例中,Text('${this.count++}')在build渲染方法里直接改变了状态变量。 + + +```ts +@Entry +@Component +struct CompA { + @State col1: Color = Color.Yellow; + @State col2: Color = Color.Green; + @State count: number = 1; + build() { + Column() { + // 应避免直接在Text组件内改变count的值 + Text(`${this.count++}`) + .width(50) + .height(50) + .fontColor(this.col1) + .onClick(() => { + this.col2 = Color.Red; + }) + Button("change col1").onClick(() =>{ + this.col1 = Color.Pink; + }) + } + .backgroundColor(this.col2) + } +} +``` + +在ArkUI中,Text('${this.count++}')在全量更新或最小化更新会产生不同的影响: + +- 全量更新: ArkUI可能会陷入一个无限的重渲染的循环里,因为Text组件的每一次渲染都会改变应用的状态,就会再引起下一轮渲染的开启。 当 this.col2 更改时,都会执行整个build构建函数,因此,Text(`${this.count++}`)绑定的文本也会更改,每次重新渲染Text(`${this.count++}`),又会使this.count状态变量更新,导致新一轮的build执行,从而陷入无限循环。 + +- 最小化更新: 当 this.col2 更改时,只有Column组件会更新,Text组件不会更改。 只当 this.col1 更改时,会去更新整个Text组件,其所有属性函数都会执行,所以会看到Text(`${this.count++}`)自增。因为目前UI以组件为单位进行更新,如果组件上某一个属性发生改变,会更新整体的组件。所以整体的更新链路是:this.col2 = Color.Red -> Text组件整体更新->this.count++->Text组件整体更新。 + + +### 推荐用法 + +建议应用的开发方法在事件处理程序中执行count++操作。 + + +```ts +@Entry +@Component +struct CompA { + @State col1: Color = Color.Yellow; + @State col2: Color = Color.Green; + @State count: number = 1; + build() { + Column() { + Text(`${this.count}`) + .width(50) + .height(50) + .backgroundColor(this.col1) + .onClick(() => { + this.count++; + }) + } + .backgroundColor(this.col2) + } +} +``` + +build函数中更改应用状态的行为可能会比上面的示例更加隐蔽,比如: + +- 在\@Builder,\@Extend或\@Styles方法内改变状态变量 。 + +- 在计算参数时调用函数中改变应用状态变量,例如 Text('${this.calcLabel()}')。 + +- 对当前数组做出修改,sort()改变了数组this.arr,随后的filter方法会返回一个新的数组。 + + +```ts +@State arr : Array<..> = [ ... ]; +ForEach(this.arr.sort().filter(....), + item => { + ... +}) +``` + +正确的执行方式为:filter返回一个新数组,后面的sort方法才不会改变原数组this.arr,示例: + + +```ts +ForEach(this.arr.filter(....).sort(), + item => { + ... +}) +``` + + +## 使用状态变量强行更新 + + +### 不推荐用法 + + +```ts +@Entry +@Component +struct CompA { + @State needsUpdate: boolean = true; + realState1: Array = [4, 1, 3, 2]; // 未使用状态变量装饰器 + realState2: Color = Color.Yellow; + + updateUI(param: any): any { + const triggerAGet = this.needsUpdate; + return param; + } + + build() { + Column({ space: 20 }) { + ForEach(this.updateUI(this.realState1), + item => { + Text(`${item}`) + }) + Text("add item") + .onClick(() => { + // 改变realState1不会触发UI视图更新 + this.realState1.push(this.realState1[this.realState1.length-1] + 1); + + // 触发UI视图更新 + this.needsUpdate = !this.needsUpdate; + }) + Text("chg color") + .onClick(() => { + // 改变realState2不会触发UI视图更新 + this.realState2 = this.realState2 == Color.Yellow ? Color.Red : Color.Yellow; + + // 触发UI视图更新 + this.needsUpdate = !this.needsUpdate; + }) + }.backgroundColor(this.updateUI(this.realState2)) + .width(200).height(500) + } +} +``` + +上述示例存在以下问题: + +- 应用程序希望控制UI更新逻辑,但在ArkUI中,UI更新的逻辑应该是由框架来检测应用程序状态变量的更改去实现。 + +- this.needsUpdate是一个自定义的UI状态变量,应该仅应用于其绑定的UI组件。变量this.realState1、this.realState2没有被装饰,他们的变化将不会触发UI刷新。 + +- 但是在该应用中,用户试图通过this.needsUpdate的更新来带动常规变量this.realState1、this.realState2的更新。此方法不合理且更新性能较差,如果只想更新背景颜色,且不需要更新ForEach,但this.needsUpdate值的变化也会带动ForEach更新。 + + +### 推荐用法 + +要解决此问题,应将realState1和realState2成员变量用\@State装饰。一旦完成此操作,就不再需要变量needsUpdate。 + + +```ts +@Entry +@Component +struct CompA { + @State realState1: Array = [4, 1, 3, 2]; + @State realState2: Color = Color.Yellow; + build() { + Column({ space: 20 }) { + ForEach(this.realState1, + item => { + Text(`${item}`) + }) + Text("add item") + .onClick(() => { + // 改变realState1触发UI视图更新 + this.realState1.push(this.realState1[this.realState1.length-1] + 1); + }) + Text("chg color") + .onClick(() => { + // 改变realState2触发UI视图更新 + this.realState2 = this.realState2 == Color.Yellow ? Color.Red : Color.Yellow; + }) + }.backgroundColor(this.realState2) + .width(200).height(500) + } +} +``` \ No newline at end of file diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588291546.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588291546.png new file mode 100644 index 0000000000000000000000000000000000000000..7161b0170e120ae2b39b39ae49b9f5e52b18f7c2 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588291546.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588450934.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588450934.png new file mode 100644 index 0000000000000000000000000000000000000000..5f503253c5cb1a5472a8b0c51a997bbef7298d50 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588450934.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588610894.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588610894.png new file mode 100644 index 0000000000000000000000000000000000000000..5b188d79e05df2b55a5b3bf08ed88d86ae65c292 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001588610894.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001602146116.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001602146116.png new file mode 100644 index 0000000000000000000000000000000000000000..fd38d85e80e7f51347208cbf8d632ac00a683684 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001602146116.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001604900446.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001604900446.png new file mode 100644 index 0000000000000000000000000000000000000000..cc042ab8cb3e282f2b1b4ed6c832dddbaf9ea6be Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001604900446.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001605293914.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001605293914.png new file mode 100644 index 0000000000000000000000000000000000000000..d100321ec2e728b831382a941aec7e529598ef25 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001605293914.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001638250945.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001638250945.png new file mode 100644 index 0000000000000000000000000000000000000000..632ca7aa248bdffec7c18ab10020c3fcf62fb72d Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001638250945.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001651365257.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001651365257.png new file mode 100644 index 0000000000000000000000000000000000000000..215b84922f433dadb80cdab93f4955b0699e8192 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001651365257.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001651665921.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001651665921.png new file mode 100644 index 0000000000000000000000000000000000000000..c766f359f0ed08e8670bb1caecc19699e1d36a3b Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001651665921.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001653949465.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001653949465.png new file mode 100644 index 0000000000000000000000000000000000000000..37c726a518b791de15f8bb2e87684c90cdac93d0 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001653949465.png differ diff --git a/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001653986573.png b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001653986573.png new file mode 100644 index 0000000000000000000000000000000000000000..575d5ea86ae4a3cc7bde1f290558223d23a8e3b7 Binary files /dev/null and b/zh-cn/application-dev/quick-start/figures/zh-cn_image_0000001653986573.png differ diff --git a/zh-cn/application-dev/reference/arkui-ts/ts-state-management.md b/zh-cn/application-dev/reference/arkui-ts/ts-state-management.md index f9c7b26b894cdde60667e49b4c61cc4c40247d31..6fe560c25eaca837892b1c8e88903b09151fc387 100644 --- a/zh-cn/application-dev/reference/arkui-ts/ts-state-management.md +++ b/zh-cn/application-dev/reference/arkui-ts/ts-state-management.md @@ -21,6 +21,9 @@ ## AppStorage +AppStorage具体UI使用说明,详见[AppStorage(应用全局的UI状态存储)](../../quick-start/arkts-appstorage.md) + + ### link10+ static link(propName: string): SubscribedAbstractProperty @@ -698,6 +701,9 @@ let res: number = AppStorage.Size(); // 1 ## LocalStorage9+ +LocalStorage具体UI使用说明,详见[LocalStorage(页面级UI状态存储)](../../quick-start/arkts-localstorage.md) + + ### constructor9+ constructor(initializingProperties?: Object) @@ -735,9 +741,7 @@ static getShared(): LocalStorage | [LocalStorage](#localstorage9) | 返回LocalStorage实例。 | -```ts -let storage: LocalStorage = LocalStorage.getShared(); -``` +getShared具体使用,见[在UI页面通过getShared接口获取在通过loadContent共享的LocalStorage实例](../../quick-start/arkts-localstorage.md#将localstorage实例从uiability共享到一个或多个视图) ### has9+ @@ -1161,6 +1165,10 @@ link.set(50); // PropB -> 49, link.get() --> undefined ## PersistentStorage + +PersistentStorage具体UI使用说明,详见[PersistentStorage(持久化存储UI状态)](../../quick-start/arkts-persiststorage.md) + + ### PersistPropsOptions | 参数名 | 类型 | 必填 | 参数描述 | @@ -1196,9 +1204,7 @@ static persistProp<T>(key: string, defaultValue: T): void **示例:** -```ts -PersistentStorage.persistProp('highScore', '0'); -``` +persistProp具体使用,见[从AppStorage中访问PersistentStorage初始化的属性](../../quick-start/arkts-persiststorage.md#从appstorage中访问persistentstorage初始化的属性) ### deleteProp10+ @@ -1352,6 +1358,9 @@ let keys: Array = PersistentStorage.Keys(); ## Environment +Environment具体使用说明,详见[Environment(设备环境查询)](../../quick-start/arkts-environment.md) + + ### EnvPropsOptions | 参数名 | 类型 | 必填 | 参数描述 | @@ -1386,21 +1395,7 @@ static envProp<S>(key: string, value: S): boolean **示例:** -```ts -Environment.envProp('accessibilityEnabled', 'default'); -``` - - -### 内置环境变量说明 - -| key | 类型 | 说明 | -| -------------------- | --------------- | ---------------------------------------- | -| accessibilityEnabled | string | 无障碍屏幕朗读是否启用。 | -| colorMode | ColorMode | 深浅色模式,可选值为:
- ColorMode.LIGHT:浅色模式;
- ColorMode.DARK:深色模式。 | -| fontScale | number | 字体大小比例。 | -| fontWeightScale | number | 字重比例。 | -| layoutDirection | LayoutDirection | 布局方向类型,可选值为:
- LayoutDirection.LTR:从左到右;
- LayoutDirection.RTL:从右到左。 | -| languageCode | string | 当前系统语言,小写字母,例如zh。 | +envProp具体使用,见[从UI中访问Environment参数](../../quick-start/arkts-environment.md#从ui中访问environment参数) ### envProps10+ @@ -1525,4 +1520,16 @@ Environment.EnvProps([{ key: 'accessibilityEnabled', defaultValue: 'default' }, }, { key: 'prop', defaultValue: 'hhhh' }]); let keys: Array = Environment.Keys(); // accessibilityEnabled, languageCode, prop -``` \ No newline at end of file +``` + + +## 内置环境变量说明 + +| key | 类型 | 说明 | +| -------------------- | --------------- | ---------------------------------------- | +| accessibilityEnabled | string | 无障碍屏幕朗读是否启用。 | +| colorMode | ColorMode | 深浅色模式,可选值为:
- ColorMode.LIGHT:浅色模式;
- ColorMode.DARK:深色模式。 | +| fontScale | number | 字体大小比例。 | +| fontWeightScale | number | 字重比例。 | +| layoutDirection | LayoutDirection | 布局方向类型,可选值为:
- LayoutDirection.LTR:从左到右;
- LayoutDirection.RTL:从右到左。 | +| languageCode | string | 当前系统语言,小写字母,例如zh。 | \ No newline at end of file