HarmonyOS 角落里的知识 —— 状态管理

944 阅读5分钟

一、前言

在探索 HarmonyOS 的过程中,我们发现了许多有趣且实用的功能和特性。有些总是在不经意间或者触类旁通的找到。或者是某些开发痛点。其中,状态管理是ArkUI开发非常核心的一个东西,我们进行了大量的使用和测试遇到了许多奇奇怪怪的问题。

该系列将着重分享、介绍HarmonyOS API11+的新版本特性或者奇奇怪怪的解决方案、BUG。(弃用API非必要不提及)

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

二、@State

这应该是用的最多的了,状态变量,将状态变量和UI的某个属性绑定,即可同步两者。

  1. 初始化行为

    组件初始化时,@State只能从外面传入一次,之后父组件值的修改并不会影响到组件内部的@State,所以它叫组件内状态~(当然你组件用if、else来更新当我没说)

     @Entry
     @Component
     export struct ExpComponent {
         @State title: string = "Title"
     ​
         build() {
             Column() {
                 ExpChildComponent({
                     childTitle: this.title
                 })
                 Button(this.title)
                     .onClick(()=>{
                         this.title = "Click"
                     })
             }
         }
     }
     @Component
     export struct ExpChildComponent {
         @State childTitle: string = "????"
     ​
         build() {
             Text(this.childTitle)
         }
     }
    

    就像这样,onClick触发后ExpChildComponent并不会发生改变了。

  2. 太多的@State

    1. 一开始我们会直接将状态变量直接声明在组件中,如:
     @Component
     export struct ExpComponent {
         @State title: string = ""
     ​
         build() {
             Text(this.title)
         }
     }
    

    但是随着,一个页面开始变得复杂的时候,我们会面临一堆的Controller,xxState等等,看起来就很头疼。这时候利用@State能观察到Object.keys(自身)返回的所有属性的特性,我们可以抽出一个ExpUIState

     @Component
     export struct ExpComponent {
         @State uiState: ExpUIState = new ExpUIState()
     ​
         build() {
             Text(this.uiState.title)
         }
     }
     ​
     class ExpUIState {
         title: string = ""
     }
    

    如此,还你一个优雅的组件。

  3. 数组

    @State能直接修饰数组:

     @State title: Model[] = [new Model(1), new Model(2)];
    

    数组自身的赋值可以观察到。

     this.title = [new Model(2)];
    

    数组项的赋值可以观察到。

     this.title[0] = new Model(2);
    

    删除数组项可以观察到。

     this.title.pop();
    

    新增数组项可以观察到。

     this.title.push(new Model(12));
    

    数组项中属性的赋值观察不到。

    this.title[0].value = 6;
    

    数组对象及其子项的赋值是可以被观察到的,所以“titles[0].value = 1 ”这种使用方法是没用滴。

  4. 作用域的影响

    得益于闭包的特性(对于Java、Kotlin开发来说不存在的东西)箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。

    @Component
    export struct ExpComponent {
        @State uiState: ExpUIState = new ExpUIState()
    
        build() {
            Column() {
                Text(this.uiState.title)
                Button("Click").onClick(()=>{
                    this.uiState.autoRefreshTitle()
                })
            }
        }
    }
    class ExpUIState {
        title: string = "??"
        autoRefreshTitle = () => {
            this.title = "AutoRefreshTitle"
        }
    }
    

    上述操作,触发onClick是不会更新UI的,如果想要触发更新需要用下面的例子:

    @Component
    export struct ExpComponent {
        @State uiState: ExpUIState = new ExpUIState()
    
        aboutToAppear(): void {
        }
    
        build() {
            Column() {
                Text(this.uiState.title)
                Button("Click").onClick(()=>{
                    let fk = this.uiState
                    this.uiState.autoRefreshTitle(fk)
                })
            }
        }
    }
    class ExpUIState {
        title: string = "??"
        autoRefreshTitle = (st:ExpUIState) => {
            st.title= "AutoRefreshTitle"
        }
    }
    

    反人类吧?阿弥陀佛。

  5. 嵌套对象

    @State修饰对象时,里面可能还有对象,或者数组.

    class ExpUIState {
        childs: ExpChild[] = []
        firstChild: ExpChild = new ExpChild()
    }
    

    很可惜的是,由于@State装饰的变量,只能监听到对象本身的地址以及第一层属性的地址变化,也就是firstChild或者childs地址的变化,例如如下操作:uiState.firstChild.subTtile="",uiState.childs.push(new ExpChild())等是没法触发刷新的。

    解决办法就是不用@State

