Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ArkUI - 状态管理 #36

Open
cnwutianhao opened this issue Mar 23, 2024 · 0 comments
Open

ArkUI - 状态管理 #36

cnwutianhao opened this issue Mar 23, 2024 · 0 comments

Comments

@cnwutianhao
Copy link
Owner

cnwutianhao commented Mar 23, 2024

在声明式 UI 中,是以状态驱动视图更新,如图1所示:

图1

其中核心的概念就是状态(State)和视图(View):

  • 状态(State):指驱动视图更新的数据(被装饰器标记的变量)

    @Entry
    @Component
    struct Index {
      @State message: string = 'Hello World'
    
      build() {
        Column() {
          Text(this.message)
            .fontSize(50)
            .onClick(() => {
              this.message = 'Hello ArkTS'
            })
        }
        .width('100%')
        .height('100%')
      }
    }
    

    Index 组件里定义了 message 变量,而 message 前面就加了 @State 装饰器,如果没有这个装饰器,message 就是一个普通的变量,但是呢,正是我们给它加上了 @State 装饰器,所以,它就变成了一个状态变量。

  • 视图(View):基于 UI 描述渲染得到的用户界面

    @Entry
    @Component
    struct Index {
      @State message: string = 'Hello World'
    
      build() {
        Column() {
          Text(this.message)
            .fontSize(50)
            .onClick(() => {
              this.message = 'Hello ArkTS'
            })
        }
        .width('100%')
        .height('100%')
      }
    }
    

    build 函数内部就是 UI 的描述,我们这里就描述了一个列式的容器,容器里有一个普通的文本,文本的内容就是 message 的值,所以最终渲染出来的视图就是在屏幕上显示一个 Hello World。

视图渲染好了以后,用户就可以对视图中的页面元素产生交互,比如去触摸、点击、拖拽等事件。这些互动事件就有可能改变状态变量的值,比如说我们这个示例里,给 Text 绑定了一个点击事件,一旦用户点击,就会修改 message 的值,而在 ArkUI 的内部,有一种机制去监控状态变量的值,一旦发现发生了变更,就会触发视图的重新渲染,所以,按照我们这个示例来看,如果现在去点击这个 Hello World 文字,就会触发点击事件,修改 message 的值,把它变成 Hello ArkTS,而一旦这个变量值发生变更,视图重新渲染,于是,屏幕上显示的文字从 Hello World 变成 Hello ArkTS。

所以像这种状态视图之间相互作用的机制,我们就称之为状态管理机制。有了这种机制以后,我们将来开发的时候,不需要自己操作页面,只需要描述页面的结构,然后定义好对应的事件,在事件里面去操作状态,就可以了,这样每当用户去产生互动时,自然就会引起页面的动态刷新。所以一个动态页面就很容易的实现了。这也就是状态管理的好处。

一、@State 装饰器

  1. @State 装饰器标记的变量必须初始化,不能为空值。

    比如上面的示例代码,message 一声明,就给它初始化了一个 Hello World。

  2. @State 装饰器支持的类型是有限制的。

    支持 Object、class、string、number、boolean、enum 类型以及这些类型的数组。

    注:虽然以上这些类型都是允许的,但是有两个特殊场景:

    1. 嵌套类型:@State 修饰的变量是 Object,如果 Object 里面的属性发生了变更其实是能触发视图的更新,但是如果 Object 里面的某个属性它又是一个 Object,也就是 Object 套 Object,那就是嵌套类型,那么内部嵌套的那个 Object 它里面的属性再发生变更,就无法触发视图更新。

    2. 数组:数组中的元素不是简单类型,而是一个对象,那么对象里面的属性发生变更,同样无法触发视图更新。

