[HarmonyOS] 鸿蒙开发中的状态管理

1. 状态管理

1.1 基本概念

用户构建一个UI界面,会依赖数据进行渲染,所依赖的这些数据可以称为状态。状态的变化会让UI重新渲染,这在ArkUI中统称为状态管理机制。

在了解状态管理机制之前,首先需要先了解下一些基本的概念:

  • 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新;
  • 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新;
  • 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据;
  • 命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段;
  • 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖;
  • 初始化子组件:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量;
  • 本地初始化:在变量声明的时候赋值,作为变量的默认值;
@Component
struct MyComponent {
  // number用@State修饰,是状态变量;
  @State count: number = 0;
  // increaseBy是常规变量,并且进行了本地初始化;
  private increaseBy: number = 1;

  build() {
  }
}

@Component
struct Parent {
  build() {
    Column() {
      // 从父组件初始化,覆盖本地定义的默认值;
      // 初始化子组件,并且将父组件中的状态变量传递给了子组件;
      // count: 1,为数据源;
      MyComponent({ count: 1, increaseBy: 2 }) // 命名参数机制,父子组件传递同步参数;
    }
  }
}

在ArkUI中,自定义组件中可以拥有变量,这些变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。

  • View(UI):UI渲染,指将build()方法和@Builder装饰的方法内的UI描述映射到界面;
  • State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,会引起UI的重新渲染;

简单来说,这就是View和State的双向绑定,当State(可以理解为数据)变化的时候,View会自动的跟着变化,重新渲染,这就省去了我们拿到数据之后,再手动更新View展示的逻辑。对比于iOS原生框架,这真的是太方便了。

2. 场景概览

这里是关于状态管理的一个总结,会列出ArkTS支持的几种方式以及特点,先有个大概印象。

所有的装饰器可以大致分为:

  • 管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。
装饰器描述使用场景
@State装饰的变量是组件自己内部的状态数据,这些状态数据的改变会调用组件的build方法进行UI刷新组件内部自己使用,可以与其他装饰器配合使用
@State @Prop和@State类似,但@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,组件内部可以修改@Prop变量,但不会通知父组件,是单向绑定的父传子,单向传递
@Provide @Consume@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定父传子孙,跨层级双向传递
  • 管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。
装饰器描述使用场景
LocalStorage多个页面之间的UI状态存储,可以跨页面共享数据;想要在多个页面共享数据,或者在逻辑中操作数据触发UI更新
AppStorageApp全局共享的UI状态存储在App全局内跨模块使用
PersistentStorage可以将AppStorage数据持久化UI的状态如果想持久化选这个
Environment用于管理设备的环境变量多语言、多主题会使用

3. 管理组件状态的装饰器

3.1 @State 组件内状态

使用@State装饰的变量,称为状态变量,有以下特点:

  • 是私有的,只能从组件内部访问;
  • 声明时必须指定类型和在本地初始化;
  • 状态变量的改变会让依赖该状态变量的UI重新渲染;

举个例子:

@Entry
@Component
struct Index {
  // 声明一个状态变量
  @State name: string = "hello"

  build() {
    Column() {
      // 默认展示的是 hello
      Text(this.name)
      Button("Change")
        .onClick(()=>{
          // 点击按钮修改状态变量,Text展示的内容也会随之变化
          this.name = "hello world"
        })
    }
    .margin({top: 20})
  }
}

可以在DevEco上运行下上面的例子,这只是最基本的使用。另外,并不是状态变量的所有改变都会引起UI的刷新,这取决于装饰的数据类型:

  • 对于boolean string number类型,可以观察到数值的变化;
  • 对于class或者Object类型,只能观察到自身和自身属性的变化(如果b对象的是a对象的一个属性,a.b.xxx的改变是观察不到的);
  • 对于array对象,可以观察到数组本身的赋值和添加、删除、更新数组的变化(数组中item的属性更改是观察不到的);
  • 对于Date对戏那个,可以观察到Date整体的赋值;
  • 对于Map,可以观察到Map整体的赋值,可以通过set clear delete 更新Map的值;
  • 对于Set,可以观察到Set整体的赋值,可以通过add clear delete更新Set的值;

状态改变只会引起和该状态变量相关组件的重新渲染。当状态变量改变时,会查询依赖该状态变量的组件,并且执行该组件的更新方法,组件进行更新渲染。

