HarmonyOS NEXT应用开发之@State装饰器:组件内状态

@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。

在状态变量相关装饰器中,@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。

说明:

从API version 9开始,该装饰器支持在ArkTS卡片中使用。

概述

@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。

@State装饰的变量拥有以下特点:

  • @State装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link、@ObjectLink装饰变量之间建立双向数据同步。

  • @State装饰的变量生命周期与其所属自定义组件的生命周期相同。

装饰器使用规则说明

@State变量装饰器 说明
装饰器参数
同步类型 不与父组件中任何类型的变量同步。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 API11及以上支持Map、Set类型。 支持undefined和null类型。 支持类型的场景请参考[观察变化](#@State变量装饰器 说明 装饰器参数 无 同步类型 不与父组件中任何类型的变量同步。 允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。 支持Date类型。 API11及以上支持Map、Set类型。 支持undefined和null类型。 支持类型的场景请参考观察变化。 API11及以上支持上述支持类型的联合类型,比如string
支持AkrUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。 类型必须被指定。 不支持any。
被装饰变量的初始值 必须本地初始化。

变量的传递/访问规则说明

传递/访问 说明
从父组件初始化 可选,从父组件初始化或者本地初始化。如果从父组件初始化将会覆盖本地初始化。 支持父组件中常规变量(常规变量对@State赋值,只是数值的初始化,常规变量的变化不会触发UI刷新,只有状态变量才能触发UI刷新)、@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp装饰的变量,初始化子组件的@State。
用于初始化子组件 @State装饰的变量支持初始化子组件的常规变量、@State、@Link、@Prop、@Provide。
是否支持组件外访问 不支持,只能在组件内访问。

图1 初始化规则图示

观察变化和行为表现

并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。本小节将介绍什么样的修改才能被观察到,以及观察到变化后,框架的是怎么引起UI刷新的,即框架的行为表现是什么。

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。

    ts 复制代码
    // for simple type
    @State count: number = 0;
    // value changing can be observed
    this.count = 1;
  • 当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。例子如下。

    声明ClassA和Model类。

    ts 复制代码
      class ClassA {
        public value: string;
      
        constructor(value: string) {
          this.value = value;
        }
      }
      
      class Model {
        public value: string;
        public name: ClassA;
        constructor(value: string, a: ClassA) {
          this.value = value;
          this.name = a;
        }
      }

    @State装饰的类型是Model

    ts 复制代码
    // class类型
    @State title: Model = new Model('Hello', new ClassA('World'));

    对@State装饰变量的赋值。

    ts 复制代码
    // class类型赋值
    this.title = new Model('Hi', new ClassA('ArkUI'));

    对@State装饰变量的属性赋值。

    ts 复制代码
    // class属性的赋值
    this.title.value = 'Hi';

    嵌套属性的赋值观察不到。

    ts 复制代码
    // 嵌套的属性赋值观察不到
    this.title.name.value = 'ArkUI';
  • 当装饰的对象是array时,可以观察到数组本身的赋值和添加、删除、更新数组的变化。例子如下。

    声明Model类。

    ts 复制代码
    class Model {
      public value: number;
      constructor(value: number) {
        this.value = value;
      }
    }

    @State装饰的对象为Model类型数组时。

    ts 复制代码
    // 数组类型
    @State title: Model[] = [new Model(11), new Model(1)];

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

    ts 复制代码
    // 数组赋值
    this.title = [new Model(2)];

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

    ts 复制代码
    // 数组项赋值
    this.title[0] = new Model(2);

    删除数组项可以观察到。

    ts 复制代码
    // 数组项更改
    this.title.pop();

    新增数组项可以观察到。

    ts 复制代码
    // 数组项更改
    this.title.push(new Model(12));

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

    ts 复制代码
    // 嵌套的属性赋值观察不到
    this.title[0].value = 6;
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。

    ts 复制代码
    @Entry
    @Component
    struct DatePickerExample {
      @State selectedDate: Date = new Date('2021-08-08')
    
      build() {
        Column() {
          Button('set selectedDate to 2023-07-08')
            .margin(10)
            .onClick(() => {
              this.selectedDate = new Date('2023-07-08')
            })
          Button('increase the year by 1')
            .margin(10)
            .onClick(() => {
              this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1)
            })
          Button('increase the month by 1')
            .margin(10)
            .onClick(() => {
              this.selectedDate.setMonth(this.selectedDate.getMonth() + 1)
            })
          Button('increase the day by 1')
            .margin(10)
            .onClick(() => {
              this.selectedDate.setDate(this.selectedDate.getDate() + 1)
            })
          DatePicker({
            start: new Date('1970-1-1'),
            end: new Date('2100-1-1'),
            selected: this.selectedDate
          })
        }.width('100%')
      }
    }
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。

  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。

框架行为

  • 当状态变量被改变时,查询依赖该状态变量的组件;

  • 执行依赖该状态变量的组件的更新方法,组件更新渲染;

  • 和该状态变量不相关的组件或者UI描述不会发生重新渲染,从而实现页面渲染的按需更新。