二、@Prop@Link 装饰器

  1. 首先看一段代码,这是实现任务统计的示例代码:

     // 任务类
     class Task {
       static id: number = 1
       // 任务名称
       name: string = `任务${Task.id++}`
       // 任务状态
       finished: boolean = false
     }
    
     // 统一的卡片样式
     @Styles function card() {
       .width('95%')
       .padding(20)
       .backgroundColor(Color.White)
       .borderRadius(15)
       .shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
     }
    
     // 任务完成样式
     @Extend(Text) function finishedTask() {
       .decoration({ type: TextDecorationType.LineThrough })
       .fontColor('#B2B2B1')
     }
    
     @Entry
     @Component
     struct PropPage {
       // 总任务数量
       @State totalTask: number = 0
       // 已完成任务数量
       @State finishTask: number = 0
       // 任务数组
       @State tasks: Task[] = []
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           Row() {
             Text('任务进度:')
               .fontSize(30)
               .fontWeight(FontWeight.Bold)
             Stack() {
               Progress({
                 value: this.finishTask,
                 total: this.totalTask,
                 type: ProgressType.Ring
               })
                 .width(100)
               Row() {
                 Text(this.finishTask.toString())
                   .fontSize(24)
                   .fontColor('#0000FF')
                 Text(' / ' + this.totalTask.toString())
                   .fontSize(24)
               }
             }
           }
           .card()
           .margin({ top: 20, bottom: 10 })
           .justifyContent(FlexAlign.SpaceEvenly)
    
           // 新增任务按钮
           Button('新增任务')
             .width(200)
             .margin({ top: 10 })
             .onClick(() => {
               // 新增任务数据
               this.tasks.push(new Task())
               // 更新任务总数量
               this.totalTask = this.tasks.length
             })
    
           // 任务列表
           List({ space: 10 }) {
             ForEach(
               this.tasks,
               (item: Task, index) => {
                 ListItem() {
                   Row() {
                     Text(item.name)
                       .fontSize(20)
                     Checkbox()
                       .select(item.finished)
                       .onChange(val => {
                         // 更新当前任务状态
                         item.finished = val
                         // 更新已完成任务数量
                         this.finishTask = this.tasks.filter(item => item.finished).length
                       })
                   }
                   .card()
                   .justifyContent(FlexAlign.SpaceBetween)
                 }
                 .swipeAction({ end: this.DeleteButton(index) })
               }
             )
           }
           .width('100%')
           .layoutWeight(1)
           .alignListItem(ListItemAlign.Center)
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
    
       @Builder DeleteButton(index: number) {
         Button() {
           Image($r('app.media.delete'))
             .fillColor(Color.White)
             .width(20)
         }
         .width(40)
         .height(40)
         .type(ButtonType.Circle)
         .backgroundColor(Color.Red)
         .margin(5)
         .onClick(() => {
           this.tasks.splice(index, 1)
           this.totalTask = this.tasks.length
           this.finishTask = this.tasks.filter(item => item.finished).length
         })
       }
     }
  2. 概念

    当父子组件之间需要数据同步时,可以使用 @Prop@Link 装饰器。

    Q:什么是父子组件?什么又是数据同步

    A:看上面这段示例代码,我们会发现代码是从上到下一股脑写的,写了上百行代码,整个代码的可读性是比较差的。要解决这个问题,可以把整个功能分成几个模块,然后按模块封装成一个一个的组件,这样在入口组件(@Entry)当中就不用写太多代码,而是去引用其他模块对应的组件。整个代码结构会更加清晰,复用性也会更好。所以,入口组件就是一个父组件,它引用了其他的组件,那么这些被引用的组件就是子组件。所以这时候组件之间就出现了这种引用关系,而组件之间引用的过程中可能就会有数据传递的需求。比如在父组件里定义了一些数据,然后在子组件里需要用,这时候就需要把父组件的数据传给子组件,单纯的传递还不够,每当数据发生变更,还要去通知子组件,这就叫数据同步。数据同步利用 @State 装饰器是实现不了的,那就需要用 @Prop@Link 装饰器来实现。

  3. @Prop@Link 装饰器对比

    @prop @link
    同步类型 单向同步 双向同步
    允许装饰的变量类型 · 父子类型一致:string、number、boolean、enum
    · 父组件是对象类型,子组件是对象属性
    · 不可以是数组、any
    · 父子类型一致:string、number、boolean、enum、object、class,以及它们的数组
    · 数组中元素增、删、替换会引起刷新
    · 嵌套类型以及数组中的对象属性无法触发视图更新
    初始化方式 允许子组件初始化 父组件传递,禁止子组件初始化
  4. 使用 @Prop 对示例代码进行封装和改造

    假设父组件中的变量采用 @State 装饰器,与之对应的子组件采用 @Prop 装饰器,那这时候就可以实现单项同步,当父组件对 @State 装饰的变量进行任意的修改时,就会立刻把这个数据传递给子组件,但反过来,子组件如果对这个数据进行了修改,是不会反向传递到父组件那里。所以,这种同步被称之为单向同步。实现原理就是拷贝

     ...
    
     @Entry
     @Component
     struct PropPage {
       // 总任务数量
       @State totalTask: number = 0
       // 已完成任务数量
       @State finishTask: number = 0
       // 任务数组
       @State tasks: Task[] = []
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           TaskStatistics({ finishTask: this.finishTask, totalTask: this.totalTask })
    
           ...
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
    
       ...
     }
    
     @Component
     struct TaskStatistics {
       @Prop finishTask: number
       @Prop totalTask: number
    
       ...
     }
  5. 使用 @Link 对示例代码进行封装和改造

    假设父组件中的变量采用 @State 装饰器,与之对应的子组件采用 @Link 装饰器,此时就是双向同步,当父组件对 @State 装饰的变量进行任意的修改时,就会立刻把这个数据传递给子组件,反过来,子组件如果对这个数据进行了修改,也会把这个数据传递给父组件。所以,这种同步被称之为双向同步。实现原理就是引用

     ...
    
     @Entry
     @Component
     struct PropPage {
       // 总任务数量
       @State totalTask: number = 0
       // 已完成任务数量
       @State finishTask: number = 0
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           ...
    
           // 任务列表
           TaskList({ finishTask: $finishTask, totalTask: $totalTask })
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
     }
    
     ...
    
     @Component
     struct TaskList {
       // 总任务数量
       @Link totalTask: number
       // 已完成任务数量
       @Link finishTask: number
       // 任务数组
       @State tasks: Task[] = []
    
       ...
     }
  6. 使用数组对示例代码进行封装和改造

     ...
    
     // 任务统计信息
     class StatisticsInfo {
       totalTask: number = 0
       finishTask: number = 0
     }
    
     @Entry
     @Component
     struct PropPage {
       // 任务统计信息
       @State info: StatisticsInfo = new StatisticsInfo()
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           TaskStatistics({ finishTask: this.info.finishTask, totalTask: this.info.totalTask })
    
           // 任务列表
           TaskList({ info: $info })
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
     }
    
     @Component
     struct TaskStatistics {
       @Prop finishTask: number
       @Prop totalTask: number
    
       ...
     }
    
     @Component
     struct TaskList {
       @Link info: StatisticsInfo
    
       ...
     }

    结论:@Prop 不支持对象类型,@Link 支持对象类型。@Prop@Link 该怎么选?如果子组件拿到父组件的值以后,只是用来展示,不做修改,用 @Prop,如果子组件需要修改父组件的值,用 @Link