3.2 @Prop 父子单向同步

我们会有这样的需求:父组件的数据会传递给子组件使用,当父组件的数据更新时,希望也会触发子组件的UI更新,此时就需要用到@Prop了。

简单来说就是:

  • 对于父组件,还是使用@State声明一个状态变量;
  • 对于子组件,使用@Prop声明一个变量,但是不能初始化;

这样@Prop装饰的变量就会和父组件建立单向的同步关系,@Prop变量可以在子组件内修改,但是修改之后不会同步给父组件。

来看个简单示例:

class TestModel {
  name: string = 'zhangsan'
}

@Entry
@Component
struct Parent {
  // 父组件的状态变量
  @State model: TestModel = new TestModel()

  build() {
    Column() {
      Text(this.model.name)
      Button('点击修改名字')
        .onClick(() => {
          // 父组件中修改状态变量,会影响到子组件
          this.model.name = "李四"
        })
      Child({name: this.model.name})
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct Child {

  // 子组件这里使用@Prop
  @Prop name: string

  build() {
    Column() {
      Text(this.name)
      Button('点击修改名字')
        .onClick(() => {
          // 子组件中修改状态变量,只会影响到当前组件
          this.name = "王五"
        })
    }
  }
}

使用@Prop修饰变量有以下限制:

  • @Prop装饰变量时会进行深拷贝,在拷贝过程中,除了基本类型、Map、Set、Date、Array外,都会丢失类型。
  • @Prop装饰器不能在@Entry装饰的自定义组件中使用;

@Prop装饰的数据可以观察到以下变化:

  • 装饰类型是 Object class string number boolean enum类型时,都可以观察到赋值的变化;
  • 装饰类型是Object或者class复杂类型时,只会观察第一层的属性的变化;
  • 对于嵌套场景,如果class是被@Observed装饰,可以观察到class属性的变化;
  • 装饰类型是Array,可以观察到数组本身的赋值、添加、删除和更新;
  • 使用父组件中的@State变量的值初始化子组件中的@Prop变量,@State变量变化时,该变量值也会同步给@Prop变量;
  • @Prop装饰的变量的修改不会影响其数据源@State装饰变量的值;
  • 除了@State,数据源也可以用@Link或@Prop装饰,对@Prop的同步机制是相同的;
  • 可以观察到Date、Map、Set类型的变量的整体赋值,通过其方法可以更新对象的值;

3.3 @Link 父子双向同步

子组件中被@Link装饰的变量和其父组件中对应的数据源建立双向数据绑定,@Link装饰的变量和其父组件中的数据源共享相同的值。也就是说,子组件中修改@Link装饰的变量,会引起父组件中状态变量的变化。

对于不同数据类型的观察,和上面@State @Props类似,这里就不一一列举了。

直接看个小例子吧:

class TestModel {
  name: string = '张三'
}

@Entry
@Component
struct Index {
  @State model: TestModel = new TestModel()

  build() {
    Column() {
      Text(this.model.name)
      Button('点击修改名字')
        .onClick(() => {
          this.model.name = "李四"
        })
      Child({model: this.model})
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct Child {
  // 使用@Link装饰的变量不能被本地初始化
  // @Link修饰的变量和父组件需要同类型
  @Link model: TestModel

  build() {
    Column(){
      Text(this.model.name)
      Button('点击修改名字')
        .onClick(() => {
          // 同时会修改父组件中的状态
          this.model.name = "王五"
        })
    }
  }
}

4 管理应用拥有的状态

如果要实现应用级的,或者想要在多个页面进行状态数据共享,就需要用到应用级别的状态管理。

4.1 LocalStorage 页面级UI状态存储

4.1.1 LocalStorage介绍

  • 组件树的根节点(被@Entry装饰的@Component),可以被分配一个LocalStorage实例,其所有的子组件将自动获得对该LocalStorage实例的访问权限;
  • LocalStorage支持UIAbility实例内多个页面间状态共享;
  • 应用程序可以创建多个LocalStorage实例,LocalStorage实例可以在页面内共享,也可以通过GetShared接口,实现跨页面、UIAbility实例内共享;
  • 被@Component装饰的组件最多可以访问一个LocalStorage实例和AppStorage,未被@Entry装饰的组件不可被独立分配LocalStorage实例,只能接受父组件通过@Entry传递来的LocalStorage实例;
  • LocalStorage中的所有属性都是可变的;

LocalStorage根据与@Component装饰的组件的同步类型不同,提供了两个装饰器:

  • @LocalStorageProp:@LocalStorageProp装饰的变量和与LocalStorage中给定属性建立单向同步关系。
  • @LocalStorageLink:@LocalStorageLink装饰的变量和在@Component中创建与LocalStorage中给定属性建立双向同步关系。

4.1.2 @LocalStorageProp和LocalStorage单向同步的简单场景

// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);

// 使LocalStorage可从@Component组件访问
@Entry(storage)
@Component
struct CompA {
  // @LocalStorageProp变量装饰器与LocalStorage中的'PropA'属性建立单向绑定
  @LocalStorageProp('PropA') storageProp1: number = 1;

  build() {
    Column({ space: 15 }) {
      // 点击后从47开始加1,只改变当前组件显示的storageProp1,不会同步到LocalStorage中
      Button(`Parent from LocalStorage ${this.storageProp1}`)
        .onClick(() => {
          this.storageProp1 += 1
        })
      Child()
    }
  }
}

@Component
struct Child {
  // @LocalStorageProp变量装饰器与LocalStorage中的'PropA'属性建立单向绑定
  @LocalStorageProp('PropA') storageProp2: number = 2;

  build() {
    Column({ space: 15 }) {
      // 当CompA改变时,当前storageProp2不会改变,显示47
      Text(`Parent from LocalStorage ${this.storageProp2}`)
    }
  }
}

在上面的示例中,CompA组件和Child组件分别在本地创建了storage的PropA对应属性的单向同步的数据,我们可以看到:

  • 在CompA中点击按钮对this.storProp1的修改,只会在CompA中生效,并不会同步回storage;
  • 在Child组件中,Text绑定的storProp2依旧显示47;

Record和Map类似,Map和Record的区别:

  • Map是JavaScript中的原生数据结构,而Record是TypeScript中的一个工具类型;
  • Map中的键可以是任意类型,而Record中的键是通过类型参数指定的;
  • Map中相同的键只能存在一个,后面的会覆盖前面的;而Record中指定的键是固定的,值的类型也是固定的;

4.1.3 @LocalStorageLink和LocalStorage双向同步的简单场景

// 构造LocalStorage实例
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
// 调用link(api9以上)接口构造'PropA'的双向同步数据,linkToPropA 是全局变量
let linkToPropA: SubscribedAbstractProperty<object> = storage.link('PropA');

@Entry(storage)
@Component
struct CompA {

  // @LocalStorageLink('PropA')在CompA自定义组件中创建'PropA'的双向同步数据,初始值为47,因为在构造LocalStorage已经给“PropA”设置47
  @LocalStorageLink('PropA') storageLink: number = 1;

  build() {
    Column() {
      Text(`incr @LocalStorageLink variable`)
        // 点击“incr @LocalStorageLink variable”,this.storageLink加1,改变同步回storage,全局变量linkToPropA也会同步改变

        .onClick(() => {
          this.storageLink += 1
        })

      // 并不建议在组件内使用全局变量linkToPropA.get(),因为可能会有生命周期不同引起的错误。
      Text(`@LocalStorageLink: ${this.storageLink} - linkToPropA: ${linkToPropA.get()}`)
    }
  }
}

上面构建了一个简单的双向同步的场景,当点击”incr @LocalStorageLink variable“修改this.storageLink状态变量的值时,会同步修改LocalStorage里面的数据。

4.1.4 应用逻辑中使用LocalStorage

上面双向同步的示例中可以看到点击事件也会修改linkToPropA的值:

let linkToPropA: SubscribedAbstractProperty<object> = storage.link('PropA');

其实我们在应用逻辑内使用LocalStorage也可以实现类似前面UI的单向、双向绑定:

let para: Record<string,number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para); // 创建新实例并使用给定对象初始化
let propA: number | undefined = storage.get('PropA') // propA == 47
let link1: SubscribedAbstractProperty<number> = storage.link('PropA'); // link1.get() == 47

let link2: SubscribedAbstractProperty<number> = storage.link('PropA'); // link2.get() == 47

let prop: SubscribedAbstractProperty<number> = storage.prop('PropA'); // prop.get() == 47

link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49

4.1.5 多个视图、页面中共享LocalStorage实例

如果想要在多个页面中共享Storage实例,可以在所属的UIAbility中创建LocalStorage实例,并调用windowStage.loadContent,如下:

// EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
    para:Record<string, number> = { 'PropA': 47 };
    storage: LocalStorage = new LocalStorage(this.para);

    onWindowStageCreate(windowStage: window.WindowStage) {
        windowStage.loadContent('pages/Index', this.storage);
    }
}

然后就可以在页面中通过getShared()方法获取到共享的LocalStorage实例,可以看下面这个例子,Index页面使用了propA的值,当跳转到Page页面,修改propA的值,返回Index页面之后,页面中的propA的值也会同步修改:

// index.ets
import router from '@ohos.router';

// 通过getShared接口获取stage共享的LocalStorage实例
let storage = LocalStorage.getShared()

@Entry(storage)
@Component
struct Index {
  // can access LocalStorage instance using 
  // @LocalStorageLink/Prop decorated variables
  @LocalStorageLink('PropA') propA: number = 1;

  build() {
    Row() {
      Column() {
        Text(`${this.propA}`)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button("To Page")
          .onClick(() => {
            router.pushUrl({
              url: 'pages/Page'
            })
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

// Page.ets
import router from '@ohos.router';

let storage = LocalStorage.getShared()

@Entry(storage)
@Component
struct Page {
  @LocalStorageLink('PropA') propA: number = 2;

  build() {
    Row() {
      Column() {
        Text(`${this.propA}`)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)

        Button("Change propA")
          .onClick(() => {
            this.propA = 100;
          })

        Button("Back Index")
          .onClick(() => {
            router.back()
          })
      }
      .width('100%')
    }
  }
}

目前遇到了一个问题,想要在单独的TS文件中实现某个方法,操作更新主页面的状态,前几天一直没有找到合适的解决方案,目前尝试使用Storage可以解决这个问题:

// 单独创建一个文件提供修改状态的方法
// util.ets
export function changeCount() {
  let storage = LocalStorage.getShared()
  storage.set("PropA", 100);
}

// 引入changeCount方法
import {changeCount} from '../common/utils/util';
// 在页面中使用Storage
let storage: LocalStorage = LocalStorage.getShared();

@Entry(storage)
@Component
struct Parent {
  @LocalStorageLink('PropA') count: number = 1;

  build() {
    Column() {
      Text(`count: ${this.count}`);
      Button("Click")
        .onClick(()=>{
          changeCount();
        })
    }
  }
}

4.2 AppStorage:应用全局的UI状态存储

AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建的单例,为应用程序UI状态属性提供中央存储。持久化数据PersistentStorage和环境变量Environment都是通过AppStorage中转,才可以和UI交互。

AppStorage支持应用的主线程内多个UIAbility实例间的状态共享。并且可以被双向同步,数据可以是存在本地或远程设备上,并具有不同的功能。如果希望这些数据在UI中使用,需要使用@StorageProp和@StorageLink。

从官方文档中来看AppStorage和LocalStorage的使用方法是很类似的。

4.2.1 在应用逻辑中使用AppStorage和LocalStorage

// AppStorage是单例,它的所有API都是静态的
AppStorage.setOrCreate('PropA', 47);

let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('PropA',17);

// propA in AppStorage == 47, propA in LocalStorage == 17
let propA: number | undefined = AppStorage.get('PropA') 

// link1.get() == 47
let link1: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); 

// link2.get() == 47
let link2: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); 

// prop.get() == 47
let prop: SubscribedAbstractProperty<number> = AppStorage.prop('PropA'); 

// two-way sync: link1.get() == link2.get() == prop.get() == 48
link1.set(48); 

// one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
prop.set(1); 

// two-way sync: link1.get() == link2.get() == prop.get() == 49
link1.set(49); 

storage.get<number>('PropA') // == 17
storage.set('PropA', 101);
storage.get<number>('PropA') // == 101

AppStorage.get<number>('PropA') // == 49
link1.get() // == 49
link2.get() // == 49
prop.get() // == 49

4.2.2 在UI内部使用AppStorage和LocalStorage

AppStorage.setOrCreate('PropA', 47);
let storage = new LocalStorage();
storage.setOrCreate('PropA', 48);

@Entry(storage)
@Component
struct CompA {
  // @StorageLink变量装饰器和AppStorage配合使用,可以建立和AppStorage中的属性的双向同步
  @StorageLink('PropA') storageLink: number = 1;
  @LocalStorageLink('PropA') localStorageLink: number = 1;

  build() {
    Column({ space: 20 }) {
      Text(`From AppStorage ${this.storageLink}`)
        .onClick(() => {
          this.storageLink += 1
        })

      Text(`From LocalStorage ${this.localStorageLink}`)
        .onClick(() => {
          this.localStorageLink += 1
        })
    }
  }
}

4.3 PersistentStorage 持久化存储UI状态

4.3.1 介绍

LocalStorage和AppStorage都是运行时的内存,如果想要在应用重新启动后数据依然保留,就需要用到PersistentStorage了。

PersistentStorage是应用程序中的可选单例对象,它的作用就是持久化存储选定的AppStorage属性,确保这些属性在应用程序重新启动时的值与应用程序关闭的值相同。

应用程序通过API,决定哪些AppStorage属性可以借助PersistentStorage持久化,UI和业务逻辑不会直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage中。

PersistentStorage和AppStorage中的属性建立双向同步,通常通过AppStorage访问PersistentStorage。

4.3.2 限制条件

PersistantStorage存储类型的限制:

  • 支持number string boolean enum等简单类型
  • 支持可以被JSON.stringfy()和JSON.parse()重构的对象
  • 不支持嵌套对象
  • 不支持undefined和null

PersistantStorage使用的限制:

  • 避免持久化大型数据
  • 避免持久化经常变化的变量
  • 最好小于2kb,写入操作时同步的,影响UI线程

4.3.3 从AppStorage中访问PersistantStorage初始化的属性

// 初始化PersistentStorage
PersistentStorage.persistProp('aProp', 47);

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  // 使用StorageLink
  @StorageLink('aProp') aProp: number = 48

  build() {
    Row() {
      Column() {
        Text(this.message)
        // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
        Text(`${this.aProp}`)
          .onClick(() => {
            this.aProp += 1;
          })
      }
    }
  }
}

PersistProp初始化流程如下:

image.png

4.3.4 在PersistentStorage之前访问AppStorage中的属性

在调用PersistantStorage.persistProp或者persistProps之前使用接口访问AppStorage中的属性是错误的,因为这样的调用顺序会丢失上一次应用程序运行中的属性值:

let aProp = AppStorage.setOrCreate('aProp', 47);
PersistentStorage.persistProp('aProp', 48);

应用在非首次运行时,先执行AppStorage.setOrCreate(‘aProp’, 47):属性aProp在AppStorage中创建,其类型为number,值为47,aProp是持久化的属性,所以会被写回到PersistentStorage磁盘中,上次存储的值会丢失;

PersistentStorage.persistProp(‘aProp’, 48):在PersistentStorage中查找到“aProp”,值为刚刚使用AppStorage接口写入的47;

4.3.5 在PersistentStorage之后访问AppStorage中的属性

开发者可以先判断是否需要覆盖上一次保存在PersistantStorage中的值,如果需要覆盖,再调用AppStorage的接口进行修改:

// 在读取PersistentStorage储存的数据后判断“aProp”的值是否大于50,如果大于50的话使用AppStorage的接口设置为47。

PersistentStorage.persistProp('aProp', 48);
if (AppStorage.get('aProp') > 50) {
    // 如果PersistentStorage存储的值超过50,设置为47
    AppStorage.setOrCreate('aProp',47);
}

4.4 Environment: 设备环境查询

如果需要应用程序运行的设备的环境参数,做出不同的场景判断,比如多语言,暗黑模式等,需要使用到Environment设备环境查询了。

Environment是ArkUI框架在应用程序启动时创建的单例对象,所有属性都是不可变的(应用不可写入),所有的属性都是简单类型。

4.4.1 内置参数

截屏2024-03-25 18.19.54.png

4.4.2 从UI中访问Environment参数:

  • 使用Environment.envProp将设备运行的环境变量存入AppStorage中:
// 将设备的语言code存入AppStorage,默认值为en
Environment.envProp('languageCode', 'en');

  • 可以使用@StorageProp链接到Component中:
@StorageProp('languageCode') lang : string = 'en';

设备环境到Component的更新链:Environment --> AppStorage -->Component。

@StorageProp关联的环境参数可以在本地更改,但不能同步回AppStorage中,因为应用对环境变量参数是不可写的,只能在Environment中查询。

4.4.3 应用逻辑使用Environment

// 使用Environment.EnvProp将设备运行languageCode存入AppStorage中;
Environment.envProp('languageCode', 'en');
// 从AppStorage获取单向绑定的languageCode的变量
const lang: SubscribedAbstractProperty<string> = AppStorage.prop('languageCode');

if (lang.get() === 'zh') {
  console.info('你好');
} else {
  console.info('Hello!');
}

4.4.4 限制条件

Environment和UIContext相关联,需要在UIContext明确的时候才可以调用。可以通过在runScopedTask里明确上下文。如果没有在UIContext明确的地方调用,将导致无法查询到设备环境数据。

5. 其它状态管理

除了组件状态管理和应用状态管理,ArkTS还提供了@Watch和$$来为开发者提供更多功能;

  • @Watch用于监听状态变量的变化;
  • $$运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步;

5.1 @Watch装饰器:状态变量更改通知

如果我们想要关注某个状态变量的值是否改变,可以使用@Watch为状态变量设置回调函数;@Watch在ArkUI框架内部判断数值有无更新用的是严格相等(===)。

观察变化和行为表现:

  • 当观察到状态变量变化时,对应的@Watch的回调方法将被处罚;
  • @Watch方法在自定义组件的属性变更之后同步执行;
  • 如果在@Watch的方法里改变了其它的状态变量,也会引起状态变更和@Watch的执行;
  • 在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变,只有在后续状态改变时,才会调用@Watch回调方法;

5.1.1 @Watch和自定义组件更新

@Component
struct TotalView {
  // 使用@Watch装饰器观察count的变化
  @Prop @Watch('onCountUpdated') count: number = 0;
  
  @State total: number = 0;
  
  // @Watch绑定的回调
  onCountUpdated(propName: string): void {
    this.total += this.count;
  }

  build() {
    Text(`Total: ${this.total}`)
  }
}

@Entry
@Component
struct CountModifier {
  @State count: number = 0;

  build() {
    Column() {
      Button('add to basket')
        .onClick(() => {
          this.count++
        })
      TotalView({ count: this.count })
    }
  }
}

5.2 $$语法:内置组件双向同步

$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。内部状态具体指什么取决于组件,例如 TextInput组件的text参数。

类似于Vue中的v-model???

简单的使用示例:

// xxx.ets
@Entry
@Component
struct TextInputExample {
  @State text: string = ''
  controller: TextInputController = new TextInputController()

  build() {
    Column({ space: 20 }) {
      Text(this.text)
      TextInput({ text: $$this.text, placeholder: 'input your word...', controller: this.controller })
        .placeholderColor(Color.Grey)
        .placeholderFont({ size: 14, weight: 400 })
        .caretColor(Color.Blue)
        .width(300)
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

当用户在输入时,Text展示的内容也会同步进行刷新。

当前$$支持的组件有: 截屏2024-03-25 19.08.11.png

5.3 @Track装饰器 class对象属性级更新

@Track应用于class对象的属性级更新,@Track装饰的属性变化时,只会触发该属性关联的UI更新。

@Track是class对象的属性装饰器,当一个class对象是状态变量时,@Track装饰的属性发生变化,只会触发该属性关联的UI更新,而未被标记的属性不能在UI中使用。

5.3.1 简单示例

使用@Track装饰器可以避免冗余刷新:

class LogTrack {
  @Track str1: string;
  @Track str2: string;

  constructor(str1: string) {
    this.str1 = str1;
    this.str2 = 'World';
  }
}

class LogNotTrack {
  str1: string;
  str2: string;

  constructor(str1: string) {
    this.str1 = str1;
    this.str2 = '世界';
  }
}

@Entry
@Component
struct AddLog {
  @State logTrack: LogTrack = new LogTrack('Hello');
  @State logNotTrack: LogNotTrack = new LogNotTrack('你好');

  isRender(index: number) {
    console.log(`Text ${index} is rendered`);
    return 50;
  }

  build() {
    Row() {
      Column() {
        Text(this.logTrack.str1) // UINode1
          .fontSize(this.isRender(1))
          .fontWeight(FontWeight.Bold)
        Text(this.logTrack.str2) // UINode2
          .fontSize(this.isRender(2))
          .fontWeight(FontWeight.Bold)
        Button('change logTrack.str1')
          .onClick(() => {
            this.logTrack.str1 = 'Bye';
          })
        Text(this.logNotTrack.str1) // UINode3
          .fontSize(this.isRender(3))
          .fontWeight(FontWeight.Bold)
        Text(this.logNotTrack.str2) // UINode4
          .fontSize(this.isRender(4))
          .fontWeight(FontWeight.Bold)
        Button('change logNotTrack.str1')
          .onClick(() => {
            this.logNotTrack.str1 = '再见';
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

上面这个示例中:

  • 类LogTrack中的属性均被@Track装饰器装饰,点击按钮"change logTrack.str1",此时UINode1刷新,UINode2不刷新,只有一条日志输出,避免了冗余刷新。

    Text 1 is rendered
    
    
  • 类logNotTrack中的属性均未被@Track装饰器装饰,点击按钮"change logNotTrack.str1",此时UINode3、UINode4均会刷新,有两条日志输出,存在冗余刷新。

    Text 3 is renderedText 4 is rendered
    
    

如果一个类中的属性很多,但是我们只依赖其中某一个属性,就可以将这个属性用@Track装饰,避免其他属性也触发刷新。

5.3.2 @Track和自定义组件刷新

下面这个示例展示组件更新和@Track的处理步骤,对象log是@State装饰的状态变量,logInfo是@Track的成员属性,其余成员属性都是非@Track装饰的,而且也不准备在UI中更新:

class Log {
  @Track logInfo: string;
  owner: string;
  id: number;
  time: Date;
  location: string;
  reason: string;

  constructor(logInfo: string) {
    this.logInfo = logInfo;
    this.owner = 'OH';
    this.id = 0;
    this.time = new Date();
    this.location = 'CN';
    this.reason = 'NULL';
  }
}

@Entry
@Component
struct AddLog {
  @State log: Log = new Log('origin info.');

  build() {
    Row() {
      Column() {
        Text(this.log.logInfo)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            // The properties without @Track can be used in the event handler.
            console.log('owner: ' + this.log.owner +
              ' id: ' + this.log.id +
              ' time: ' + this.log.time +
              ' location: ' + this.log.location +
              ' reason: ' + this.log.reason);
            this.log.time = new Date();
            this.log.id++;

            this.log.logInfo += ' info.';
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

怎样学习鸿蒙?

首先必学的是开发语言 ArkTS,这是重中之重,然后就是ArkUI声明式UI开发、Stage模型、网络/数据库管理、分布式应用开发、进程间通信与线程间通信技术、OpenHarmony多媒体技术……。中间还有许多的知识点,都整理成思维导图来分享给大家~
在这里插入图片描述
此外,小编精心准备了一份联合鸿蒙官方发布笔记整理收纳的《鸿蒙开发学习笔记》,内容包含ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

==【有需要的小伙伴,可以扫描下方二维码免费领取!!!】 ==

快速入门

  • 开发准备
  • 构建第一个ArkTS应用(Stage模型)
  • 构建第一个ArkTS应用(FA模型)
  • 构建第一个JS应用(FA模型)
    在这里插入图片描述

开发基础知识

  • 应用程序包基础知识
  • 应用配置文件(Stage模型)
  • 应用配置文件概述(FA模型)
    在这里插入图片描述

资源分类与访问

  • 资源分类与访问
  • 创建资源目录和资源文件
  • 资源访问
    在这里插入图片描述

学习ArkTs语言

  • 初识ArkTS语言
  • 基本语法
  • 状态管理
  • 其他状态管理
  • 渲染控制
    在这里插入图片描述

基于ArkTS声明式开发范式

  • UI开发(ArkTS声明式开发范式)概述
  • 开发布局
  • 添加组件
  • 显示图片
  • 使用动画
  • 支持交互事件
  • 性能提升的推荐方法

在这里插入图片描述

兼容JS的类Web开发范式

  • 概述
  • 框架说明
  • 构建用户界面
  • 常见组件开发指导
  • 动效开发指导
  • 自定义组件
    在这里插入图片描述

Web组件

  • 概述
  • 设置基本属性和事件
  • 并发
  • 窗口管理
  • WebGL
  • 媒体
  • 安全
  • 网络与连接
  • 电话服务
  • 数据管理

  • 在这里插入图片描述

应用模型

  • 概述
  • Stage模型开发指导
  • FA模型开发指导
    在这里插入图片描述
2024完整鸿蒙学习资料领取方式:扫描下方二维码即可
  • 24
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值