使用场景

装饰简单类型的变量

以下示例为@State装饰的简单类型,count被@State装饰成为状态变量,count的改变引起Button组件的刷新:

  • 当状态变量count改变时,查询到只有Button组件关联了它;

  • 执行Button组件的更新方法,实现按需刷新。

ts 复制代码
@Entry
@Component
struct MyComponent {
  @State count: number = 0;

  build() {
    Button(`click times: ${this.count}`)
      .onClick(() => {
        this.count += 1;
      })
  }
}

装饰class对象类型的变量

  • 自定义组件MyComponent定义了被@State装饰的状态变量count和title,其中title的类型为自定义类Model。如果count或title的值发生变化,则查询MyComponent中使用该状态变量的UI组件,并进行重新渲染。

  • EntryComponent中有多个MyComponent组件实例,第一个MyComponent内部状态的更改不会影响第二个MyComponent。

ts 复制代码
class Model {
  public value: string;

  constructor(value: string) {
    this.value = value;
  }
}

@Entry
@Component
struct EntryComponent {
  build() {
    Column() {
      // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化
      MyComponent({ count: 1, increaseBy: 2 })
        .width(300)
      MyComponent({ title: new Model('Hello World 2'), count: 7 })
    }
  }
}

@Component
struct MyComponent {
  @State title: Model = new Model('Hello World');
  @State count: number = 0;
  private increaseBy: number = 1;

  build() {
    Column() {
      Text(`${this.title.value}`)
        .margin(10)
      Button(`Click to change title`)
        .onClick(() => {
          // @State变量的更新将触发上面的Text组件内容更新
          this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'Hello ArkUI';
        })
        .width(300)
        .margin(10)

      Button(`Click to increase count = ${this.count}`)
        .onClick(() => {
          // @State变量的更新将触发该Button组件的内容更新
          this.count += this.increaseBy;
        })
        .width(300)
        .margin(10)
    }
  }
}

从该示例中,我们可以了解到@State变量首次渲染的初始化流程:

  1. 使用默认的本地初始化:

    ts 复制代码
    @State title: Model = new Model('Hello World');
    @State count: number = 0;
  2. 对于@State来说,命名参数机制传递的值并不是必选的,如果没有命名参数传值,则使用本地初始化的默认值:

ts 复制代码
   class C1 {
      public count:number;
      public increaseBy:number;
      constructor(count: number, increaseBy:number) {
        this.count = count;
        this.increaseBy = increaseBy;
     }
   }
   let obj = new C1(1, 2)
   MyComponent(obj)

装饰Map类型变量

说明:

从API version 11开始,@State支持Map类型。

在下面的示例中,message类型为Map<number, string>,点击Button改变message的值,视图会随之刷新。