四、@Provide@Consume

@Provide@Consume 可以跨组件提供类似于 @State@Link 的双向同步。

使用 @Provide@Consume 对示例代码进行封装和改造:

...

// 任务统计信息
class StatisticsInfo {
  totalTask: number = 0
  finishTask: number = 0
}

@Entry
@Component
struct PropPage {
  // 任务统计信息
  @Provide info: StatisticsInfo = new StatisticsInfo()

  build() {
    Column({ space: 10 }) {
      // 任务进度卡片
      TaskStatistics()

      // 任务列表
      TaskList()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F2F3')
  }
}

@Component
struct TaskStatistics {
  @Consume info: StatisticsInfo

  ...
}

@Component
struct TaskList {
  @Consume info: StatisticsInfo

  ...
}

结论:@Provide@Consume 不需要显示的传参,内部会帮你去实现,但是代价是资源上面的损耗,所以,多数情况下,能用 @State@Prop@Link 就不要用 @Provide@Consume 了,除非是跨组件那种的场景。

五、@Observed@ObjectLink

作用@Observed@ObjectLink 装饰器用于在涉及嵌套对象数组元素为对象的场景中进行双向数据同步。

  1. 嵌套对象

     class Person {
       name: string
       age: number
       friend: Person
    
       constructor(name: string, age: number, friend?: Person) {
         this.name = name
         this.age = age
         this.friend = friend
       }
     }
     @Entry
     @Component
     struct Parent {
       @State p: Person = new Person('Xxx', 20, new Person('Yyy', 20))
    