三、@Prop

  1. 初始化行为

    几乎和@State一致,但是@Prop变量允许在本地修改,但修改后的变化不会同步回父组件。当数据源更改时,@Prop装饰的变量都会更新,并且会覆盖本地所有更改。因此,数值的同步是父组件到子组件(所属组件),子组件数值的变化不会同步到父组件。

    相比起@State重点就是父组件修改后子组件会同步。苦恼@State没法修改的人,有福了~

  2. 套一堆的@Prop

    不要这么做。如果你的子组件使用@Prop、孙子组件也用@Prop,我其实推荐你使用@Provide(当然你硬要用也行)

  3. @Require

    使用@Prop有一个好处就是,可以使用@Require。变成一个必传的参数(福音啊)。@State是可以不传的。

    @Require @Prop index: number 
    
  4. @Observed

    坏消息:@Prop也没法观察嵌套对象~,因为父组件传入时,用的@State传入的。好消息:加一个@Observed就好了~

    @Component
    export struct ExpComponent {
        @State uiState: ExpUIState = new ExpUIState()
    
        aboutToAppear(): void {
        }
    
        build() {
            Column() {
                ExpChildComponent({
                    child: this.uiState.firstChild
                })
                Button("Click").onClick(() => {
                    this.uiState.firstChild.subTitle = "????"
                })
            }
        }
    }
    
    @Component
    export struct ExpChildComponent {
        @Require @Prop child: ExpChild
    
        build() {
            Text(this.child.subTitle)
        }
    }
    
    
    @Observed
    class ExpUIState {
        childs: ExpChild[] = []
        firstChild: ExpChild = new ExpChild()
    }
    
    @Observed
    class ExpChild {
        subTitle: string = "啊"
    }
    

    在嵌套场景下,每一层都要用@Observed装饰,且每一层都要被@Prop接收。

    四、@Link

    1. 初始化行为

      @Link装饰的变量与其父组件中的数据源共享相同的值

      PS:从API version 9开始,@Link子组件从父组件初始化@State的语法为Comp({ aLink: this.aState })。同样Comp({aLink: $aState})也支持(早该如此了)

    2. 给Dialog用

      如果你在某个Dialog中,想同步数据到组件,@Link是一个很好的选择:

       @CustomDialog
       struct CustomDialog01 {
         @Link inputValue: string;
         controller: CustomDialogController;
       ​
         build() {
           Column() {
             Text('Change text')
               .fontSize(20)
               .margin({ top: 10, bottom: 10 })
             TextInput({ placeholder: '', text: this.inputValue })
               .height(60)
               .width('90%')
               .onChange((value: string) => {
                 this.inputValue = value;
               })
           }
         }
       }
       ​
       @Entry
       @Component
       struct DialogDemo01 {
         @State inputValue: string = 'click me';
         dialogController: CustomDialogController = new CustomDialogController({
           builder: CustomDialog01({
             inputValue: $inputValue
           })
         })
       ​
         build() {
           Column() {
             Button(this.inputValue)
               .onClick(() => {
                 this.dialogController.open();
               })
               .backgroundColor(0x317aff)
           }
           .width('100%')
           .margin({ top: 5 })
         }
       }
      

      当然,你也可以传个函数进去~

    3. @Link套娃

      就像@Prop一样,我很建议你使用@Provide@Consume

    五、@Watch

    @Watch应用于对状态变量的监听,这个装饰器可以说是非常直观的一个了,就是用来观察状态变量的。

    1. 不要在里面修改状态变量

       @State @Watch("onUiStateChange") uiState: ExpUIState = new ExpUIState()
       onUiStateChange(){
           this.uiState.firstChild = new ExpChild()
       }
      

      相信你看得出来,这是一个死循环吧?

    2. 子组件事件传递到父组件

      可通过状态同步@Link@Provide@Consume进行父子组件间的状态通知,结合@Watch可以将状态变量的修改在组件间传递,实现类似的功能。

    3. 粘性

      @Watch没有粘性,所以第一次初始化并不会触发@Watch!

    4. 日志

      @Watch可以观察状态变量变化的特性,很适合来做Log日志不是吗~

    六、预告

    下篇继续分享@State、@Prop、@Provide、@ObjectLink、@ObjectLinkV2、@Link、@Watch、@Track(可能)

    七、结尾

    没了。

    如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