ts 复制代码
@Entry
@Component
struct MapSample {
  @State message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]])

  build() {
    Row() {
      Column() {
        ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
          Text(`${item[0]}`).fontSize(30)
          Text(`${item[1]}`).fontSize(30)
          Divider()
        })
        Button('init map').onClick(() => {
          this.message = new Map([[0, "a"], [1, "b"], [3, "c"]])
        })
        Button('set new one').onClick(() => {
          this.message.set(4, "d")
        })
        Button('clear').onClick(() => {
          this.message.clear()
        })
        Button('replace the first one').onClick(() => {
          this.message.set(0, "aa")
        })
        Button('delete the first one').onClick(() => {
          this.message.delete(0)
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}

装饰Set类型变量

说明:

从API version 11开始,@State支持Set类型。

在下面的示例中,message类型为Set<number>,点击Button改变message的值,视图会随之刷新。

ts 复制代码
@Entry
@Component
struct SetSample {
  @State message: Set<number> = new Set([0, 1, 2, 3, 4])

  build() {
    Row() {
      Column() {
        ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
          Text(`${item[0]}`).fontSize(30)
          Divider()
        })
        Button('init set').onClick(() => {
          this.message = new Set([0, 1, 2, 3, 4])
        })
        Button('set new one').onClick(() => {
          this.message.add(5)
        })
        Button('clear').onClick(() => {
          this.message.clear()
        })
        Button('delete the first one').onClick(() => {
          this.message.delete(0)
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}

State支持联合类型实例

@State支持联合类型和undefined和null,在下面的示例中,count类型为number | undefined,点击Button改变count的属性或者类型,视图会随之刷新。

ts 复制代码
@Entry
@Component
struct EntryComponent {
  build() {
    Column() {
      MyComponent()
    }
  }
}

@Component
struct MyComponent {
  @State count: number | undefined = 0;

  build() {
    Column() {
      Text(`count(${this.count})`)
      Button('change')
        .onClick(() => {
          this.count = undefined;
        })
    }
  }
}

常见问题

使用箭头函数改变状态变量未生效

箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。所以在该场景下, changeCoverUrl的this指向PlayDetailViewModel,而不是被装饰器@State代理的状态变量。

反例:

ts 复制代码
export default class PlayDetailViewModel {
  coverUrl: string = '#00ff00'

  changeCoverUrl= ()=> {
    this.coverUrl = '#00F5FF'
  }

}

ts 复制代码
import PlayDetailViewModel from './PlayDetailViewModel'

@Entry
@Component
struct PlayDetailPage {
  @State vm: PlayDetailViewModel = new PlayDetailViewModel()

  build() {
    Stack() {
      Text(this.vm.coverUrl).width(100).height(100).backgroundColor(this.vm.coverUrl)
      Row() {
        Button('点击改变颜色')
          .onClick(() => {
            this.vm.changeCoverUrl()
          })
      }
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Top)
  }
}

所以要将当前this.vm传入,调用代理状态变量的属性赋值。

正例:

ts 复制代码
export default class PlayDetailViewModel {
  coverUrl: string = '#00ff00'

  changeCoverUrl= (model:PlayDetailViewModel)=> {
    model.coverUrl = '#00F5FF'
  }

}

ts 复制代码
import PlayDetailViewModel from './PlayDetailViewModel'

@Entry
@Component
struct PlayDetailPage {
  @State vm: PlayDetailViewModel = new PlayDetailViewModel()

  build() {
    Stack() {
      Text(this.vm.coverUrl).width(100).height(100).backgroundColor(this.vm.coverUrl)
      Row() {
        Button('点击改变颜色')
          .onClick(() => {
            let self = this.vm
            this.vm.changeCoverUrl(self)
          })
      }
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Top)
  }
}

状态变量的修改放在构造函数内未生效

在状态管理中,类会被一层"代理"进行包装。当在组件中改变该类的成员变量时,会被该代理进行拦截,在更改数据源中值的同时,也会将变化通知给绑定的组件,从而实现观测变化与触发刷新。当开发者把状态变量的修改放在构造函数里时,此修改不会经过代理(因为是直接对数据源中的值进行修改),即使修改成功执行,也无法观测UI的刷新。

【反例】

ts 复制代码
@Entry
@Component
struct Index {
  @State viewModel: TestModel = new TestModel();

  build() {
    Row() {
      Column() {
        Text(this.viewModel.isSuccess ? 'success' : 'failed')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.viewModel.query()
          })
      }.width('100%')
    }.height('100%')
  }
}

export class TestModel {
  isSuccess: boolean = false
  model: Model

  constructor() {
    this.model = new Model(() => {
      this.isSuccess = true
      console.log(`this.isSuccess: ${this.isSuccess}`)
    })
  }

  query() {
    this.model.query()
  }
}

export class Model {
  callback: () => void

  constructor(cb: () => void) {
    this.callback = cb
  }

  query() {
    this.callback()
  }
}

上文示例代码将状态变量的修改放在构造函数内,界面开始时显示"failed",点击后日志打印"this.isSuccess: true"说明修改成功,但界面依旧显示"failed",未实现刷新。

【正例】

ts 复制代码
@Entry
@Component
struct Index {
  @State viewModel: TestModel = new TestModel();

  build() {
    Row() {
      Column() {
        Text(this.viewModel.isSuccess ? 'success' : 'failed')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.viewModel.query()
          })
      }.width('100%')
    }.height('100%')
  }
}

export class TestModel {
  isSuccess: boolean = false
  model: Model = new Model(() => {
  })

  query() {
    this.model = new Model(() => {
      this.isSuccess = true
    })
    this.model.query()
  }
}

export class Model {
  callback: () => void

  constructor(cb: () => void) {
    this.callback = cb
  }

  query() {
    this.callback()
  }
}

上文示例代码将状态变量的修改放在类的普通方法中,界面开始时显示"failed",点击后显示"success"。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ......

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ......

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ......

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题

2.性能优化方向

3.架构方向

4.鸿蒙开发系统底层方向

5.鸿蒙音视频开发方向

6.鸿蒙车载开发方向

7.鸿蒙南向开发方向

相关推荐
wywcool2 分钟前
JVM学习之路(5)垃圾回收
java·jvm·后端·学习
王佑辉4 分钟前
【jvm】双亲委派机制
jvm
杨荧38 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
锐策39 分钟前
〔 MySQL 〕数据类型
数据库·mysql
天天要nx1 小时前
D68【python 接口自动化学习】- python基础之数据库
数据库·python
Allen Bright1 小时前
Redis安装
数据库·redis·缓存
门牙咬脆骨1 小时前
【MYSQL】数据库日志 (了解即可)
数据库·mysql
ModelBulider1 小时前
十三、注解配置SpringMVC
java·开发语言·数据库·sql·mysql
豆 腐1 小时前
MySQL【四】
android·数据库·笔记·mysql
熊明才2 小时前
Ubuntu 22.04.4 LTS + certbot 做自动续签SSL证书(2024-11-14亲测)
数据库·网络协议·ssl