       build() {
         Column() {
           Text(`${this.p.friend.name} : ${this.p.friend.age},`)
             .onClick(() => this.p.friend.age++)
         }
       }
     }

    通过上面这两段代码可以发现 Xxx 这个对象持有了 Yyy 对象,这就是嵌套对象。利用 Text 去渲染 Xxx 的 Friend 的 name 和 age,当发生点击事件时,去修改 Yyy 的 age,但是我们知道嵌套对象它的属性变更是无法被感知到,因此就无法触发视图的更新。要解决这个问题,需要做两件事:

    (1)需要给嵌套对象它所对应的类型上面加上 @Observed 装饰器

     @Observed
     class Person {
       ...
     }

    (2)需要给嵌套对象内部的对象加上 @ObjectLink 装饰器

     @Component
     struct Child {
       @ObjectLink p: Person
    
       build() {
         Column() {
           Text(`${this.p.name} : ${this.p.age}`)
         }
       }
     }
    
     @Entry
     @Component
     struct Parent {
       @State p: Person = new Person('Xxx', 20, new Person('Yyy', 20))
    
       build() {
         Column() {
           Child({ p: this.p.friend })
             .onClick(() => this.p.friend.age++)
         }
       }
     }
  2. 数组元素为对象

     @Observed
     class Person {
       name: string
       age: number
       friend: Person
    
       constructor(name: string, age: number, friend?: Person) {
         this.name = name
         this.age = age
         this.friend = friend
       }
     }
    
     @Component
     struct Child {
       @ObjectLink p: Person
    
       build() {
         Column() {
           Text(`${this.p.name} : ${this.p.age}`)
         }
       }
     }
    
     @Entry
     @Component
     struct Parent {
       @State p: Person = new Person('Xxx', 20, new Person('Yyy', 20))
       @State ps: Person[] = [new Person('Aaa', 20), new Person('Bbb', 20)]
    
       build() {
         Column() {
           Child({ p: this.p.friend })
             .onClick(() => this.p.friend.age++)
           Text('==== 朋友列表 ====')
           ForEach(
             this.ps,
             p => {
               Child({ p: p }).onClick(() => p.age++)
             }
           )
         }
       }
     }

    只要有了 @Observed,然后传递子组件的属性时,加上 @ObjectLink,那么,也能够触发视图的更新了。

  3. 示例代码

     // 任务类
     @Observed
     class Task {
       static id: number = 1
       // 任务名称
       name: string = `任务${Task.id++}`
       // 任务状态
       finished: boolean = false
     }
    
     // 统一的卡片样式
     @Styles function card() {
       .width('95%')
       .padding(20)
       .backgroundColor(Color.White)
       .borderRadius(15)
       .shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
     }
    
     // 任务完成样式
     @Extend(Text) function finishedTask() {
       .decoration({ type: TextDecorationType.LineThrough })
       .fontColor('#B2B2B1')
     }
    
     // 任务统计信息
     class StatisticsInfo {
       totalTask: number = 0
       finishTask: number = 0
     }
    
     @Entry
     @Component
     struct PropPage {
       // 任务统计信息
       @Provide info: StatisticsInfo = new StatisticsInfo()
    
       build() {
         Column({ space: 10 }) {
           // 任务进度卡片
           TaskStatistics()
    
           // 任务列表
           TaskList()
         }
         .width('100%')
         .height('100%')
         .backgroundColor('#F1F2F3')
       }
     }
    
     @Component
     struct TaskStatistics {
       @Consume info: StatisticsInfo
    
       build() {
         Row() {
           Text('任务进度:')
             .fontSize(30)
             .fontWeight(FontWeight.Bold)
           Stack() {
             Progress({
               value: this.info.finishTask,
               total: this.info.totalTask,
               type: ProgressType.Ring
             })
               .width(100)
             Row() {
               Text(this.info.finishTask.toString())
                 .fontSize(24)
                 .fontColor('#0000FF')
               Text(' / ' + this.info.totalTask.toString())
                 .fontSize(24)
             }
           }
         }
         .card()
         .margin({ top: 20, bottom: 10 })
         .justifyContent(FlexAlign.SpaceEvenly)
       }
     }
    
     @Component
     struct TaskList {
       @Consume info: StatisticsInfo
       // 任务数组
       @State tasks: Task[] = []
    
       handleTaskChange() {
         // 更新任务总数量
         this.info.totalTask = this.tasks.length
         // 更新已完成任务数量
         this.info.finishTask = this.tasks.filter(item => item.finished).length
       }
    
       build() {
         Column() {
           // 新增任务按钮
           Button('新增任务')
             .width(200)
             .margin({ top: 10, bottom: 10 })
             .onClick(() => {
               // 新增任务数据
               this.tasks.push(new Task())
               // 更新任务总数量
               this.handleTaskChange()
             })
    
           // 任务列表
           List({ space: 10 }) {
             ForEach(
               this.tasks,
               (item: Task, index) => {
                 ListItem() {
                   TaskItem({ item: item, onTaskChange: this.handleTaskChange.bind(this) })
                 }
                 .swipeAction({ end: this.DeleteButton(index) })
               }
             )
           }
           .width('100%')
           .layoutWeight(1)
           .alignListItem(ListItemAlign.Center)
         }
       }
    
       @Builder DeleteButton(index: number) {
         Button() {
           Image($r('app.media.delete'))
             .fillColor(Color.White)
             .width(20)
         }
         .width(40)
         .height(40)
         .type(ButtonType.Circle)
         .backgroundColor(Color.Red)
         .margin(5)
         .onClick(() => {
           this.tasks.splice(index, 1)
           this.handleTaskChange()
         })
       }
     }
    
     @Component
     struct TaskItem {
       @ObjectLink item: Task
       onTaskChange: () => void
    
       build() {
         Row() {
           if (this.item.finished) {
             Text(this.item.name)
               .finishedTask()
           } else {
             Text(this.item.name)
           }
           Checkbox()
             .select(this.item.finished)
             .onChange(val => {
               // 更新当前任务状态
               this.item.finished = val
               // 更新已完成任务数量
               this.onTaskChange()
             })
         }
         .card()
         .justifyContent(FlexAlign.SpaceBetween)
       }
     }
  4. 运行效果,如图2所示:

    图2

  5. 总结

    @Observed@ObjectLink 主要用来解决嵌套对象里面,对象属性变更无法触发数组刷新和数组里的元素式对象属性变更无法触发视图更新的问题。解决方案是给对象上面添加 @Observed 装饰器,同时给嵌套的对象或数组元素对象的变量上加 @ObjectLink 装饰器;当子组件调用父组件方法,我们的办法是把父组件的方法作为参数传递进来,但是传递过程中会有 this 的丢失,解决办法是传递这个函数过程当中,用 bind 把这个 this 绑定进去。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant