注:以下内容是对鸿蒙开发文档的入门章节的 ArkTS 部分的学习整理,仅供参考。

1. ArkTS 概述

  1. ArkTS

    • HarmonyOS 的应用开发语言
    • 围绕应用开发在 TypeScript 基础上的进一步扩展
  2. ArkTS Vs. TypeScript

    • 强制使用静态类型

    • 取消动态类型特性(如 any、unknown 类型的使用等)

    • 禁止在运行时改变对象布局

    • 限制运算符语义

    • 不支持结构类型

    • 与 JavaScript 无缝互通

    • 支持 ArkUI 框架的声明式语法和其他特性(如自定义组件、状态管理、条件渲染、循环渲染等)

2. ArkTS 编程规范

规则分为两个级别:要求、建议。

2.1 代码风格

  1. 标识符命名【建议】

    标识符 命名规则 示例
    类名、枚举名、命名空间名 UpperCamelCase Worker
    变量名、方法名、参数名 lowerCamelCase sendMsg
    常量名、枚举值 全部大写,单词间使用下划线隔开 MAX_USER_SIZE
    布尔变量名 避免使用否定的布尔变量名 isError、isFound
  2. 代码格式

    • 使用空格缩进,禁止使用 tab 字符(可以在 IDE 或代码编辑器中配置,将 Tab 键自动扩展为 2 个空格)【建议】

    • 行宽不超过 120 个字符(除非命令、URL、error 信息等)【建议】

    • 条件语句和循环语句的实现必须使用大括号(即使条件体/循环体只有一行代码)【建议】

    • 表达式换行需保持一致性,运算符放行末【建议】

      1
      2
      3
      4
      if (userCount > MAX_USER_COUNT ||
      userCount < MIN_USER_COUNT) {
      doSomething();
      }
    • 多个变量定义和赋值语句不允许写在一行要求

      1
      2
      3
      4
      5
      6
      7
      8
      let maxCount = 10, isCompleted = false;
      let pointX, pointY;
      pointX = 10; pointY = 0;
      // 应改为
      let maxCount = 10;
      let isCompleted = false;
      let pointX = 0;
      let pointY = 0;
    • 建议字符串使用单引号【建议】

    • 对象字面量属性超过 4 个,需要都换行【建议】

    • 把 else/catch 放在 if/try 代码块关闭括号的同一行【建议】

    • 大括号 { 和语句在同一行【建议】

2.2 编程实践

  1. 建议添加类属性的可访问修饰符【建议】
    • 在 ArkTS 中,提供了 private, protected 和 public 可访问修饰符。
    • 默认情况下一个属性的可访问修饰符为 public。
    • 如果类中包含 private 属性,无法通过对象字面量初始化该类。
  2. 不建议省略浮点数小数点前后的 0【建议】
  3. 判断变量是否为 Number.NaN 时必须使用 Number.isNaN() 方法要求
  4. 数组遍历优先使用 Array 对象方法要求
    • 对于数组的遍历处理,应该优先使用 Array 对象方法,如:forEach(), map(), every(), filter(), find(), findIndex(), reduce(), some()。
  5. 不要在控制性条件表达式中执行赋值操作【建议】
  6. 在 finally 代码块中,不要使用 return、break、continue 或抛出异常,避免 finally 块非正常结束要求
    • 非正常结束的 finally 代码块会影响 try 或 catch 代码块中异常的抛出,也可能会影响方法的返回值。
    • 所以要保证 finally 代码块正常结束。
  7. 避免使用 ESObject【建议】
    • ESObject 主要用在 ArkTS 和 TS/JS 跨语言调用场景中的类型标注,在非跨语言调用场景中使用 ESObject 标注类型,会引入不必要的跨语言调用,造成额外性能开销。
  8. 使用 T[] 表示数组类型【建议】

3. TS 到 ArkTS 的适配规则

约束分为两个级别:错误、警告。

  • 错误:必须要遵从的约束,否则会导致程序编译失败。
  • 警告:推荐遵从的约束,未来可能会导致程序编译失败。

3.1 约束概述

  1. 强制使用静态类型:ArkTS 中禁止使用 any 类型,有如下优势,
    • 开发者能够容易理解代码中使用了哪些数据结构
    • 减少运行时的类型检查,有助于提升性能
  2. 禁止在运行时变更对象布局:ArkTS 禁止以下行为,
    • 向对象中添加新的属性或方法。
    • 从对象中删除已有的属性或方法。
    • 任意类型的值赋值给对象属性。
  3. 限制运算符的语义:如一元运算符 + 只能作用于数值类型。
  4. 不支持结构类型(structural typing):如结构完全相同的两个类被视作两个不同的类型。

3.2 约束说明

规则 解释 级别 补充
arkts-identifiers-as-prop-names 对象的属性名必须是合法的标识符(即非数值和字符串) 错误 ArkTS 支持属性名为字符串字面量{['name']: "Jack"})和枚举中的字符串值{[Test.A]: 1})。
arkts-no-symbol 不支持 Symbol() API 错误 ArkTS 只支持 Symbol.iterator,使自定义对象支持迭代。
arkts-no-private-identifiers 不支持以 # 开头的私有字段 错误 改用 private 关键字声明私有字段。
arkts-unique-names 类型、命名空间的命名必须唯一(即与变量名和函数名不同) 错误 类型包括:类、接口、枚举。
arkts-no-var 使用 let 而非 var 错误
arkts-no-any-unknown 使用具体的类型而非 anyunknown 错误 ArkTS 要求显式指定具体类型。
arkts-no-call-signatures 使用 class 而非具有 call signature 的类型 错误 调用签名(call signature)用于描述函数类型,包括函数的参数和返回值类型。如:type Demo = { (someArg: string): string } 就使用了调用签名。
arkts-no-ctor-signatures-type 使用 class 而非具有构造签名的类型 错误 构造签名(constructor signature)用于描述构造函数类型。如 type Demo = { new (s: string): SomeObject } 就使用了构造签名。
arkts-no-multiple-static-blocks 仅支持一个静态块 错误 静态块(Static Block)是在类中通过 static {} 定义的一段静态代码,这段代码在类加载时执行一次,常用于初始化静态属性或执行其他静态初始化逻辑。ArkTS 当前不支持静态块语法,支持后,.ets 文件中使用静态块时需遵循本约束。
arkts-no-indexed-signatures 不支持 index signature 错误 type Demo = { [index: number]: string } 就使用了索引签名。
arkts-no-intersection-types 使用继承而非 intersection type 错误 type Employee = Identity & Contact 就使用了交叉类型。ArkTs 支持使用接口继承实现类似效果,如 interface Employee extends Identity, Contact
arkts-no-typing-with-this 不支持 this 类型 错误 改用显式具体类型。
arkts-no-conditional-types 不支持条件类型 错误 type Y<T> = T extends Array<infer Item> ? Item: never 就使用了条件类型。ArkTS 同样不支持 infer 关键字(infer 关键字在 TypeScript 中用于在条件类型中推断类型)。
arkts-no-ctor-prop-decls 不支持在 constructor 中声明字段 错误 constructor(protected ssn: string){ } 就在 constructor 中声明了字段,即 TypeScript 中类的字段声明兼初始化的简写形式。
arkts-no-ctor-signatures-iface 接口中不支持构造签名 错误 改用函数或方法。如 interface I { create(s: string): I } 也实现了类似的效果。
arkts-no-aliases-by-index 不支持索引访问类型 错误
arkts-no-props-by-index 不支持通过索引访问字段(即 obj[field] 的方式) 错误 ArkTS 不支持动态声明字段,不支持动态访问字段。ArkTS 支持通过索引访问 TypedArray (例如 Int32Array)中的元素(TypedArray 是是 JavaScript 中一种用于处理二进制数据的对象类型)。
arkts-no-structural-typing 不支持 structural typing 错误 编译器无法比较两种类型的 publicAPI 并决定它们是否相同。改用继承、接口或类型别名。
arkts-no-inferred-generic-params 需要显式标注泛型函数类型实参 错误 如果可以从传递给泛型函数的参数中推断出具体类型,ArkTS 允许省略泛型类型实参,否则,省略泛型类型实参会发生编译时错误。禁止仅基于泛型函数返回类型推断泛型类型参数,如 function greet<T>(): T { return 'Hello' as T; }greet() 调用时,泛型类型参数 T 被推断为 unknown
arkts-no-untyped-obj-literals 需要显式标注对象字面量的类型 错误 在 ArkTS 中,需要显式标注对象字面量的类型,否则,将发生编译时错误。在某些场景下,编译器可以根据上下文推断出字面量的类型。在以下上下文中不支持使用字面量初始化类和接口:初始化具有 any、Object 或 object 类型的任何对象;初始化带有方法的类或接口;初始化包含自定义含参数的构造函数的类;初始化带 readonly 字段的类。
arkts-no-obj-literals-as-types 对象字面量不能用于类型声明 错误 let o: {x: number, y: number} 使用了对象字面量类型声明。改用类或者接口声明类型。
arkts-no-noninferrable-arr-literals 数组字面量必须仅包含可推断类型的元素 错误 本质上,ArkTS 将数组字面量的类型推断为数组所有元素的联合类型。如果其中任何一个元素的类型无法根据上下文推导出来(例如,无类型的对象字面量),则会发生编译时错误。
arkts-no-func-expressions 使用箭头函数而非函数表达式 错误 let f = function(s: string) { console.log(s); } 是函数表达式,let f = (s: string) => { console.log(s); } 是箭头函数。
arkts-no-class-literals 不支持使用类表达式 错误 必须显式声明一个类。如 const Rectangle = class { } 是类表达式。
arkts-implements-only-iface 类不允许被 implements 错误 ArkTS 不允许类被 implements,只有接口可以被 implements
arkts-no-method-reassignment 不支持修改对象的方法 错误 改用封装函数或者继承。
arkts-as-casts 类型转换仅支持 as T 语法 错误 在 ArkTS 中,as 关键字是类型转换的唯一语法,错误的类型转换会导致编译时错误或者运行时抛出 ClassCastException 异常。ArkTS 不支持使用<type> 语法进行类型转换。当需要将 primitive 类型(如 numberboolean)转换成引用类型时,请使用 new 表达式,如 new Number(5.0) 而不是 5.0 as Number
arkts-no-jsx 不支持 JSX 表达式 错误
arkts-no-polymorphic-unops 一元运算符 +-~ 仅适用于数值类型 错误 与 TypeScript 不同,ArkTS 不支持隐式将字符串转换成数值,必须进行显式转换。
arkts-no-delete 不支持 delete 运算符 错误 ArkTS 中,对象布局在编译时就确定了,且不能在运行时被更改。
arkts-no-type-query 仅允许在表达式中使用 typeof 运算符 错误 ArkTS 仅支持在表达式中使用 typeof 运算符,不允许使用 typeof 作为类型。即只允许 typeof 作值运算,而不是类型运算。
arkts-instanceof-ref-types 部分支持 instanceof 运算符 错误 在 ArkTS 中,instanceof 运算符的左操作数的类型必须为引用类型(例如,对象、数组或者函数),否则会发生编译时错误。此外,在 ArkTS 中,instanceof 运算符的左操作数不能是类型,必须是对象的实例
arkts-no-in 不支持 in 运算符 错误 in 运算符是 JavaScript 和 TypeScript 中的一个内置运算符,用于检查对象是否具有指定的属性(包括继承的属性)。
arkts-no-destruct-assignment 不支持解构赋值 错误 包括数组解构和对象解构。如 let [one, two] = [1, 2]; 就是解构赋值。
arkts-no-comma-outside-loops 逗号运算符,仅用在 for 循环语句中 错误 注意与声明变量、函数参数传递时的逗号分隔符不同。如 for(let i = 0, j = 0; i< 10; ++i, j += 2) { } 就使用了逗号运算符。
arkts-no-destruct-decls 不支持解构变量声明 错误 let {x, y} = returnZeroPoint(); 就是解构变量声明。
arkts-no-types-in-catch 不支持在 catch 语句标注类型 错误 在 TypeScript 的 catch 语句中,只能标注 anyunknown 类型。由于 ArkTS 不支持这些类型,应省略类型标注。
arkts-no-for-in 不支持 for .. in 错误
arkts-no-mapped-types 不支持映射类型 错误 type OptionsFlags<Type> = { [Property in keyof Type]: boolean } 就是映射类型。
arkts-no-with 不支持 with 语句 错误 with 语句是 JavaScript 中的一种语法结构,用于将代码的作用域设置为特定的对象,从而简化对该对象属性的访问。很不推荐使用。
arkts-limited-throw 限制 throw 语句中表达式的类型 错误 ArkTS 只支持抛出 Error 类或其派生类的实例。禁止抛出其他类型(例如 number 或 string)的数据。
arkts-no-implicit-return-types 限制省略函数返回类型标注 错误 ArkTS 在部分场景中支持对函数返回类型进行推断。return 语句中的表达式是对某个函数或方法进行调用,且该函数或方法的返回类型没有被显著标注时,会出现编译时错误。在这种情况下,请标注函数返回类型。
arkts-no-destruct-params 不支持参数解构的函数声明 错误 function drawText({ text = '', location: [x, y] = [0, 0], bold = false }) { } 就是函数声明的参数解构。
arkts-no-nested-funcs 不支持在函数内声明函数 错误 ArkTS 不支持在函数内声明函数,改用 lambda 函数。如 let logToConsole: (message: string) => void = (message: string): void => { } 就是 lambda 函数(即箭头函数)。
arkts-no-standalone-this 不支持在函数和类的静态方法中使用 this 错误 ArkTS 不支持在函数和类的静态方法中使用 this,只能在类的实例方法中使用 this
arkts-no-generators 不支持生成器函数 错误 改用使用 asyncawait 机制进行并行任务处理。
arkts-no-is 使用 instanceofas 进行类型保护 错误 ArkTS 不支持 is 运算符,必须用 instanceof 运算符替代。在使用之前,必须使用 as 运算符将对象转换为需要的类型。
arkts-no-spread 部分支持展开运算符 错误 ArkTS 仅支持使用展开运算符展开数组、Array 的子类和 TypedArray(例如 Int32Array)。仅支持使用在以下场景中:传递给剩余参数时;复制一个数组到数组字面量。
arkts-no-extend-same-prop 不能继承具有相同方法的两个接口 错误
arkts-no-decl-merging 不支持声明合并 错误 ArkTS 不支持类、接口的声明合并。
arkts-extends-only-class 接口不能继承类 错误 ArkTS 不支持接口继承类,接口只能继承接口。
arkts-no-ctor-signatures-funcs 不支持构造函数类型 错误 改用 lambda 函数。如 type PersonCtor = new (name: string, age: number) => Person 就使用了构造函数类型。
arkts-no-enum-mixed-types 只能使用类型相同的编译时表达式初始化枚举成员 错误 ArkTS 不支持使用在运行期间才能计算的表达式来初始化枚举成员。此外,枚举中所有显式初始化的成员必须具有相同的类型
arkts-no-enum-merging 不支持 enum声明合并 错误
arkts-no-ns-as-obj 命名空间不能被用作对象 错误
arkts-no-ns-statements 不支持命名空间中的非声明语句 错误 在 ArkTS 中,命名空间用于定义标识符可见范围,只在编译时有效。因此,不支持命名空间中的非声明语句。可以将非声明语句写在函数中。如 const x = 1 就是声明语句。
arkts-no-require 不支持 requireimport 赋值表达式 错误 import m = require('mod') 就使用了 requireimport 赋值表达式。
arkts-no-export-assignment 不支持 export = ... 语法 错误 改用常规的 exportimport
arkts-no-ambient-decls 不支持 ambient module 声明 错误 由于 ArkTS 本身有与 JavaScript 交互的机制, ArkTS 不支持 ambient module 声明。如 declare module 'someModule' { } 就是一个 ambient module 声明。
arkts-no-module-wildcards 不支持在模块名中使用通配符 错误 declare module '*!text' { } 声明了一个通配模块,匹配所有通过 !text 插件导入的模块。
arkts-no-umd 不支持通用模块定义(UMD) 错误
arkts-no-new-target 不支持 new.target 错误 ArkTS 没有原型的概念,因此不支持 new.target。此特性不符合静态类型的原则。new.target 是 JavaScript 中的一个仅在构造函数中可用的元属性,它指向被调用的构造函数,如果构造函数不是通过 new 关键字调用的,则 new.targetundefined
arkts-no-definite-assignment 不支持确定赋值断言 警告 改为在声明变量的同时为变量赋值。如 let x!: number 就使用了确定赋值断言。
arkts-no-prototype-assignment 不支持在原型上赋值 错误 ArkTS 没有原型的概念,因此不支持在原型上赋值。此特性不符合静态类型的原则。
arkts-no-globalthis 不支持 globalThis 警告 由于 ArkTS 不支持动态更改对象的布局,因此不支持全局作用域和 globalThis。
arkts-no-utility-types 不支持一些 utility 类型 错误 ArkTS 仅支持 PartialRequiredReadonlyRecord,不支持TypeScript 中其他的 Utility Types。对于 Record 类型的对象,通过索引访问到的值的类型是包含 undefined 的联合类型。
Partial<T>:将类型 T 的所有属性变为可选。
Required<T>:将类型 T 的所有属性变为必选。
Readonly<T>:将类型 T 的所有属性变为只读。
Record<K, T>:构造一个对象类型,其属性键为类型 K,属性值为类型 T
arkts-no-func-props 不支持对函数声明属性 错误 由于 ArkTS 不支持动态改变函数对象布局,因此,不支持对函数声明属性。
arkts-no-func-apply-call 不支持 Function.applyFunction.call 错误 在 ArkTS 中,this 的语义仅限于传统的 OOP 风格,函数体中禁止使用 this
arkts-no-func-bind 不支持 Function.bind 警告 在 ArkTS 中,this 的语义仅限于传统的 OOP 风格,函数体中禁止使用 this
arkts-no-as-const 不支持 as const 断言 错误 在标准 TypeScript 中,as const 用于标注字面量的相应字面量类型,而 ArkTS 不支持字面量类型。
arkts-no-import-assertions 不支持导入断言 错误 改用常规的 import 语法。如 import data from './data.json' assert { type: 'json' }; 断言指定模块类型为 JSON,这是 ECMAScript 语法。
arkts-limited-stdlib 限制使用标准库 错误 ArkTS 不允许使用 TypeScript 或 JavaScript 标准库中的某些接口。大部分接口与动态特性有关。ArkTS 不允许使用全局对象的属性和方法: Infinity, NaN, isFinite, isNaN, parseFloat, parseInt,但可以使用Number 对应的属性和方法。
arkts-strict-typing 强制进行严格类型检查 错误 在编译阶段,会进行 TypeScript 严格模式的类型检查,包括:noImplicitReturns, strictFunctionTypes, strictNullChecks, strictPropertyInitialization
arkts-strict-typing-required 不允许通过注释关闭类型检查 错误 不允许通过注释关闭类型检查,不支持使用 @ts-ignore@ts-nocheck
arkts-no-ts-deps 允许 .ets 文件 import.ets/.ts/.js 文件源码, 不允许 .ts/.js 文件 import.ets 文件源码 错误
arkts-no-classes-as-obj class 不能被用作对象 警告 在 ArkTS 中,class 声明的是一个新的类型,不是一个值。因此,不支持将 class 用作对象(例如将 class 赋值给一个对象)。
arkts-no-misplaced-imports 不支持在 import 语句前使用其他语句 错误 在 ArkTS 中,除动态 import 语句外,所有 import 语句需要放在所有其他语句之前。如 import('module2').then(() => {}).catch(() => {}) 就是动态 import 语句。
arkts-limited-esobj 限制使用 ESObject 类型 警告 为了防止动态对象(来自 .ts/.js 文件)在静态代码(.ets 文件)中的滥用,ESObject 类型在 ArkTS 中的使用是受限的。唯一允许使用 ESObject 类型的场景是将其用在局部变量的声明中。ESObject 类型变量的赋值也是受限的,只能被来自跨语言调用的对象赋值,例如:ESObject、any、unknown、匿名类型等类型的变量。禁止使用静态类型的值(在 .ets 文件中定义的)初始化 ESObject 类型变量。ESObject 类型变量只能用在跨语言调用的函数里或者赋值给另一个 ESObject 类型变量。

ArkTS 不允许使用的标准库接口为,

1
2
3
4
5
6
7
全局对象的属性和方法:eval

Object:__proto__、__defineGetter__、__defineSetter__、__lookupGetter__、__lookupSetter__、assign、create、defineProperties、defineProperty、freeze、fromEntries、getOwnPropertyDescriptor、getOwnPropertyDescriptors、getOwnPropertySymbols、getPrototypeOf、hasOwnProperty、is、isExtensible、isFrozen、isPrototypeOf、isSealed、preventExtensions、propertyIsEnumerable、seal、setPrototypeOf

Reflect:apply、construct、defineProperty、deleteProperty、getOwnPropertyDescriptor、getPrototypeOf、isExtensible、preventExtensions、setPrototypeOf

Proxy:handler.apply()、handler.construct()、handler.defineProperty()、handler.deleteProperty()、handler.get()、handler.getOwnPropertyDescriptor()、handler.getPrototypeOf()、handler.has()、handler.isExtensible()、handler.ownKeys()、handler.preventExtensions()、handler.set()、handler.setPrototypeOf()

4. TS 到 ArkTS 的适配示例

4.1 代码更改

  1. arkts-identifiers-as-prop-names

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let wantInfo: W = {
    'bundleName': 'com.huawei.hmos.browser',
    'action': 'ohos.want.action.viewData',
    'entities': ['entity.system.browsable']
    }

    // 更改为

    let wantInfo: W = {
    bundleName: 'com.huawei.hmos.browser',
    action: 'ohos.want.action.viewData',
    entities: ['entity.system.browsable']
    }
  2. arkts-no-any-unknown

    1
    2
    3
    4
    5
    function printProperties(obj: any) { }

    // 更改为

    function printProperties(obj: Record<string, Object>) { }
  3. arkts-no-call-signature

    1
    2
    3
    4
    5
    6
    7
    interface I {
    (value: string): void;
    }

    // 更改为

    type I = (value: string) => void
  4. arkts-no-ctor-signatures-type, arkts-no-ctor-signatures-iface

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    type ControllerConstructor = {
    new (value: string): Controller;
    }

    /* 或者,
    interface ControllerConstructor {
    new (value: string): Controller;
    }
    */

    class Menu {
    controller: ControllerConstructor = Controller // new this.controller(value) 创建 Controller 对象
    }

    // 更改为

    type ControllerConstructor = () => Controller;

    class Menu {
    controller: ControllerConstructor = (value: string) => { // this.controller(value) 创建 Controller 对象
    return new Controller(value);
    }
    }
  5. arkts-no-indexed-signatures

    1
    2
    3
    4
    5
    function foo(data: { [key: string]: string }) { }

    // 更改为

    function foo(data: Record<string, string>) { }
  6. arkts-no-typing-with-this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class C {
    getInstance(): this {
    return this;
    }
    }

    // 更改为

    class C {
    getInstance(): C {
    return this;
    }
    }
  7. arkts-no-inferred-generic-params

    1
    2
    3
    4
    5
    let originMenusMap: Map<string, C> = new Map(arr.map(item => [item.str, (item instanceof C) ? item: null]));

    // 更改为

    let originMenusMap: Map<string, C | null> = new Map<string, C | null>(arr.map<[string, C | null]>(item => [item.str, (item instanceof C) ? item: null]));
  8. arkts-no-regexp-literals

    1
    2
    3
    4
    5
    let regex: RegExp = /\s*/g;

    // 更改为

    let regexp: RegExp = new RegExp('\\s*','g'); // 在字符串中需要对反斜杠进行转义,因此这里使用了双反斜杠 \\。
  9. arkts-no-untyped-obj-literals

    • 从 SDK 中导入类型,标注 object literal 类型
    • class 为 object literal 标注类型,需要 class 的构造函数无参数
    • class/interface 为 object literal 标注类型,需要使用 identifier 作为 object literal 的 key
    • 使用 Record 为 object literal 标注类型,需要使用字符串作为 object literal 的 key
  10. arkts-no-obj-literals-as-types

    1
    2
    3
    4
    5
    type Person = { name: string, age: number }

    // 更改为

    interface Person { name: string, age: number }
  11. arkts-no-mapped-types

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class C {
    a: number = 0
    b: number = 0
    c: number = 0
    }
    type OptionsFlags = {
    [Property in keyof C]: string
    }

    // 更改为

    type OptionsFlags = Record<keyof C, string>
  12. arkts-no-globalthis(通过构建单例对象实现全局对象的功能)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // GlobalContext.ts
    // 构造单例对象
    export class GlobalContext {
    private constructor() {}
    private static instance: GlobalContext;
    private _objects = new Map<string, Object>();

    public static getContext(): GlobalContext {
    if (!GlobalContext.instance) {
    GlobalContext.instance = new GlobalContext();
    }
    return GlobalContext.instance;
    }

    getObject(value: string): Object | undefined {
    return this._objects.get(value);
    }

    setObject(key: string, objectClass: Object): void {
    this._objects.set(key, objectClass);
    }
    }

    // file1.ts
    import { GlobalContext } from '../GlobalContext'

    export class Test {
    value: string = '';
    foo(): void {
    GlobalContext.getContext().setObject('value', this.value);
    }
    }

    // file2.ts
    import { GlobalContext } from '../GlobalContext'

    GlobalContext.getContext().getObject('value');
  13. arkts-no-special-imports

    1
    2
    3
    4
    5
    import type {A, B, C, D } from '***'

    // 更改为

    import {A, B, C, D } from '***'
  14. arkts-no-side-effects-imports

    1
    2
    3
    4
    5
    import 'module'

    // 更改为

    import('module')

4.2 拷贝实现

  1. 浅拷贝

    1
    2
    3
    4
    5
    6
    7
    function shallowCopy(obj: object): object {
    let newObj: Record<string, Object> = {};
    for (let key of Object.keys(obj)) {
    newObj[key] = obj[key];
    }
    return newObj;
    }
  2. 深拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function deepCopy(obj: object): object {
    let newObj: Record<string, Object> | Object[] = Array.isArray(obj) ? [] : {};
    for (let key of Object.keys(obj)) {
    if (typeof obj[key] === 'object') {
    newObj[key] = deepCopy(obj[key]);
    } else {
    newObj[key] = obj[key];
    }
    }
    return newObj;
    }

5. ArkTS 编程实践 - 高性能

  1. 使 const 声明不变的变量。
  2. number 类型变量避免整型和浮点型混用。
  3. 数值计算避免溢出
    • 加法、减法、乘法、指数运算等:避免大于 INT32_MAX 或小于 INT32_MIN。
    • &(and)、>>>(无符号右移)等:避免大于 INT32_MAX。
  4. 循环中提取常量,减少属性访问次数。
  5. 建议使用参数传递函数外的变量。
  6. 避免使用可选参数,改用必选参数并考虑参数默认值的使用。
  7. 数值数组推荐使用 TypedArray。
  8. 避免使用稀疏数组。
  9. 避免使用联合类型数组。
  10. 避免频繁抛出异常。

6. UI 范式基本语法

6.1 ArkTS 的基本组成

  1. 装饰器: 用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如 @Component 表示自定义组件,@Entry 表示该自定义组件为入口组件,@State 表示组件中的状态变量,状态变量变化会触发 UI 刷新。
  2. UI 描述:以声明式的方式来描述 UI 的结构,例如 build() 方法中的代码块。
  3. 自定义组件:可复用的 UI 单元,可组合其他组件。
  4. 系统组件:ArkUI 框架中默认内置的基础和容器组件,可直接被开发者调用,比如 ColumnTextDividerButton
  5. 属性方法:组件可以通过链式调用配置多项属性,如 fontSize()width()height()backgroundColor() 等。
  6. 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在 Button 后面的 onClick()
  7. 扩展语法
    • @Builder/@BuilderParam:特殊的封装 UI 描述的方法,细粒度的封装和复用 UI 描述。
    • @Extend/@Styles:扩展内置组件和封装属性样式,更灵活地组合内置组件。
    • stateStyles:多态样式,可以依据组件的内部状态的不同,设置不同样式。

6.2 声明式 UI 描述

  1. 组件创建

    1
    2
    3
    4
    5
    6
    // 无参数
    Divider()

    // 有参数(参数可以是常量、变量或表达式)
    Image('https://xyz/test.jpg')
    Image('https://' + this.imageUrl)
  2. 属性配置:以 “.” 链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行。

    1
    2
    3
    4
    5
    6
    7
    8
    // 属性值可以是常量、变量、表达式或内置枚举值
    Text('hello')
    .fontSize(20)
    .fontColor(Color.Red)
    .fontWeight(FontWeight.Bold)
    Image('test.jpg')
    .width(this.count % 2 === 0 ? 100 : 200)
    .height(this.offset + 100)
  3. 事件配置:以 “.” 链式调用的方式配置系统组件支持的事件,建议每个事件方法单独写一行。

    1
    2
    3
    4
    Button('Click me')
    .onClick(() => {
    this.myText = 'ArkUI';
    })
  4. 子组件配置:在尾随闭包 "{...}" 中为组件添加子组件的 UI 描述。

    1
    2
    3
    4
    5
    6
    7
    8
    Column() {
    Text('Hello')
    .fontSize(100)
    Divider()
    Text(this.myText)
    .fontSize(100)
    .fontColor(Color.Red)
    }

    只有容器组件才支持子组件配置。

6.3 自定义组件

组件语法

  1. 组件:在 ArkUI 中,UI 显示的内容均为组件,由框架直接提供的称为系统组件;由开发者定义的称为自定义组件,其具有以下特点,

    • 可组合:组合使用系统组件、及其属性和方法。

    • 可重用:可以被其他组件重用。

      通过 export 导出该自定义组件,并在使用的页面通过 import 导入该自定义组件,并在 build() 函数中多次创建,实现自定义组件的重用。

    • 数据驱动 UI 更新:状态变量的改变,驱动 UI 刷新。

  2. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    @Entry
    @Resuable
    @Component
    struct 自定义组件名 {
    build() {
    // 自定义组件的 UI 描述
    }
    }
    • struct:关键字,通过 struct 自定义组件名 {...} 构成自定义组件

      • 自定义组件名、类名、函数名不能和系统组件名相同。
    • @component:该装饰器仅能装饰 struct 关键字声明的数据结构,使其具备组件化的能力,需要实现 build 方法描述 UI。

      • 从 API version 11 开始,@Component 可以接受一个可选的 bool 类型参数 freezeWhenInactive,表示是否开启组件冻结。

        1
        2
        3
        @Component({ freezeWhenInactive: true })
        struct MyComponent {
        }
    • build():函数,定义自定义组件的声明式 UI 描述

    • @Entry:该装饰器装饰的自定义组件将作为 UI 页面的入口

      • 在单个 UI 页面中,最多可以使用 @Entry 装饰一个自定义组件。

      • 从API version 10开始,@Entry 可以接受一个可选的 options 参数。该参数结构为,

        1
        2
        3
        4
        5
        6
        type options = LocalStorage | EntryOptions;
        interface EntryOptions {
        routerName?: string, // 表示作为命名路由页面的名字。
        storage?: LocalStorage, // 页面级的 UI 状态存储。
        useSharedStorage?: boolean // 是否使用 LocalStorage.getShared() 接口返回的 LocalStorage 实例对象,默认值 false。该字段的优先级高于 storage 字段。
        }
        1
        2
        3
        4
        @Entry({ routeName : 'myPage' })
        @Component
        struct MyComponent {
        }
    • @Resuable:该装饰器装饰的自定义组件具备可复用能力

  3. 成员函数/变量

    • 约束:自定义组件的成员函数/变量为私有的,且不建议声明成静态函数
    • 初始化:自定义组件的成员变量可以本地初始化,也可以从父组件通过参数传递初始化
  4. build():所有声明在 build() 函数的语句统称为 UI 描述,其有以下约束,

    • @Entry 装饰的自定义组件:build() 函数下的根节点唯一且必要,且必须为容器组件,其中 ForEach 禁止作为根节点

    • @Component 装饰的自定义组件:build() 函数下的根节点唯一且必要,可以为非容器组件,其中 ForEach 禁止作为根节点

    • 不允许声明本地变量

    • 不允许直接使用 console.info

    • 不允许创建本地的作用域

    • 不允许直接调用没有用 @Builder 装饰的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @Component
      struct ParentComponent {
      doSomeCalculations() {
      }

      @Builder doSomeRender() {
      Text(`Hello World`)
      }

      build() {
      Column() {
      // 反例:不能调用没有用@Builder装饰的方法
      this.doSomeCalculations();
      // 正例:可以调用
      this.doSomeRender();
      }
      }
      }
    • 不允许使用 switch 语法,改用 if

    • 不允许使用表达式,改用 if

    • 不允许直接改变状态变量。因为 UI=f(State)UI = f(State),所以不能在自定义组件的 build()@Builder 方法里直接改变状态变量,这可能会造成循环渲染的风险。API8 及以前,ArkUI 采用全量更新,API9 至今,ArkUI 采用最小化更新。在 build() 函数中直接修改状态变量的情况包括,

      • @Builder@Extend@Styles 方法内改变状态变量

      • 在计算参数时调用函数中改变应用状态变量,例如 Text('${this.calcLabel()}')

      • 对当前数组做出修改,sort() 改变了数组 this.arr,随后的 filter() 方法会返回一个新的数组。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        // 反例
        @State arr : Array<...> = [ ... ];
        ForEach(this.arr.sort().filter(...),
        item => {
        ...
        })
        // 正确的执行方式为:filter返回一个新数组,后面的sort方法才不会改变原数组this.arr
        ForEach(this.arr.filter(...).sort(),
        item => {
        ...
        })
  5. 通用样式:自定义组件通过 “.” 链式调用的形式设置通用样式。其本质上是给自定义组件套了一个不可见的容器组件,而这些样式是设置在容器组件上的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Component
    struct ChildComponent {
    build() {
    Button(`Hello World`)
    }
    }

    @Entry
    @Component
    struct MyComponent {
    build() {
    Row() {
    ChildComponent()
    .width(200)
    .height(300)
    .backgroundColor(Color.Red)
    }
    }
    }

生命周期

  • 自定义组件@Component 装饰的 UI 单元,可以组合多个系统组件实现 UI 的复用,可以调用组件的生命周期。
  • 页面:应用的 UI 页面。可以由一个或者多个自定义组件组成,@Entry 装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个 @Entry。只有被 @Entry 装饰的组件才可以调用页面的生命周期。
image-20250210165935338
组件生命周期

组件生命周期,即 @Component 装饰的自定义组件的生命周期。

Hook 时机 允许 禁止
aboutToAppear 组件即将出现(创建自定义组件的新实例后,执行其 build() 函数之前)
onDidBuild 组件 build() 函数执行完成之后 埋点数据上报 更改状态变量、使用 animateTo 等,使得 UI 不稳定
aboutToDisappear 组件析构销毁之前 更改状态变量,特别是 @Link变量,使得程序不稳定;使用 async await,阻止了自定义组件的垃圾回收
页面生命周期

页面生命周期,即 @Entry 装饰的自定义组件的生命周期。

Hook 时机 备注
onPageShow 页面每次显示(包括路由过程、应用进入前台等)
onPageHide 页面每次隐藏(包括路由过程、应用进入后台等)
onBackPress 用户点击返回按钮 返回 true 表示页面自己处理返回逻辑,不进行页面路由;
返回 false 表示使用默认的路由返回逻辑,不设置返回值按照 false 处理

自定义布局

通过以下接口,可以以测算的方式布局自定义组件内子组件的位置

Hook 时机 作用
onMeasureSize 组件每次布局时,先于 onPlaceChildren 计算子组件的尺寸
onPlaceChildren 组件每次布局时 设置子组件的起始位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
CustomLayout({ builder: ColumnChildren })
}
}
}

// 通过 builder 的方式传递多个组件,作为自定义组件的一级子组件(即不包含容器组件,如 Column)
@Builder
function ColumnChildren() {
ForEach([1, 2, 3], (index: number) => { // 暂不支持 lazyForEach 的写法
Text('S' + index)
.fontSize(30)
.width(100)
.height(100)
.borderWidth(2)
.offset({ x: 10, y: 20 })
})
}

@Component
struct CustomLayout {
@Builder
doNothingBuilder() {
};

@BuilderParam builder: () => void = this.doNothingBuilder;
@State startSize: number = 100;
private result: SizeResult = { // 组件的尺寸信息,也是 onMeasureSize 的返回值
width: 0,
height: 0
};

// 第一步:计算各子组件的大小(这一步实现了组件大小依次递增的效果)
onMeasureSize(_selfLayoutInfo: GeometryInfo, children: Array<Measurable>, _constraint: ConstraintSizeOptions):MeasureResult {
/*
* 函数功能
* ArkUI 框架会在自定义组件确定尺寸时,将该自定义组件的节点信息和尺寸范围通过 onMeasureSize 传递给该开发者。不允许在 onMeasureSize 函数中改变状态变量。
* 参数解释,
* - selfLayoutInfo 父组件布局信息。
* interface GeometryInfo { // 父组件布局信息,继承自SizeResult。这里 Length 具体为 number 类型时,单位是 vp。
* borderWidth: { top: Length, right: Length, bottom: Length, left: Length }, // 父组件边框宽度
* margin: { top: Length, right: Length, bottom: Length, left: Length }, // 父组件 margin 信息
* padding: { top: Length, right: Length, bottom: Length, left: Length }, // 父组件 padding 信息
* width: number, // 测量后的宽
* height: number // 测量后的高
* }
* - children 子组件布局信息。
* interface Measurable { // 子组件位置信息。
* measure(constraint: ConstraintSizeOptions) : MeasureResult // 调用此方法对子组件的尺寸范围进行限制。
* }
* interface ConstraintSizeOptions { // 设置约束尺寸,组件布局时,进行尺寸范围限制。
* minWidth: Length, maxWidth: Length, minHeight: Length, maxHeight
* }
* interface MeasureResult {width: number, height: number} // 测量后的布局信息
* - constraint 父组件 constraint 信息。
*
* 返回值:组件尺寸信息。
* interface SizeResult {width: number, height: number}
*/
let size = 100;
children.forEach((child) => {
let result: MeasureResult = child.measure({
minHeight: size,
minWidth: size,
maxWidth: size,
maxHeight: size
})
size += result.width / 2;
})
this.result.width = 100; // 设置布局宽度
this.result.height = 400; // 设置布局高度
return this.result;
}

// 第二步:放置各子组件的位置(这一步实现了组件共右下角的效果)
onPlaceChildren(_selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, _constraint: ConstraintSizeOptions):MeasureResult {
/*
* 函数功能
* ArkUI 框架会在自定义组件布局时,将该自定义组件的子节点自身的尺寸范围通过 onPlaceChildren 传递给该自定义组件。不允许在 onPlaceChildren 函数中改变状态变量。
* 参数解释
* - selfLayoutInfo 父组件布局信息。
* - children 父组件布局信息。
* interface Layoutable { // 子组件布局信息。
* measureResult: MeasureResult // 子组件测量后的尺寸信息,继承自 SizeResult。
* }
* - constraint 父组件 constraint 信息。
*/
let startPos = 300;
children.forEach((child) => {
let pos = startPos - child.measureResult.height;
child.layout({ x: pos, y: pos })
})
return this.result
}

build() {
this.builder()
}
}

属性访问限定符的使用限制

当属性访问限定符的使用违反以下限制时,ArkTS 会产生告警日志。

变量类型 初始化限制
private/@StorageLink/@StorageProp/@LocalStorageLink/@LocalStorageProp/@Consume 变量 被外部初始化 ❌
使用本地值进行初始化
@State/@Prop/@Provide/@BuilderParam/常规成员变量(不涉及更新的普通变量) 被外部初始化
使用本地值进行初始化
@Link/@ObjectLink/@Require 变量 被外部初始化
使用本地值进行初始化 ❌

由于 struct 没有继承能力,上述所有的这些变量使用 protected 修饰时,会有编译告警日志提示。

6.4 装饰器(UI 描述复用)

@Builder 装饰器 - 自定义构建函数

  1. 解释:@Builder 装饰器是 ArkUI 提供的一种轻量的 UI 元素复用机制,开发者可以将重复使用的 UI 元素抽象成一个被 @Builder 所装饰的方法,以在 build 方法里调用。@Builder 装饰的函数也称为“自定义构建函数”。

  2. 语法 - 定义及使用

    • 私有自定义构建函数:在组件内部定义的被 @Builder 修饰的函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      @Entry
      @Component
      struct BuilderDemo {
      @State message: string = "Hello World";
      // 私有自定义函数(无参数) - 定义
      @Builder
      showTextBuilder() {
      Text(this.message)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
      }
      // 私有自定义函数(有参数) - 定义
      @Builder
      showTextValueBuilder(param: string) {
      Text(param)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
      }
      build() {
      Column() {
      // 私有自定义函数(无参数) - 使用
      this.showTextBuilder()
      // 私有自定义函数(有参数) - 使用
      this.showTextValueBuilder('Hello @Builder')
      }
      }
      }
      • 允许在自定义组件内定义一个或多个自定义构建函数,该方法被认为是该组件的私有、特殊类型的成员函数
      • 私有自定义构建函数允许在自定义组件内build 方法其他自定义构建函数中调用。
      • 私有自定义构建函数的函数体中,this 指代当前所属组件,建议通过 this 访问自定义组件的状态变量而不是参数传递。
    • 全局自定义构建函数:在全局定义的被 @Builder 修饰的函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      // 全局自定义函数(无参数) - 定义
      @Builder
      function showTextBuilder() {
      Text('Hello World')
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
      }
      // 全局自定义函数(有参数) - 定义
      @Builder
      function showTextValueBuilder(param: string) {
      Text(param)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
      }

      @Entry
      @Component
      struct BuilderDemo {
      build() {
      Column() {
      // 全局自定义函数(无参数) - 使用
      showTextBuilder()
      // 全局自定义函数(有参数) - 使用
      showTextValueBuilder('Hello @Builder')
      }
      }
      }
      • 如果不涉及组件状态变化,建议使用全局的自定义构建方法。
      • 全局自定义构建函数允许在 build 方法其他自定义构建函数中调用。
  3. 语法 - 参数传递

    • 按值传参

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @Builder
      function overBuilder(paramA1: string, param2: boolean) {
      Row() {
      Text(`UseStateVarByValue: ${paramA1}, ${param2}`)
      }
      }

      @Entry
      @Component
      struct Parent {
      @State label: string = 'Hello';

      build() {
      Column() {
      overBuilder(this.label, true)
      }
      }
      }
    • 按引用传参

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      interface  Tmp {
      paramA1: string;
      }

      @Builder
      function overBuilder(params: Tmp) {
      Row() {
      Text(`UseStateVarByReference: ${params.paramA1} `)
      }
      }

      @Entry
      @Component
      struct Parent {
      @State label: string = 'Hello';

      build() {
      Column() {
      // 在父组件中调用 overBuilder 组件时,
      // 把 this.label 通过引用传递的方式传给 overBuilder 组件。
      overBuilder({ paramA1: this.label })
      Button('Click me').onClick(() => {
      // 单击 Click me 后,UI 文本从 Hello 更改为 ArkUI。
      this.label = 'ArkUI';
      })
      }
      }
      }
    • 使用说明

      • 参数的类型必须与参数声明的类型一致,不允许 undefinednull 和返回 undefinednull 的表达式。
      • @Builder 修饰的函数内部不允许改变参数值,否则会框架会抛出运行时错误。
      • @Builder 修饰的函数内部的 UI 语法遵循 UI 语法规则
      • 只有传入一个参数,且参数需要直接传入对象字面量才会按引用传递该参数,其余传递方式均为按值传递。‼️
      • 按引用传递参数时,传递的参数可为状态变量,此时状态变量的改变会引起 @Builder 修饰的函数内的 UI 刷新。
      • 按引用传递参数时,ArkUI 提供 $$ 作为按引用传递参数的范式

@LocalBuilder 装饰器 - 维持组件父子关系

  1. 解释:@LocalBuilder 装饰器在私有 @Builder 的基础上,解决了组件的父子关系和状态管理的父子关系保持一致的问题。在父组件中定义的函数,如果被 @Builder 装饰并传递给子组件调用,那么函数体中的 this 会指向子组件。然而,如果函数被 @LocalBuilder 装饰,那么函数体中的 this 始终指向父组件,此时组件的父子关系和状态管理的父子关系始终保持一致。

  2. 语法 - 定义及使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Entry
    @Component
    struct Parent {
    @State label: string = 'Hello';

    @LocalBuilder
    MyBuilderFunction() {
    // ...
    } // @LocalBuilder 函数的定义

    build() {
    Column() {
    this.MyBuilderFunction(); // @LocalBuilder 函数的使用
    }
    }
    }
    • @LocalBuilder 函数的定义及使用注意同[私有自定义构建函数](#@Builder 装饰器 - 自定义构建函数)。
    • @LocalBuilder 函数只能在所属组件内声明,不允许全局声明。
    • @LocalBuilder 装饰器不能与其他内置装饰器或自定义装饰器一起使用
    • @LocalBuilder 不能装饰自定义组件内的静态方法
  3. 语法 - 参数传递:分为按值传递按引用传递@LocalBuilder 函数的参数传递语法和注意同[自定义构建函数](#@Builder 装饰器 - 自定义构建函数)。

    • 若子组件调用父组件的 @LocalBuilder 函数,传入的参数发生变化,不会引起 @LocalBuilder 方法内的 UI 刷新。‼️【文档描述不清】
  4. @LocalBuilder@Builder 区别说明:若子组件调用父组件的 @LocalBuilder 函数,传入的参数发生变化,不会引起 @LocalBuilder 方法内的 UI 刷新。原因为 @Localbuilder 装饰的函数绑定在父组件上,状态变量刷新机制是刷新本组件以及其子组件,对父组件无影响,故无法引发刷新。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    class LayoutSize {
    size: number = 0;
    }

    @Entry
    @Component
    struct Parent {
    label: string = 'parent';
    @State layoutSize: LayoutSize = { size: 0 };

    /*
    * 函数被 @LocalBuilder 修饰,函数体 this 指向父组件(或者说定义的组件),
    * 函数被 @Builder 修饰,函数体 this 指向子组件(或者说调用的组件)
    */
    @LocalBuilder
    // @Builder
    componentBuilder($$: LayoutSize) {
    Text(`${'this :' + this.label}`);
    Text(`${'size :' + $$.size}`);
    }

    build() {
    Column() {
    Child({ contentBuilder: this.componentBuilder });
    }
    }
    }

    @Component
    struct Child {
    label: string = 'child';
    @BuilderParam @Require contentBuilder: ((layoutSize: LayoutSize) => void); // 父组件传递的 @LocalBuilder / @Builder 函数
    @State layoutSize: LayoutSize = { size: 0 };

    build() {
    Column() {
    /*
    * 父组件传递的 @LocalBuilder 函数,contentBuilder 函数体里边的 this 指向父组件(或者说定义的组件),
    * 父组件传递的 @Builder 函数,contentBuilder 函数体里边的 this 指向子组件(或者说调用的组件)
    */
    this.contentBuilder({ size: this.layoutSize.size });
    /* 用户点击更改子组件的状态变量 ==> 只有父组件传递 @Builder 函数,UI 页面才会被触发渲染,否则不变 */
    Button("add child size").onClick(() => {
    this.layoutSize.size += 1;
    })
    }
    }
    }

@BuilderParam 装饰器 - 引用 @Builder 函数

  1. 解释:@BuilderParam 装饰器用于装饰指向 @Builder 函数的变量。

    这里的 @Builder 函数也包括 @LocalBuilder 函数。

  2. 语法 - 变量初始化:被 @BuilderParam 所装饰的变量只允许通过 @Builder 函数进行初始化。

    • 本地初始化:私有或全局自定义构建函数进行初始化。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Builder function overBuilder() {}

      @Component
      struct Child {
      @Builder doNothingBuilder() {};
      // 私有自定义构建函数初始化 @BuilderParam 变量
      @BuilderParam customBuilderParam: () => void = this.doNothingBuilder;
      // 全局自定义构建函数初始化 @BuilderParam 变量
      @BuilderParam customOverBuilderParam: () => void = overBuilder;
      build(){}
      }
    • 参数传递初始化:父组件的自定义构建函数进行初始化。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      @Component
      struct Child {
      @Builder customBuilder() {};
      @BuilderParam customBuilderParam: () => void = this.customBuilder;

      build() {
      Column() {
      this.customBuilderParam()
      }
      }
      }

      @Entry
      @Component
      struct Parent {
      @Builder componentBuilder() {
      Text(`Parent builder `)
      }

      build() {
      Column() {
      Child({ customBuilderParam: this.componentBuilder }) // 父组件自定义构建函数初始化子组件 @BuilderParam 变量(此时 customBuilderParam 的 this 会指向子组件)
      }
      }
      }
      • 箭头函数:还是上边的例子,对于父组件的私有构建函数,可以使用 Child({ customBuilderParam: (): void => { this.componentBuilder() } }) 初始化子组件 @BuilderParam 变量。这种借用箭头函数初始化的好处在于,即使 @Builder 函数传递给子组件,函数体中的 this 还是指向父组件,而不是子组件。

      • 尾随闭包:还是上边的例子,父组件可以使用以下语法初始化子组件 @BuilderParam 变量。所谓尾随闭包初始化,即通过组件后紧跟的大括号 {} 形成尾随闭包场景,作为参数传递给子组件 @BuilderParam 变量。注意,此时自定义组件必须有且仅有一个@BuilderParam 装饰的属性,同时自定义组件不支持使用 width 等通用属性

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        build() {
        Column() {
        // Child() 后紧跟的 {} 形成尾随闭包,作为参数传递给 @BuilderParam customBuilderParam: () => void
        Child(){
        Column(){
        Text(`Parent builder `); // 这里的 this 始终指向父组件
        }
        .backgroundColor(Color.Yellow)
        .onClick(()=>{
        this.text = 'changeHeader';
        })
        }
        }
        }

    @BuilderParam 所装饰的变量的类型可以声明为 () => void

  3. @BuilderParam 所修饰变量的 this 指向(参数传递初始化时)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    @Component
    struct Child {
    label: string = `Child`;
    @Builder customBuilder() {};
    @Builder customChangeThisBuilder() {};
    @BuilderParam customBuilderParam: () => void = this.customBuilder;
    @BuilderParam customChangeThisBuilderParam: () => void = this.customChangeThisBuilder;

    build() {
    Column() {
    this.customBuilderParam()
    this.customChangeThisBuilderParam()
    }
    }
    }

    @Entry
    @Component
    struct Parent {
    label: string = `Parent`;

    @Builder componentBuilder() {
    Text(`${this.label}`)
    }

    @LocalBuilder componentBuilder2() {
    Text(`${this.label}`)
    }

    build() {
    Column() {
    this.componentBuilder() // this 指向当前 @Entry 所装饰的 Parent 组件,即 label 变量的值为 "Parent"。
    this.componentBuilder2() // this 指向当前 @Entry 所装饰的 Parent 组件,即 label 变量的值为 "Parent"。
    Child({
    customBuilderParam: this.componentBuilder, // this 指向的是子组件 Child,即 label 变量的值为 "Child"。
    customChangeThisBuilderParam: (): void => { this.componentBuilder() } // 使用箭头函数,其 this 指向宿主对象,即 label 变量的值为 "Parent"。
    })
    Child({
    customBuilderParam: this.componentBuilder2, // this 指向的是当前组件 Parent,即 label 变量的值为 "Parent"。
    customChangeThisBuilderParam: (): void => { this.componentBuilder2() } // this 指向的是当前组件 Parent,即 label 变量的值为 "Parent"。
    })

    }
    }
    }
    • 父组件直接传递 @Builder 修饰的函数,this 指向子组件
    • 父组件以箭头函数传递 @Builder 修饰的函数,this 指向父组件
    • 父组件传递 @LocalBuilder 修饰的函数,this 指向父组件

wrapBuilder - 封装全局 @Builder

  1. 解释:wrapBuilder 是一个全局函数,用于封装全局 @Builder。它接受一个 @Builder 函数作为参数,并返回一个 WrappedBuilder 类型的对象。通过该对象的 build 方法,可以实现 UI 描述的复用。

    1
    2
    3
    4
    5
    6
    7
    /* 下述的 Args extends Object[] 是需要包装的 @builder 函数的参数列表 */
    declare function wrapBuilder< Args extends Object[]>(builder: (...args: Args) => void): WrappedBuilder;

    declare class WrappedBuilder< Args extends Object[]> {
    builder: (...args: Args) => void;
    constructor(builder: (...args: Args) => void);
    }
  2. 语法

    1
    2
    3
    let builderVar: WrappedBuilder<[string, number]> = wrapBuilder(MyBuilder); // builderVar.build('hello', 123) 的方式复用 UI 描述

    let builderArr: WrappedBuilder<[string, number]>[] = [wrapBuilder(MyBuilder)]; // builderArr[0].build('hello', 123) 的方式复用 UI 描述
  3. 注意事项

    • wrapBuilder 方法只能以全局 @Builder 函数作为参数。
    • WrappedBuilder 对象的 builder() 属性方法只能在 struct 内部使用
    • 无法重复定义 WrappedBuilder 变量,意思是一旦将一个变量或对象的属性赋值为 WrappedBuilder 对象,后续的再赋值则不会起作用,只生效第一次定义的 WrappedBuilder

6.5 装饰器(样式)

@Style - 定义组件重用样式

  1. 解释:@Style 装饰器所修饰的方法包含多条样式,可以在组件声明的位置直接调用。

    • @Styles 方法不能有参数
    • @Styles 方法内不支持使用逻辑组件,如 if。逻辑组件内的属性不生效。
    • @Styles 只能在当前文件内使用,不支持 export。 推荐使用 AttributeModifier 实现 export 功能。
  2. 语法 - 定义

    • 组件 @Style

      1
      2
      3
      4
      5
      6
      @Component
      struct FancyUse {
      @Styles functionName() {
      /* ... */
      }
      }
      • 组件内的 @Styles 可以通过 this 访问组件的常量和状态变量
      • 组件内的 @Styles 的优先级高于全局 @Styles
    • 全局 @Style

      1
      @Styles function functionName() { /* ... */ }
  3. 使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 定义在全局的 @Styles 封装的样式
    @Styles function globalFancy () {
    .width(150)
    .height(100)
    .backgroundColor(Color.Pink)
    }

    @Entry
    @Component
    struct FancyUse {
    @State heightValue: number = 100;
    // 定义在组件内的 @Styles 封装的样式
    @Styles fancy() {
    .width(200)
    .height(this.heightValue)
    .backgroundColor(Color.Yellow)
    .onClick(() => {
    this.heightValue = 200
    })
    }

    build() {
    Column({ space: 10 }) {
    Text('FancyA')
    .globalFancy() // 使用全局的 @Styles 封装的样式
    .fontSize(30)
    Text('FancyB')
    .fancy() // 使用组件内的 @Styles 封装的样式
    .fontSize(30)
    }
    }
    }

@Extend - 定义扩展组件样式

  1. 解释:@Extend 装饰器所修饰的方法也包含多条样式,可以在组件声明的位置直接调用。@Styles 装饰器是为了实现样式的重用,而 Extend 装饰器是用于扩展原生组件的样式

  2. 语法

    1
    @Extend(UIComponentName) function functionName { ... }
    • @Extend 函数支持封装指定组件的私有属性私有事件自身定义的全局方法
    • @Extend 装饰的函数支持参数,参数可以为 function,作为 Event 事件的句柄;参数也可以为状态变量,状态变量改变会触发 UI 的渲染。
    • @Extend 仅支持在全局定义,不支持在组件内部定义。
    • @Extend 只能在当前文件内使用,不支持 export。 推荐使用 AttributeModifier 实现 export 功能。
  3. 使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // Text 组件扩展的样式
    @Extend(Text)
    function fancyText(weightValue: number, color: Color) {
    .fontStyle(FontStyle.Italic)
    .fontWeight(weightValue)
    .backgroundColor(color)
    }

    @Entry
    @Component
    struct FancyUse {
    @State label: string = 'Hello World'

    build() {
    Row({ space: 10 }) {
    Text(`${this.label}`)
    .fancyText(100, Color.Blue) // 使用 Text 组件扩展的样式
    Text(`${this.label}`)
    .fancyText(200, Color.Pink)
    Text(`${this.label}`)
    .fancyText(300, Color.Orange)
    }.margin('20%')
    }
    }

stateStyles - 多态样式

  1. 解释:stateStyles 是组件的属性方法,可以依据组件的内部状态的不同,快速设置不同样式,即支持多态样式

  2. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    .stateStyles({
    focused: { // 获焦态(仅支持通过外接键盘的 tab 键、方向键触发)
    /* ... */
    },
    normal: { // 正常态
    /* ... */
    },
    pressed: { // 按压态
    /* ... */
    },
    disabled: { // 不可用态
    /* ... */
    },
    selected: { // 选中态
    /* ... */
    }
    })
    • focusednormalpresseddisabledselected 字段的取值不仅可以是样式,也可以是 @Styles 函数、组件内的常规变量和状态变量
  3. 使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Entry
    @Component
    struct CompWithInlineStateStyles {
    @State focusedColor: Color = Color.Red;
    normalColor: Color = Color.Green

    build() {
    Column() {
    Button('clickMe').height(100).width(100)
    .stateStyles({
    normal: {
    .backgroundColor(this.normalColor)
    },
    focused: {
    .backgroundColor(this.focusedColor)
    }
    })
    .onClick(() => {
    this.focusedColor = Color.Pink
    })
    .margin('30%')
    }
    }
    }

AttributeModifier - 动态、跨文件样式

  1. 解释:AttributeModifier 是 ArkUI 引入的新的样式机制,支持通过 Modifier 对象动态修改属性,与 @Styles@Extend 相比,AttributeModifier 支持跨文件导出多态样式业务逻辑等。

  2. 语法

    • 自定义指定组件的 AttributeModifier

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      declare interface AttributeModifier<T> {

      applyNormalAttribute?(instance: T): void; // 默认态

      applyPressedAttribute?(instance: T): void; // 按压态

      applyFocusedAttribute?(instance: T): void; // 焦点态

      applyDisabledAttribute?(instance: T): void; // 禁用态

      applySelectedAttribute?(instance: T): void; // 选择态

      }

      T组件的属性类型,开发者可以在回调中获取到属性对象,通过该对象设置属性。

    • 在指定组件上应用自定义的 AttributeModifier

      1
      2
      3
      declare class CommonMethod<T> {
      attributeModifier(modifier: AttributeModifier<T>): T;
      }
  3. 使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // button_modifier.ets
    export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
    applyNormalAttribute(instance: ButtonAttribute): void {
    // instance为Button的属性对象,设置正常状态下属性值
    instance.backgroundColor('#17A98D')
    .borderColor('#707070')
    .borderWidth(2)
    }

    applyPressedAttribute(instance: ButtonAttribute): void {
    // instance为Button的属性对象,设置按压状态下属性值
    instance.backgroundColor('#2787D9')
    .borderColor('#FFC000')
    .borderWidth(5)
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // demo.ets
    import { MyButtonModifier } from './button_modifier'

    @Entry
    @Component
    struct attributeDemo {
    @State modifier: MyButtonModifier = new MyButtonModifier();

    build() {
    Row() {
    Column() {
    Button("Button")
    .attributeModifier(this.modifier)
    }
    .width('100%')
    }
    .height('100%')
    }
    }

6.6 装饰器(其他)

@AnimatableExtend - 定义可动画属性

  1. 解释:@AnimatableExtend 装饰器所修饰的函数可以变为组件的属性方法。通过这个属性方法,我们可以将组件的不可动画属性转变为可动画属性。在动画执行期间,利用逐帧回调函数,在每一帧中计算并调整不可动画属性的值,使这些属性也能够实现动画效果。此外,还可以在该属性方法中修改组件的可动画属性,从而实现逐帧的布局效果。

    • 可动画属性:如果一个属性方法在 animation 属性方法前调用,改变这个属性的值可以使 animation 属性的动画效果生效,这个属性称为可动画属性。比如 heightwidth 等。
    • 不可动画属性:如果一个属性方法在 animation 属性方法前调用,改变这个属性的值不能使 animation 属性的动画效果生效,这个属性称为不可动画属性。比如 Polyline 组件的 points 属性等。
  2. 语法

    1
    2
    3
    @AnimatableExtend(UIComponentName) function functionName(value: typeName) { 
    .propertyName(value)
    }
    • @AnimatableExtend 函数仅支持定义在全局,不支持在组件内部定义。
    • @AnimatableExtend 函数的参数类型必须 为 number 类型或者实现 AnimatableArithmetic<T> 接口的自定义类型。为了让不可动画属性变得可动画,则需要让对应的数据结构(如数组、结构体、颜色等)实现 AnimatableArithmetic<T> 接口后,再定义 @AnimatableExtend 函数。
    • @AnimatableExtend 函数体内只能调用 @AnimatableExtend 括号内组件 UIComponentName 的属性方法
  3. 使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @AnimatableExtend(Text)
    function animatableWidth(width: number) {
    .width(width)
    }

    @Entry
    @Component
    struct AnimatablePropertyExample {
    @State textWidth: number = 80;

    build() {
    Column() {
    Text("AnimatableProperty")
    .animatableWidth(this.textWidth)
    .animation({ duration: 2000, curve: Curve.Ease })
    Button("Play")
    .onClick(() => {
    this.textWidth = this.textWidth == 80 ? 160 : 80;
    })
    }.width("100%")
    .padding(10)
    }
    }

@Require - 校验构造传参

  1. 解释:@Require 装饰器装饰 @Prop@State@Provide@BuilderParam 和普通变量(无状态装饰器修饰的变量),校验这些变量是否需要构造传参

  2. 使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    @Entry
    @Component
    struct Index {
    @State message: string = 'Hello World';

    @Builder
    buildTest() {
    Row() {
    Text('Hello, world')
    .fontSize(30)
    }
    }

    build() {
    Row() {
    Child({
    regular_value: this.message,
    state_value: this.message,
    provide_value: this.message,
    initMessage: this.message,
    message: this.message,
    buildTest: this.buildTest,
    initBuildTest: this.buildTest
    })
    }
    }
    }

    @Component
    struct Child {
    @Builder
    buildFunction() {
    Column() {
    Text('initBuilderParam')
    .fontSize(30)
    }
    }

    @Require regular_value: string = 'Hello';
    @Require @State state_value: string = "Hello";
    @Require @Provide provide_value: string = "Hello";
    @Require @BuilderParam buildTest: () => void;
    @Require @BuilderParam initBuildTest: () => void = this.buildFunction;
    @Require @Prop initMessage: string = 'Hello';
    @Require @Prop message: string;

    build() {
    Column() {
    Text(this.initMessage)
    .fontSize(30)
    Text(this.message)
    .fontSize(30)
    this.initBuildTest();
    this.buildTest();
    }
    .width('100%')
    .height('100%')
    }
    }

@Resuable - 组件复用

  1. 解释:@Builder 装饰器所装饰的自定义组件被标记为可复用组件,常与 @Component 装饰器结合使用。当可复用组件从组件树上被移除时,组件和其对应的 JSView 对象都会被放入复用缓存中,后续创建新自定义组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。

    • @Reusable 装饰器仅用于自定义组件
    • @Resuable 组件中不支持使用 ComponentContent
    • @Reusable 组件不支持嵌套使用,即在一个可复用组件中使用另一个可复用组件。
    • @Reusable 组件用于渲染的变量需要被 @State 所修饰,否则组件复用会存在 UI 无法更新的问题。
    • @Reusable 组件使用时,可以通过属性方法 .reuseId(id: str) 的方式为复用组件分配复用组,此时相同 reuseId 的组件会在同一个复用组中复用。
  2. 生命周期函数

    Hook 时机 类型
    aboutToReuse 可复用组件:复用缓存 -> 节点树 aboutToReuse(params: Record<string, ESObject>): void
    aboutToRecycle 可复用组件:节点树 -> 复用缓存 aboutToRecycle(): void

    @Reusable 组件复用时,需要在 aboutToReuse Hook 中更新 @State 变量,其中 params 为组件的构造参数。

  3. 组件复用场景

    • 标准型:复用组件之间布局完全相同。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 复用组件
      @Reusable
      @Component
      export struct CardView {
      @State item: string = '';

      build() {
      Column() {
      Text(this.item)
      .fontSize(30)
      }
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // 列表渲染复用组件
      @Entry
      @Component
      struct ReuseDemo {
      private data: MyDataSource = new MyDataSource();

      build() {
      Column() {
      List() {
      LazyForEach(this.data, (item: string) => {
      ListItem() {
      CardView({ item: item })
      }
      }, (item: string) => item)
      }
      }
      }
      }
    • 有限变化型:复用组件之间存在不同,但类型有限。此时需要为复用组件设置多个不同的 reuseId,实现分组复用。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // 复用组件
      @Reusable
      @Component
      struct ReusableComponent {
      @State item: number = 0;

      build() {
      Column() {
      if (this.item % 2 === 0) {
      Text(`Item ${this.item} ReusableComponentOne`)
      } else {
      Text(`Item ${this.item} ReusableComponentTwo`)
      }
      }
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // 列表渲染复用组件
      @Entry
      @Component
      struct Index {
      private data: MyDataSource = new MyDataSource();

      build() {
      Column() {
      List() {
      LazyForEach(this.data, (item: number) => {
      ListItem() {
      ReusableComponent({ item: item })
      .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
      }
      }, (item: number) => item.toString())
      }
      }
      }
      }
    • 组合型:复用组件之间存在不同,但拥有共同的子组件。此时需要将复用组件抽离为 @Builder 函数,其共同的子组件设置为可复用组件。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 复用组件
      @Reusable
      @Component
      struct ChildComponentA { // 类似定义 ChildComponentB、ChildComponentC、ChildComponentD
      @State item: string = '';

      build() {
      Row() {
      Text(`Item ${this.item} Child Component D`)
      }
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      // 列表渲染复用组件
      @Entry
      @Component
      struct MyComponent {
      private data: MyDataSource = new MyDataSource();

      @Builder
      itemBuilderOne(item: string) {
      Column() {
      ChildComponentA({ item: item })
      ChildComponentB({ item: item })
      ChildComponentC({ item: item })
      }
      }

      @Builder
      itemBuilderTwo(item: string) {
      Column() {
      ChildComponentA({ item: item })
      ChildComponentC({ item: item })
      ChildComponentD({ item: item })
      }
      }

      @Builder
      itemBuilderThree(item: string) {
      Column() {
      ChildComponentA({ item: item })
      ChildComponentB({ item: item })
      ChildComponentD({ item: item })
      }
      }

      build() {
      List({ space: 40 }) {
      LazyForEach(this.data, (item: string, index: number) => {
      ListItem() {
      if (index % 3 === 0) {
      this.itemBuilderOne(item)
      } else if (index % 5 === 0) {
      this.itemBuilderTwo(item)
      } else {
      this.itemBuilderThree(item)
      }
      }
      }, (item: number) => item.toString())
      }
      }
      }

7. 状态管理

7.1 状态管理概述

  1. 状态管理:在自定义组件中,变量需要通过装饰器进行装饰,才能成为状态变量。状态变量的变化会触发 UI 的重新渲染和更新。如果不使用状态变量,UI 仅会在初始化时进行渲染,而在后续的操作中将不会自动刷新。

    状态管理的功能仅支持在 UI 主线程使用,不能在子线程、worker、taskpool 中使用。

  2. 基本概念

    • 状态变量:被状态装饰器装饰的变量,状态变量值的改变引起 UI 的渲染更新
    • 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算,常规变量值的改变永远不会引起 UI 的刷新
    • 数据源/同步源状态变量原始来源,可以同步给不同的状态数据,通常指父组件传给子组件的数据。
    • 命名参数机制:父组件将指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。如 CompA({ aProp: this.aProp })
    • 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖
    • 初始化子组件父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量
    • 本地初始化:在变量声明的时候赋值,作为变量的默认值
  3. 状态管理 V1

    • 装饰器分类 - 状态变量的影响范围层面

      • 管理组件内状态的装饰器同一个组件树上(即同一个页面内)组件内或不同组件层级的状态。

        装饰器 装饰对象 作用
        @State 变量 变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源
        变量数值改变时,会引起相关组件的渲染刷新
        @Prop 变量 变量可以和父组件建立单向同步关系
        变量是可变的,但修改不会同步回父组件
        复杂类型是数据源的深拷贝
        @Link 变量 变量可以和父组件建立双向同步关系
        变量的修改会同步给父组件中建立双向数据绑定的数据源,
        父组件的更新也会同步给子组件中的该变量。
        @Provide/@Consume 变量 变量用于跨组件层级(多层组件)同步状态变量。
        变量通过 alias(别名)或者属性名绑定。
        @Observed class class 为需要观察多层嵌套场景class。仅可观察第一层属性
        单独使用 @Observed 没有任何作用,需要和 @ObjectLink@Prop 联用。
        @ObjectLink 变量 变量接收 @Observed 装饰的 class 的实例,用于观察多层嵌套场景,和父组件的数据源构建双向同步
      • 管理应用级状态的装饰器:不同页面,甚至不同 UIAbility 的状态。

        装饰器 作用
        @StorageLink/@LocalStorageLink 实现应用和组件状态的双向同步
        @StorageProp/@LocalStorageProp 实现应用和组件状态的单向同步
        • AppStorageLocalStorage 的一个特殊的单例,作为应用级的数据库,与进程绑定。可以通过 @StorageProp@StorageLink 装饰器将其与组件进行联动。
        • LocalStorage 是用于存储应用程序声明的应用状态的内存“数据库”,通常用于页面级的状态共享。通过 @LocalStorageProp@LocalStorageLink 装饰器,可以将其与 UI 进行联动。
    • 装饰器分类 - 数据的传递形式和同步类型层面

      • 只读的单向传递
      • 可变更的双向传递
    • 其他功能

      • @Watch:用于监听所装饰的状态变量的变化。
      • $$ 运算符:给内置组件提供 TS 变量的引用,使得 TS 变量和内置组件的内部状态保持同步
  4. 状态管理 V2(API 12 起支持)

    • 装饰器语法

      装饰器 装饰对象 作用
      @ObservedV2 class 被装饰的 class 具有深度监听的能力
      @ObservedV2@Trace 配合使用可以使 class 中的属性具有深度观测的能力
      @Trace 变量 装饰被 @ObservedV2 装饰的 class 中的属性,被装饰的属性具有深度观测的能力
      @ComponentV2 struct 被装饰的 struct 中能使用新的装饰器
      @Local@Param@Event@Once@Monitor@Provider@Consumer
      @Local 变量 被装饰的变量为组件内部状态无法从外部初始化
      @Param 变量 被装饰的变量作为组件的输入,可以接受从外部传入初始化并同步
      复杂类型是数据源的引用
      @Once 变量 被装饰的变量仅初始化时同步一次,需要@Param 一起使用
      @Event 方法 被装饰的方法作为组件输出,可以通过该方法影响父组件中变量
      @Monitor 方法 装饰被 @ComponentV2 装饰的自定义组件或 @ObservedV2 装饰的类中的方法,能够对状态变量进行深度监听
      @Provider/@Consumer 变量 被装饰的变量跨组件层级双向同步
      @Computed getter 方法 被装饰的 getter 方法表示计算属性,在被计算的值变化的时候,只会计算一次

      状态管理 V2 需要通过 @Param@Event 实现双向同步@Monitor@Trace 实现深层监听状态变量,状态变量在一次事件中多次变化时,仅会以最终的结果判断是否触发 @Monitor 监听事件。

    • 其他功能

      • !! 运算符:双向绑定语法糖。
  5. 状态管理 V1 Vs. 状态管理 V2

    状态管理 V1 状态管理 V2
    使用代理观察数据 数据本身就是可观察的
    状态变量依赖 UI
    一个视图的更改不会通知其他视图更新。
    状态变量独立于 UI
    更改数据会触发视图更新。
    无法深度观测和监听
    只能感知对象第一层属性变化。
    支持深度观测和监听
    且不影响性能。
    存在冗余更新的问题。 支持精准更新最小化更新
    装饰器使用限制多,不易用 装饰器易用性高、拓展性强。
    组件中状态变量的输入输出不明确
    不利于组件化。
    组件中状态变量的输入输出明确
    有利于组件化。

7.2 状态管理 V1

组件状态管理

@State - 组件内状态
  1. 解释:@State 所装饰的变量拥有了状态属性,称之为状态变量@State 变量改变时,会触发其直接绑定的 UI 组件的刷新。@State 变量也是大部分状态变量的数据源

    • 单向数据同步@State 变量与子组件中的 @Prop 变量之间。
    • 双向数据同步@State 变量与子组件中的 @Link@ObjectLink 变量之间。
  2. 语法

    1
    @State 变量名: 变量类型 = 初始值;
    • 允许装饰的变量类型,

      • Objectclassstringnumberbooleanenum 及其数组。
      • Date
      • MapSet、联合类型。(API11+)
      • undefinednull
      • LengthResourceStrResourceColor。(ArkUI 提供)
    • 初始化规则,

      • 本地初始化:必须

      • 从父组件初始化:可选。父组件传入的非 undefined 值,会覆盖本地初始化的值;否则以本地初始化的值为准。

      • 允许的父组件的数据源类型、子组件被初始化的参数类型,

        img

    • 访问控制:不支持组件外访问

    • 数据同步:不与父组件中任何类型的变量同步

  3. UI 更新原理 - 按需更新

    • 观察到状态变量的修改
    • 查询依赖该状态变量的组件,并执行组件的更新方法,组件更新渲染
  4. 可以被观察到的状态变化(假设 value 为对应类型的 @State 变量)

    • 类型为 stringnumberboolean 的状态变量:直接修改

      1
      this.value = 123; // 支持
    • 类型为 Objectclass 的状态变量:直接修改,属性赋值(仅支持第一层属性的修改)。

      1
      2
      3
      this.value = new Person(); // 支持
      this.value.content = "hello"; // 支持
      this.value.info.msg = "world"; // 不支持 ‼️

      对于嵌套监测场景,可以使用 @Observed 装饰 class,此时 class 属性的变化都可以被框架观察到,但是注意,组件还是最多只支持状态变量第一层属性的修改引发的 UI 渲染。

    • 类型为 Array 的状态变量:直接修改,添加、删除、更新数组元素。不支持元素的属性赋值。

      1
      2
      3
      4
      5
      this.value = [new Person()]; // 支持
      this.value[0] = new Person(); // 支持
      this.value.pop(); // 支持
      this.value.push(new Person()); // 支持
      this.value[0].content = "hello"; // 不支持 ‼️
    • 类型为 DateMapSet 的状态变量:直接修改,Date / Map / Set 接口的调用。

  5. 使用说明

    • 构造函数中状态变量的变更无效‼️:在 class 类型的状态变量中,如果在构造函数中将修改状态变量的代码封装为箭头函数,并将其存储在对象中,随后通过实例方法调用该函数时,状态变量的变更将不会被框架检测到。这是因为:

      • 代理机制:在状态管理中,类被代理包装。当组件中的状态变量被修改时,代理会拦截该操作,更新数据源并通知相关组件,从而实现数据变更的观测和 UI 更新。
      • 构造函数中的 this 指向:在类的构造函数中,this 还未被代理封装,指向的是原始实例对象(数据源)。因此,如果将修改属性的操作封装在箭头函数中,由于箭头函数的 this 指向定义时的作用域,即原始实例对象,不是代理对象。
      • 后续调用的影响:当实例化结束后,即使调用该箭头函数进行属性修改,实际上修改的是数据源,而非代理对象。因此,框架无法检测到这种变化。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      @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%')
      }
      }

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

      constructor() {
      // 错误代码 - 开始
      this.model = new Model(() => {
      this.isSuccess = true; // constructor 中的 this 指向未经代理的数据源,此时 this 上的属性更新不会被框架所监测到
      console.log(`this.isSuccess: ${this.isSuccess}`);
      })
      // 错误代码 - 结束
      }

      query() {
      // 更正代码 - 开始
      this.model.callback = () => {
      this.isSuccess = true; // 定义在原型上的实例方法中的 this 指向被代理的数据源,此时 this 上的属性更新会被框架所监测到
      console.log(`this.isSuccess: ${this.isSuccess}`);
      }
      // 更正代码 - 结束
      this.model.query();
      }
      }

      export class Model {
      callback: () => void

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

      query() {
      this.callback();
      }
      }
    • 箭头函数修改状态变量无效‼️:如果 class 类型的状态变量的实例方法使用箭头函数定义,并在其中修改属性值,这些修改将不会被框架检测到。这是因为箭头函数的 this 指向定义时的作用域,即原始实例对象,而不是代理对象,类似《构造函数中状态变量的变更无效》。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      @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)
      }
      }

      class PlayDetailViewModel {
      coverUrl: string = '#00ff00'
      // 错误代码 - 开始
      changeCoverUrl = () => {
      this.coverUrl = '#00F5FF'; // 定义在实例上的实例方法中的 this 指向未经代理的数据源,此时 this 上的属性更新不会被框架所监测到
      }
      // 错误代码 - 结束

      // 更正代码1 - 开始
      changeCoverUrl1 = (model: PlayDetailViewModel) => {
      model.coverUrl = '#00F5FF' // 这里采用传进来的 PlayDetailViewModel 对象,肯定已经被代理了
      }
      // 更正代码1 - 结束

      // 更正代码2 - 开始
      changeCoverUrl2() {
      this.coverUrl = '#00F5FF'; // 定义在原型上的实例方法中的 this 指向被代理的数据源,此时 this 上的属性更新会被框架所监测到
      }
      // 更正代码2 - 结束
      }
    • 复杂类型常量重复赋值触发页面刷新‼️:在状态管理 V1 中,当类类型的状态变量被赋予相同的常量时,页面会触发刷新。这是因为状态管理机制为被 @Observed 装饰的类对象,以及使用状态变量装饰器(如 @State)装饰的复杂类型(Class、Date、Map、Set、Array)添加了一层代理,以监测属性变化或 API 调用。在下面的代码示例中,一个数组类型的状态变量的元素被赋值给 class 类型的状态变量。由于数组的元素是普通的 Object 类型,而 class 类型的状态变量是 Proxy 类型,因此即便赋值相同的常量,引用判断结果不相等。这导致类状态变量被认为发生了变化,从而触发页面刷新。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      class DataObj {
      name: string = 'default name';

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

      @Entry
      @Component
      struct Index {
      list: DataObj[] = [new DataObj('a'), new DataObj('b'), new DataObj('c')];
      @State dataObjFromList: DataObj = this.list[0];

      build() {
      Column() {
      ConsumerChild({ dataObj: this.dataObjFromList })
      Button('change to self')
      .onClick(() => {
      this.dataObjFromList =
      this.list[0]; // this.dataObjFromList 是 Proxy 类型,this.list[0] 是 Object 类型,二者类型不同,会触发状态变量值的更新,进而触发页面刷新
      })
      }
      }
      }

      @Component
      struct ConsumerChild {
      @Link @Watch('onDataObjChange') dataObj: DataObj;

      onDataObjChange() {
      console.log("dataObj changed");
      }

      build() {
      Column() {
      Text(this.dataObj.name).fontSize(30)
      }
      }
      }
      • 修正方式一:使用 @Observed 装饰 DataObject 类,使得该类的实例对象都是 Proxy 类型,数组的元素也是 Proxy 类型,此时相同的对象赋值不会再触发页面刷新。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        @Observed
        class DataObj {
        name: string = 'default name';

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

        // ...
      • 修正方式二:使用 @ohos.arkui.StateManagement 提供的 UIUtilsgetTarget() 方法,获取对应状态变量的原始对象,相同的对象赋值时,经原始对象对比后再确定是否继续赋值。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        import { UIUtils } from '@ohos.arkui.StateManagement';

        // ...

        @Entry
        @Component
        struct Index {
        // ...

        build() {
        Column() {
        ConsumerChild({ dataObj: this.dataObjFromList })
        Button('change to self').onClick(() => {
        // 获取原始对象来和新值做对比
        if (UIUtils.getTarget(this.dataObjFromList) !== this.list[0]) {
        this.dataObjFromList = this.list[0];
        }
        })
        }
        }
        }

        // ...
    • 类的静态函数和组件的实例方法中直接修改状态变量的属性无效‼️:如果 class 类型的状态变量在静态函数或组件的实例方法中修改其属性,这些修改不会被监测到,从而无法触发 UI 刷新。这是因为当 class 类型的状态变量作为参数传递给静态函数或组件的实例方法时,实际上传递的是原始实例对象,对其属性的修改无法被代理监测。一种解决方法是先将状态变量赋值给一个新的变量,这个新的变量会被自动加上 Proxy 代理,成为 Proxy 类型的对象。然后将这个 Proxy 对象作为参数传递给静态函数或组件的实例方法,这样对属性的修改就会被框架检测到,从而触发页面刷新。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      class Balloon {
      volume: number;
      constructor(volume: number) {
      this.volume = volume;
      }

      static increaseVolume(balloon:Balloon) {
      balloon.volume += 2;
      }
      }

      @Entry
      @Component
      struct Index {
      @State balloon: Balloon = new Balloon(10);

      reduceVolume(balloon:Balloon) {
      balloon.volume -= 1;
      }

      build() {
      Column({space:8}) {
      Text(`The volume of the balloon is ${this.balloon.volume} cubic centimeters.`)
      .fontSize(30)
      Button(`increaseVolume`)
      .onClick(()=>{
      // 错误代码 - 开始
      // 通过类的静态函数直接修改 class 类型的状态变量 => 无法触发 UI 刷新
      Balloon.increaseVolume(this.balloon);
      // 错误代码 - 结束

      // 更正代码 - 开始
      const balloon = this.balloon;
      Balloon.increaseVolume(balloon);
      // 更正代码 - 结束
      })
      Button(`reduceVolume`)
      .onClick(()=>{
      // 错误代码 - 开始
      // 通过组件的实例方法直接修改 class 类型的状态变量 => 无法触发 UI 刷新
      this.reduceVolume(this.balloon);
      // 错误代码 - 结束

      // 更正代码 - 开始
      const balloon = this.balloon;
      this.reduceVolume(balloon);
      // 更正代码 - 结束
      })
      }
      .width('100%')
      .height('100%')
      }
      }
    • 禁止在 build() 函数中修改状态变量,否则状态管理框架会在运行时报出 Error 级别日志。

    • 可以在 aboutToAppear 中注册用于修改组件状态变量的箭头函数,但是必须在 aboutToDisappear 中将注册的箭头函数清空,否则会因为箭头函数捕获了自定义组件的 this 实例,导致自定义组件无法被释放,从而造成内存泄漏。

@Prop - 父子单向同步
  1. 解释:@Prop 所装饰的变量和父组件建立了单向的同步关系(父组件 -> 子组件)

    • @Prop 变量允许在本地修改,但修改后的变化不会同步回父组件。
    • 父组件数据源更改时,@Prop 变量都会更新,并且会覆盖本地所有更改。
  2. 语法

    1
    @Prop 变量名: 变量类型 [= 初始值];
    • 允许装饰的变量类型:同 @State。但注意,@Prop 的类型必须和数据源的类型相同。

    • 初始化规则,

      • 本地初始化:可选

      • 从父组件初始化:如果本地有初始化,则可选,否则必须

      • 允许的父组件的数据源类型、子组件被初始化的参数类型,

        img

    • 访问控制:不支持组件外访问

    • 数据同步:单向同步,对父组件状态变量值的修改,将同步给子组件,@Prop 变量,子组件 @Prop 变量的修改不会同步到父组件的状态变量上。

    • 数据拷贝:深拷贝,在拷贝的过程中除了基本类型、MapSetDateArray 外,都会丢失类型。

    • 嵌套传递:在组件复用场景,建议 @Prop 深度嵌套数据不要超过 5 层。嵌套太多会导致深拷贝占用的空间过大和垃圾回收,引起性能问题,建议使用 @ObjectLink

  3. UI 更新原理

    • @Prop 变量更新,更新渲染仅停留在当前组件(子)
    • @Prop 数据源更新, @Prop 变量被父组件的数据源重置,进而触发更新渲染(父 -> 子)
  4. 可以被观察到的状态变化:同 @State。但注意,数据源的 @Link@Prop@State 变量对 @Prop 变量的同步机制是相同的,即单向同步机制。

  5. 使用说明

    • @Observed@Prop 的联合使用

      • @Observed 的作用:当一个类被 @Observed 修饰时,这个类创建的实例对象都会被包装为 Proxy 对象。这意味着,该实例对象的属性发生修改时,会被系统监视到。
      • @Prop 的作用: @Prop 修饰的变量可以接收来自父组件状态变量传递的、被 @Observed 修饰的类的实例对象。这个传递的对象可以是父组件的状态变量本身,也可以是状态变量的属性或元素,甚至可以是多层嵌套的属性 。例如,父组件状态变量为 s ,传递给子组件的参数可以是 s.a.b.c.d 这样深层嵌套的对象。
      • 联合使用效果:当使用 @Prop 接收被 @Observed 修饰的类的实例对象后,父组件中对这个传递过来的实例对象的属性进行修改, @Prop 能够监测到这些变化,并触发子组件的页面更新,从而保证子组件展示的内容与父组件状态保持一致。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      let nextId: number = 1;

      @Observed
      class BookInfo {
      public publishedAt: Date;
      public author: string;

      constructor(publishedAt: Date, author: string) {
      this.publishedAt = publishedAt;
      this.author = author;
      }
      }

      @Observed
      class Book {
      public id: number;
      public title: string;
      public pages: number;
      public readIt: boolean;
      public info: BookInfo;

      constructor(title: string, pages: number, info: BookInfo = new BookInfo(new Date('2020-02-02'), 'Jackson')) {
      this.id = nextId++;
      this.title = title;
      this.pages = pages;
      this.readIt = false;
      this.info = info;
      }
      }

      @Component
      struct BookCard {
      @Prop book: Book;

      build() {
      Column() {
      Text(`《${this.book.title}》: ${this.book.pages} pages, ${this.book.readIt ? 'have read' : 'haven\'t read'}`)
      BookInfoCard({ bookInfo: this.book.info, bookId: this.book.id })
      Button("alter readIt")
      .onClick(() => {
      this.book.readIt = !this.book.readIt;
      });
      }
      .margin({ top: 20, bottom: 20 })
      }
      }

      @Component
      struct BookInfoCard {
      @Prop bookInfo: BookInfo;
      @Prop bookId: number;

      build() {
      Column() {
      Text(`book Id: ${this.bookId}`)
      Text(`published At: ${this.bookInfo.publishedAt.toDateString()}, created By: ${this.bookInfo.author}`)
      }
      }
      }

      @Component
      struct AuthorCard {
      @Prop name: string = '';

      build() {
      Column() {
      Text(`author Card: ${this.name}`)
      }
      }
      }

      @Entry
      @Component
      struct Library {
      @State books: Book[] = [new Book("C#", 765), new Book("JS", 652), new Book("TS", 765)];
      @State basicAuthor: BookInfo = new BookInfo(new Date(), 'Potter');

      @Styles
      commonStyles(){
      .width(312)
      .height(40)
      .borderRadius(20)
      .margin(12)
      .padding({ left: 20 })
      }

      build() {
      Column() {
      ForEach(this.books, (book: Book) => {
      BookCard({ book: book })
      },
      (book: Book) => book.id.toString())
      BookInfoCard({ bookInfo: this.books[1].info, bookId: 2 })
      AuthorCard({ name: this.books[1].info.author });
      AuthorCard({ name: this.basicAuthor.author });
      Divider().color(Color.Red).height(20)
      Button('修改第一本书的 id')
      .onClick(() => {
      this.books[0].id = 123; // BookCard 更新渲染(@Observed 的原因)
      })
      Button('修改第二本书的的 author')
      .onClick(() => {
      // BookCard 的 BookInfoCard 不更新(非 @State 可监测范围,且不是 @Observed 修饰的类的实例);
      // BookInfoCard 更新(@Observed 的原因);
      // AuthorCard 没更新(非 @State 可监测范围,且不是 @Observed 修饰的类的实例)
      this.books[1].info.author = 'Tom';
      })
      Button('修改第三本书')
      .onClick(() => {
      this.books[2] = new Book('JavaScript', 90, new BookInfo(new Date(), 'Adren')); // Library 更新渲染(@State 可监测的原因)
      })
      Button('修改 basicAuthor 的 author')
      .onClick(()=>{
      this.basicAuthor.author = 'HAHAHAHA'; // AuthorCard 更新渲染(@State 可监测的原因)
      })
      }
      }
      }
    • 类的静态函数和组件的实例方法中直接修改状态变量的属性无效‼️:同 @State 的使用说明的同名问题,是没有数据代理导致的问题。

    • 应用进入后台时,@Prop 变量无法刷新,推荐使用 @Link 变量代替。

  1. 解释:@Link 所装饰的变量和父组件建立了双向数据绑定(父组件 <-> 子组件)

  2. 语法

    1
    @Link 变量名: 变量类型;
    • 允许装饰的变量类型:同 @State。但注意,@Link 的类型必须和数据源的类型相同(父组件 @State: T, 子组件 @Link: T)。

    • 初始化规则,

      • 本地初始化:禁止

      • 从父组件初始化:必须@Link 变量 aLink 从父组件 @State 变量 aState 的初始化语法可以简写为 Comp({aLink: $aState})@Link 变量仅允许被状态变量初始化,不能用常量初始化。

      • 允许的父组件的数据源类型、子组件被初始化的参数类型,

        img

    • 访问控制:不支持组件外访问

    • 数据同步:双向同步。父组件中的状态变量可以与子组件 @Link 建立双向同步,当其中一方改变时,另外一方能够感知到变化。@Link 变量可以与父组件 @State, @StorageLink@Link 建立双向绑定

    • 可用范围:@Link 装饰器不能在 @Entry 装饰的自定义组件中使用

  3. UI 更新原理

    • 初始渲染时,父组件将 @State 变量的包装类通过构造函数传递给子组件,子组件的 @Link 变量的包装类拿到父组件的 @State 变量后,将 @Link 变量的包装类的 this 指针注册到父组件的 @State 变量上。即初始渲染后,
      • 父组件的 @State 变量有子组件 @Link 变量包装类的 this 指针
      • 子组件有父组件 @State 变量的包装类
    • @Link 数据源更新,父组件对所有依赖其变更的 @State 变量的系统组件(elementId)和状态变量(如 @Link 包装类)进行遍历更新。子组件中所有依赖 @Link 变量的系统组件都被通知更新。(父 -> 子)
    • @Link 变量更新,子组件调用父组件 @State 包装类的 set 方法,将更新的数值同步回子组件。子组件 @Link 变量和父组件 @State 变量分别遍历依赖其的系统组件,进行相应的 UI 刷新。(子 -> 父)
  4. 可以被观察到的状态变化:同 @State

  5. 使用说明

    • 双向同步时单向修改本地变量:结合使用 @Watch@Link,可以在双向数据同步的过程中修改本地的 @State 变量。同时,本地 @State 变量的更改不会对双向同步的数据产生影响。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      /*
      sourceNumber 变量双向同步时,memberMessage 变量被更新;但是 memberMessage 的更新不会影响到 sourceNumber
      */
      @Entry
      @Component
      struct Parent {
      @State sourceNumber: number = 0;

      build() {
      Column() {
      Text(`父组件的 sourceNumber:` + this.sourceNumber)
      Child({ sourceNumber: this.sourceNumber })
      Button('父组件更改 sourceNumber')
      .onClick(() => {
      this.sourceNumber++;
      })
      }
      .width('100%')
      .height('100%')
      }
      }

      @Component
      struct Child {
      @State memberMessage: string = 'Hello World';
      @Link @Watch('onSourceChange') sourceNumber: number;

      onSourceChange() {
      this.memberMessage = this.sourceNumber.toString();
      }

      build() {
      Column() {
      Text(this.memberMessage)
      Text(`子组件的 sourceNumber:` + this.sourceNumber.toString())
      Button('子组件更改 memberMessage')
      .onClick(() => {
      this.memberMessage = 'Hello memberMessage';
      })
      }
      }
      }
    • @Link 变量类型和数据源类型完全相同‼️:@Link 变量的数据源必须是父组件的状态变量,父组件 @State: T,则子组件 @Link: T,即 @Link 变量不能是父组件的状态变量的属性/元素。这一点与 @Prop 变量不同。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      @Observed
      class Info {
      public age: number = 0;

      constructor(age: number) {
      this.age = age;
      }
      }

      @Component
      struct LinkChild {
      @Link testNum: number;

      build() {
      Text(`LinkChild testNum ${this.testNum}`)
      }
      }

      @Component
      struct LinkChild2 {
      @Link testNum: Info;

      build() {
      Text(`LinkChild testNum ${this.testNum.age}`)
      }
      }

      @Entry
      @Component
      struct Parent {
      @State info: Info = new Info(1);

      build() {
      Column() {
      Text(`Parent testNum ${this.info.age}`)
      .onClick(() => {
      this.info.age += 1;
      })

      // 错误代码 - 开始
      // LinkChild({ testNum: this.info.age }) // @Link 装饰的变量和数据源 @State 类型不一致!
      // 错误代码 - 结束

      // 更正代码 - 开始
      LinkChild2({ testNum: this.info }) // @Link 装饰的变量和数据源 @State 类型一致!
      // 更正代码 - 结束
      }
      }
      }
    • 类的静态函数和组件的实例方法中直接修改状态变量的属性无效‼️:同 @State 的使用说明的同名问题,是没有数据代理导致的问题。

@Provide/@Consume - 组件后代双向同步
  1. 解释:@Provide@Consume,用于祖先组件与后代组件的双向数据绑定,用于状态数据的多层级传递@Provide 所装饰的变量位于祖先组件中,作为被提供给后代组件的状态变量;@Consume 所装饰的变量位于后代组件中,用于**消费(绑定)**祖先组件提供的状态变量。

    • @Provide@Consume 可以通过相同的变量名或者相同的别名(alias)绑定,建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常。
    • @Provide 变量和 @Consume 变量是一对多的关系。不允许在同一个自定义组件及其子组件中声明多个同名或者同别名的 @Provide 变量,@Provide 变量的属性名或别名需要唯一且确定,如果声明多个同名或者同别名的 @Provide 变量,会发生运行时报错
  2. 语法

    1
    2
    3
    4
    5
    6
    7
    // 基于相同变量名提供和消费状态变量
    @Provide 变量名: 变量类型 = 初始值; // 祖先组件中
    @Consume 变量名: 变量类型; // 后代组件中

    // 基于相同别名提供和消费状态变量
    @Provide('别名') 变量名1: 变量类型 = 初始值; // 祖先组件中
    @Consume('别名') 变量名2: 变量类型; // 后代组件中
    • 装饰器参数:可选的常量字符串,表示变量别名。如果指定了别名,则通过别名来绑定变量,否则,通过变量名绑定变量。

    • 允许装饰的变量类型:同 @State。但注意,@Provide 变量和 @Consume 变量的类型必须相同。

    • 初始化规则

      - @Provide @Consume
      本地初始化 必须 禁止
      从父组件初始化 可选 禁止
      仅允许通过相同的变量名和别名从 @Provide 初始化。
      允许的父组件的数据源类型
      子组件被初始化的参数类型
      img img
    • 访问控制:不支持组件外访问

    • 数据同步:双向同步。组件组件中的 @Provide 变量和后代组件中的 @Consume 组件建立双向同步,当其中一方改变时,另外一方能够感知到变化。双向同步操作与 @State@Link 的组合相同。

    • 重写参数:@Provide 支持 allowOverride 参数,此时 @Provide 变量可以被重写

      1
      2
      @Provide({allowOverride : '别名'}) 变量名: number = 10; // 重写通过别名提供的 @Provide 变量
      @Provide({allowOverride : '变量名'}) 变量名: number = 10; // 重写通过变量名提供的 @Provide 变量
      1
      2
      3
      interface ProvideOptions {
      allowOverride?: string // 是否允许 @Provide 重写。允许在同一组件树下通过 allowOverride 重写同名的 @Provide。如果开发者未配置 allowOverride,定义了同名的 @Provide,运行时会报错。
      }
  3. UI 更新原理

    • 初始渲染时,@Provide 变量被以 map 的形式传递给 @Provide 变量所属组件的所有子组件,子组件中如果使用 @Consume 变量,则在 map 中以该变量名或别名为键,查找对应的 @Provide 变量,
      • 如果查找到,@Consume 变量会进行自身初始化,并保存该 @Provide 变量的引用,同时将自身注册给 @Provide 变量
      • 如果查找不到,框架会抛出 JS Error。
    • @Provide 变量更新后,会遍历更新所有依赖它的系统组件(elementId)和状态变量(@Consume)。子组件中所有依赖 @Consume 的系统组件(elementId)都会被通知更新.。
    • @Consume 变量更新后,调用预先保存的 @Provide 变量的更新方法,将更新的数值同步回 @Provide
  4. 可以被观察到的状态变化:同 @State

  5. 使用说明

    • 尾随闭包初始化 @BuilderParam 时的 @Provide 变量未定义问题‼️:当在一个组件中使用尾随闭包来初始化 @BuilderParam 变量,并同时定义 @Provide 变量时,可能会遇到 @BuilderParam 变量生成的子组件无法通过 @Consume 访问 @Provide 定义的变量的问题。这是因为在调用 this.builder() 时,this 指向的是当前组件的父组件,而不是当前组件本身。因此,子组件无法找到当前组件中定义的 @Provide 变量。【文档描述不清】
      • 为了避免此问题,建议不要在同一个组件中同时使用 @BuilderParam@Provide。通过将它们分开使用,可以确保子组件正确访问 @Provide 定义的变量。
    • 类的静态函数和组件的实例方法中直接修改状态变量的属性无效‼️:同 @State 的使用说明的同名问题,是没有数据代理导致的问题。
  1. 解释:@Observed@ObjectLink,用于嵌套场景的观察,用于弥补装饰器仅能观察一层的能力限制。所谓嵌套场景,即二维数组、对象数组或属性为 classclass@Observed 所装饰的类可以通过 new 创建实例,该实例属性的变化可以被监测到;@ObjectLink 所装饰的类可以接收被 @Observed 所装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定,该实例可以是来自数组状态变量的元素,对象状态变量的属性。

    • 数据双向同步:@Observed + @ObjectLink
    • 数据单向同步:@Observed + @Prop
  2. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Observed
    class 类名 {
    public 属性名1: 属性类型1;
    public 属性名2: 属性类型2;

    constructor(属性名1: 属性类型1, 属性名2: 属性类型2) {
    this.属性名1 = 属性类型1;
    this.属性名2 = 属性类型2;
    }
    }
    1
    @ObjectLink 变量名: 变量类型;
    • 允许装饰的变量类型:@Observed 用于装饰 class@ObjectLink 用于装饰被 @Observed 装饰的 class 实例,包含继承 DateArrayMapSetclass 实例,支持和 undefinednull 组成的联合类型。

    • 初始化规则

      • 本地初始化:禁止

      • 从父组件初始化:必须。用于初始化的数据源必须同时满足,

        • 类型必须是 @Observed 装饰的 class
        • 必须是 @State@Link@Provide@Consume 或者 @ObjectLink 装饰的 class 的属性或数组项
      • 允许的父组件的数据源类型、子组件被初始化的参数类型,

        img

    • 数据同步:双向同步

    • 访问控制:@ObjectLink 变量是只读的,其属性可变,但不能直接被赋值,否则会导致运行时报错(可以在父组件中通过整体替换来实现)@ObjectLink 变量相当于指向数据源的指针(引用)

    • 可用范围:@ObjectLink 装饰器不能在 @Entry 装饰的自定义组件中使用

    • 潜在问题:使用 @Observed 装饰 class 会改变 class 原始的原型链,因此 @Observed 和其他类装饰器装饰同一个 class 可能会带来问题。

    • @ObjectLink Vs. @Prop@ObjectLink 变量存储对数据源的引用,与数据源建立双向同步关系,@Prop 变量存储对数据源的深拷贝,与数据源建立单向同步。

  3. 可以观察到的状态变化:被 @Observed 装饰 class 的实例的整体赋值或属性变更;被 @Observed 装饰的继承 Array/Date/Map/Set 的实例的整体赋值或属性变更,以及相应 API 的调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    @Observed
    class ObservedArray<T> extends Array<T> {
    // new ObservedArray<T>(arr: T[]) 创建的数组的元素的增删改都可以被监测到,基于此可以实现二维数组 [new ObservedArray<T>(arr: T[])],数组项或数组项的数组项的增删改都可以被监测到。
    constructor(args: T[]) {
    super(...args);
    }
    }

    @Observed
    class DateClass extends Date {
    constructor(args: number | string) {
    super(args);
    }
    }

    @Observed
    class MyMap<K, V> extends Map<K, V> {
    public name: string;

    constructor(name?: string, args?: [K, V][]) {
    super(args);
    this.name = name ? name : "My Map";
    }

    getName() {
    return this.name;
    }
    }

    @Observed
    class MySet<T> extends Set<T> {
    public name: string;

    constructor(name?: string, args?: T[]) {
    super(args);
    this.name = name ? name : "My Set";
    }

    getName() {
    return this.name;
    }
    }
  4. UI 更新原理

    • 初始渲染时,被 @Observed 装饰的 class 的实例会被不透明的代理对象包装,class 上的属性的 settergetter 方法被代理。子组件的 @ObjectLink 变量接收被 @Observed 装饰的 class 的实例,@ObjectLink 变量的包装类会将自己注册@Observed class。
    • @Observed 类的实例的属性更改,执行相应的 settergetter 方法,遍历依赖它的 @ObjectLink 包装类,通知数据更新
  5. 使用说明

    • 通过组件嵌套实现嵌套对象属性的深度监测:假设现在有数据结构 ParentCounter 如下,其中入口组件 Index@State 变量 counter 的类型为 Array<ParentCounter>counter 的数据深度为 3,第一层访问到数组元素 ParentCounter 实例,第二层访问到 counterchildCounter 实例,第三层访问到 childCounter 实例的 counter@State 变量只能检测到第一层数据的修改,即数组元素的增删改。为了监测到每一层数据,需要封装两个组件 ParentCompChildComp,其中 ParentCompChildComp 的父组件。Parent 组件声明 @ObjectLink 变量,使用 counter 的元素初始化,ChildComp 组件声明 @ObjectLink 变量,使用 Parent@ObjectLink 变量的 childCounter 属性初始化,此时在入口组件对 @State 变量的任意层次的修改都会被监测到,并在 UI 上进行渲染更新。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      let nextId = 1;

      @Observed
      class ChildCounter {
      counter: number;

      constructor(counter: number) {
      this.counter = counter;
      }
      }

      @Observed
      class ParentCounter {
      _id: number;
      counter: number;
      childCounter: ChildCounter;

      constructor(counter: number) {
      this._id = nextId++;
      this.counter = counter;
      this.childCounter = new ChildCounter(counter);
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      @Component
      struct ParentComp {
      @ObjectLink value: ParentCounter;

      build() {
      Column({ space: 10 }) {
      Text(`ParentComp.counter ${this.value.counter}(click +1)`)
      .fontSize(20)
      .onClick(() => {
      this.value.counter++;
      })
      ChildComp({ value: this.value.childCounter })
      Divider().height(2)
      }
      }
      }

      @Component
      struct ChildComp {
      @ObjectLink value: ChildCounter;

      build() {
      Text(`ChildComp.counter ${this.value.counter}(click +1)`)
      .onClick(() => {
      this.value.counter += 1;
      })
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      @Entry
      @Component
      struct Index {
      @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];

      build() {
      Row() {
      Column() {
      ParentComp({ value: this.counter[0] })
      ParentComp({ value: this.counter[1] })
      ParentComp({ value: this.counter[2] })
      Divider().height(5)
      ForEach(this.counter,
      (item: ParentCounter) => {
      ParentComp({ value: item })
      },
      (item: ParentCounter) => 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+1,counter[0].childCounter.counter+10')
      .fontSize(20).height(50)
      .onClick(() => {
      this.counter[0].counter++;
      this.counter[0].childCounter.counter += 10;
      })
      Text('Parent: set counter[0] to 10')
      .fontSize(20).height(50)
      .onClick(() => {
      this.counter[0].counter = 10;
      })
      }
      }
      }
      }
    • @Observed 类的构造函数中对属性的变更无效:一般来说,使用 @Observed 装饰类后,会给该类使用一层“代理”进行包装。当在组件中改变该类的成员变量时,会被该代理进行拦截,在更改数据源中值的同时,也会将变化通知给绑定的组件,从而实现观测变化与触发刷新。但是在类的构造函数中对成员变量进行的赋值或者修改时,操作不会经过代理(因为是直接对数据源中的值进行修改),也就无法被观测到。因此,不要在构造函数中对成员变量的值进行变更。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      @Observed
      class RenderClass {
      waitToRender: boolean = false;

      constructor() {
      setTimeout(() => {
      this.waitToRender = true;
      console.log("更改 waitToRender 的值为:" + this.waitToRender);
      }, 1000)
      }
      }

      @Entry
      @Component
      struct Index {
      @State @Watch('renderClassChange') renderClass: RenderClass = new RenderClass();
      @State textColor: Color = Color.Black;

      renderClassChange() {
      console.log("renderClass 的值被更改为:" + this.renderClass.waitToRender);
      }

      build() {
      Row() {
      Column() {
      Text("renderClass 的值为:" + this.renderClass.waitToRender)
      .fontSize(20)
      .fontColor(this.textColor)
      Button("Show")
      .onClick(() => {
      // 使用其他状态变量强行刷新 UI 的做法并不推荐,此处仅用来检测 waitToRender 的值是否更新
      this.textColor = Color.Red;
      })
      }
      .width('100%')
      }
      .height('100%')
      }
      }
    • @ObjectLink/@Prop 变量的数据源在父组件实际刷新时更新,而不是在父组件数据源变化后立刻发生的。下述事例中,点击按钮,日志输出顺序为:1-2-3-4-5。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      @Observed
      class Person {
      name: string = '';
      age: number = 0;

      constructor(name: string, age: number) {
      this.name = name;
      this.age = age;
      }
      }

      @Observed
      class Info {
      person: Person;

      constructor(person: Person) {
      this.person = person;
      }
      }

      @Entry
      @Component
      struct Parent {
      @State @Watch('onChange01') info: Info = new Info(new Person('Bob', 10));

      onChange01() {
      console.log(':::onChange01:' + this.info.person.name); // 2,被监测变量变更时,@Watch 函数立即同步执行
      }

      build() {
      Column() {
      Text(this.info.person.name).height(40)
      Child({
      per: this.info.person, clickEvent: () => {
      console.log(':::clickEvent before', this.info.person.name); // 1
      this.info.person = new Person('Jack', 12);
      console.log(':::clickEvent after', this.info.person.name); // 3
      }
      })
      }
      }
      }

      @Component
      struct Child {
      @ObjectLink @Watch('onChange02') per: Person;
      clickEvent?: () => void;

      onChange02() {
      console.log(':::onChange02:' + this.per.name); // 5,Parent 组件刷新,异步触发 Child 组件更新,更新 @ObjectLink 数据源
      }

      build() {
      Column() {
      Button(this.per.name)
      .height(40)
      .onClick(() => {
      this.onClickType();
      })
      }
      }

      private onClickType() {
      if (this.clickEvent) {
      this.clickEvent();
      }
      console.log(':::--------此时 Child 中的 this.per.name 值仍然是:' + this.per.name); // 4,此时只是将 Child 组件标记为需要父组件更新的节点
      }
      }
    • 类的静态函数和组件的实例方法中直接修改状态变量的属性无效‼️:同 @State 的使用说明的同名问题,是没有数据代理导致的问题。

应用状态管理

LocalStorage - 页面级 UI 状态存储
  1. 解释:LocalStorage 是页面级的 UI 状态存储,通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例。@LocalStorageProp 装饰的变量与 LocalStorage 中给定属性建立单向同步关系@LocalStorageLink 装饰的变量与 LocalStorage 中给定属性建立双向同步关系。可以通过 LocalStorage 提供的 API 接口在自定义组件外手动触发 Storage 对应 key 的增删改查。

    • 应用程序可以创建多个 LocalStorage 实例,LocalStorage 实例可以在页面内共享,也可以通过 getShared 接口,实现跨页面UIAbility 实例内共享。getShared 接口仅能获取当前 Stage 通过 windowStage.loadContent 传入的 LocalStorage 实例,否则返回 undefined
    • 组件树的根节点,即被 @Entry 装饰的 @Component 组件,可以被分配一个 LocalStorage 实例,此组件的所有子组件实例将自动获得对该 LocalStorage 实例的访问权限
    • @Component 组件既可以自动继承来自父组件的 LocalStorage 实例,也可以传入指定的 LocalStorage 的实例。
    • LocalStorage 中的所有属性都是可变的
    • 应用程序决定 LocalStorage 对象的生命周期:当应用释放最后一个指向 LocalStorage 的引用时,比如销毁最后一个自定义组件,LocalStorage 将被 JS Engine 垃圾回收。
  2. 语法

    1
    2
    @LocalStorageProp("键值") 变量名: 变量类型 = 初始值;
    @LocalStorageLink("键值") 变量名: 变量类型 = 初始值;
    • 装饰器参数:必须的常量字符串,用于绑定 LocalStorage 中对应的属性,初始化 @LocalStorageXxx 变量。因为无法保证 LocalStorage 一定存在给定的 key@LocalStorageXxx 变量一定需要本地初始化。

    • 允许装饰的变量类型:同 @State 。但是,变量类型必须被指定,建议和 LocalStorage 中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。

    • 初始化规则,

      • 本地初始化:必须@LocalStorageXxx 变量只能从 LocalStorage 中key 对应的属性初始化,如果没有对应 key 的话,将使用本地默认值初始化,并存入 LocalStorage 中

      • 从父组件初始化:禁止

      • 允许的父组件的数据源类型、子组件被初始化的参数类型,

        @LocalStorageProp @LocalStorageLink
        img img
    • 访问控制:不支持组件外访问

    • 数据同步

      @LocalStorageProp @LocalStorageLink
      单向同步
      同步方向为:LocalStorage 的对应属性到组件的状态变量。组件本地的修改是允许的,但是 LocalStorage 中给定的属性一旦发生变化,将覆盖本地的修改。
      双向同步
      同步方向为:LocalStorage 的对应属性到自定义组件,从自定义组件到 LocalStorage 对应属性。
  3. 可以观察到的状态变化:同 @State 变量。

  4. UI 更新原理

    @LocalStorageProp @LocalStorageLink
    img img
  5. LocalStorage API

    • 构造函数

      • 语法:constructor(initializingProperties?: Object)
      • 说明:可选使用 initializingProperties 包含的属性和数值初始化 LocalStorage。initializingProperties 不能为 undefined
      1
      2
      let para: Record<string, number> = { 'PropA': 47 };
      let storage: LocalStorage = new LocalStorage(para);
    • 静态方法

      • static getShared(): LocalStorage 获取当前 stage 共享的 LocalStorage 实例。
    • 实例方法

      方法类型 功能 返回值
      has(propName: string): boolean 判断 propName 对应的属性是否在 LocalStorage 中存在。 如果 propName 对应的属性在 LocalStorage 中存在,则返回 true,不存在则返回 false。
      `get(propName: string): T undefined` 获取 propName 在 LocalStorage 中对应的属性值。
      set<T>(propName: string, newValue: T): boolean 在 LocalStorage 中设置 propName 对应属性的值。如果 newValue 的值和 propName 对应属性的值相同,即不需要做赋值操作,状态变量不会通知 UI 刷新 propName 对应属性的值。从 API version 12 开始, newValue 可以为 null 或 undefined。 如果 LocalStorage 中不存在 propName 对应的属性,返回 false。设置成功返回 true。
      setOrCreate<T>(propName: string, newValue: T): boolean 如果 propName 已经在 LocalStorage 中存在,并且 newValue 和 propName 对应属性的值不同,则设置 propName 对应属性的值为 newValue,否则状态变量不会通知 UI 刷新 propName 对应属性的值。
      如果 propName 不存在,则创建 propName 属性,值为 newValue。setOrCreate 只可以创建单个 LocalStorage 的键值对,如果想创建多个 LocalStorage 键值对,可以多次调用此方法。从 API version 12 开始,newValue 可以为 null 或 undefined。
      如果 LocalStorage 中存在 propName,则更新其值为 newValue,返回 true。
      如果 LocalStorage 中不存在 propName,则创建 propName,并初始化其值为 newValue,返回 true。
      `public ref(propName: string): AbstractProperty undefined` 如果给定的 propName 在 LocalStorage 中存在,则获得 LocalStorage 中 propName 对应数据的引用。否则,返回 undefined。
      与 link 的功能基本一致,但不需要手动释放返回的 AbstractProperty 类型的变量。
      public setAndRef<T>(propName: string, defaultValue: T): AbstractProperty<T> 与 ref 接口类似,如果给定的 propName 在 LocalStorage 中存在,则获得 LocalStorage 中 propName 对应数据的引用。
      如果不存在,则使用 defaultValue 在 LocalStorage 中创建和初始化 propName 对应的属性,并返回其引用。defaultValue 须为 T 类型,可以为 null 或 undefined。
      与 setAndLink 的功能基本一致,但不需要手动释放返回的 AbstractProperty 类型的变量。
      AbstractProperty 的实例,为 LocalStorage 中 propName 对应属性的引用。
      link<T>(propName: string): SubscribedAbstractProperty<T> 如果给定的 propName 在 LocalStorage 实例中存在,则返回与 LocalStorage 中 propName 对应属性的双向绑定数据
      双向绑定数据的修改会被同步回 LocalStorage中,LocalStorage 会将变化同步到所有绑定该 propName 的数据和 Component 中。
      如果 LocalStorage 中不存在 propName,则返回 undefined。
      SubscribedAbstractProperty 的实例,与 LocalStorage 中 propName 对应属性的双向绑定的数据,如果 LocalStorage 中不存在对应的 propName,则返回 undefined。
      setAndLink<T>(propName: string, defaultValue: T): SubscribedAbstractProperty<T> 与 link 接口类似,如果给定的 propName 在 LocalStorage 中存在,则返回该 propName 对应的属性的双向绑定数据。如果不存在,则使用 defaultValue 在 LocalStorage 中创建和初始化 propName 对应的属性,返回其双向绑定数据。defaultValue 必须为 T 类型,从 API 12 开始 defaultValue 可以为 null 或 undefined。 SubscribedAbstractProperty 的实例,与 LocalStorage 中 propName 对应属性的双向绑定的数据。
      prop<S>(propName: string): SubscribedAbstractProperty<S> 如果给定的 propName 在 LocalStorage 中存在,则返回与 LocalStorage 中 propName 对应属性的单向绑定数据。如果 LocalStorage 中不存在 propName,则返回 undefined。单向绑定数据的修改不会被同步回 LocalStorage 中。 SubscribedAbstractProperty 的实例,和 LocalStorage 中 propName 对应属性的单向绑定的数据。如果 LocalStorage 中不存在对应的 propName,则返回 undefined。
      setAndProp<S>(propName: string, defaultValue: S): SubscribedAbstractProperty<S> 与 prop 接口类似。如果 propName 在 LocalStorage 中存在,则返回该 propName 对应的属性的单向绑定数据。如果不存在,则使用 defaultValue 在 LocalStorage 中创建和初始化propName对应的属性,返回其单向绑定数据。 defaultValue 必须为 S 类型,从 API version 12 开始 defaultValue 可以为 null 或 undefined。 SubscribedAbstractProperty 的实例,和 LocalStorage 中 propName 对应属性的单向绑定的数据。
      delete(propName: string): boolean 在 LocalStorage 中删除 propName 对应的属性。在 LocalStorage 中删除属性的前提是该属性已经没有订阅者,如果有订阅者,则返回 false。如果没有订阅者则删除成功并返回 true。 如果 LocalStorage 中有对应的属性,且该属性已经没有订阅者,则删除成功,返回 true。如果属性不存在,或者该属性还存在订阅者,则返回 false。
      keys(): IterableIterator<string> 返回 LocalStorage 中所有的属性名。 LocalStorage 中所有的属性名。
      size(): number 返回 LocalStorage 中的属性数量。 LocalStorage 中的属性数量。
      clear(): boolean 删除 LocalStorage 中所有的属性。删除所有属性的前提是已经没有任何订阅者。如果有订阅者,clear 不会生效并返回 false。如果没有订阅者则删除成功并返回 true。 如果 LocalStorage 中的属性已经没有任何订阅者,则删除成功,并返回 true。否则返回 false。
      • 属性的订阅者
        • @LocalStorageLink、@LocalStorageProp 装饰的变量。
        • 通过 link、prop、setAndLink、setAndProp 接口返回的 SubscribedAbstractProperty 的实例。
      • 属性的订阅者的删除方式
        • 删除 @LocalStorageLink、@LocalStorageProp 所在的自定义组件。
        • 对 link、prop、setAndLink、setAndProp 接口返回的 SubscribedAbstractProperty 的实例调用 aboutToBeDeleted 接口。
    • AbstractProperty 对象的实例方法

      方法 功能
      get(): T 读取 AppStorage/LocalStorage 中所引用属性的数据。
      set(newValue: T): void 更新 AppStorage/LocalStorage 中所引用属性的数据,newValue 必须是 T 类型,可以为 null 或 undefined。
      info(): string 读取 AppStorage/LocalStorage 中所引用属性的属性名。
    • SubscribedAbstractProperty 对象的实例方法

      方法 功能
      abstract get(): T 读取从 AppStorage/LocalStorage 同步属性的数据。
      abstract set(newValue: T): void 设置 AppStorage/LocalStorage 同步属性的数据, newValue 必须是 T 类型,从 API version 12 开始可以为 null 或 undefined。
      abstract aboutToBeDeleted(): void 取消 SubscribedAbstractProperty 实例对 AppStorage/LocalStorage 的单/双向同步关系,并无效化 SubscribedAbstractProperty 实例,即当调用 aboutToBeDeleted 方法之后不能再使用 SubscribedAbstractProperty 实例调用set或 get 方法。
      info(): string 返回属性名称。
  6. 使用说明

    • LocalStorage 实例添加到入口组件后,该组件的所有子组件将自动获得对该 LocalStorage 实例的访问权限

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      class Data {
      code: number;

      constructor(code: number) {
      this.code = code;
      }
      }

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

      @Component
      struct Child {
      // @LocalStorageLink 变量装饰器与 LocalStorage 中的 'PropA' 属性建立双向绑定
      @LocalStorageLink('PropA') childLinkNumber: number = 1;
      // @LocalStorageLink 变量装饰器与 LocalStorage 中的 'PropB' 属性建立双向绑定
      @LocalStorageLink('PropB') childLinkObject: Data = new Data(0);

      build() {
      Column({ space: 15 }) {
      Button(`Child from LocalStorage ${this.childLinkNumber}`)// 更改将同步至 LocalStorage 中的 'PropA' 以及 Parent.parentLinkNumber
      .onClick(() => {
      this.childLinkNumber += 1;
      })

      Button(`Child from LocalStorage ${this.childLinkObject.code}`)// 更改将同步至 LocalStorage 中的 'PropB' 以及 Parent.parentLinkObject.code
      .onClick(() => {
      this.childLinkObject.code += 1;
      })
      }
      }
      }

      // 使LocalStorage可从@Component组件访问
      // @Entry(storage) // 写法一
      @Entry({storage}) // 写法二
      @Component
      struct Parent {
      // @LocalStorageLink 变量装饰器与 LocalStorage 中的 'PropA' 属性建立双向绑定
      @LocalStorageLink('PropA') parentLinkNumber: number = 1;
      // @LocalStorageLink 变量装饰器与 LocalStorage 中的 'PropB' 属性建立双向绑定
      @LocalStorageLink('PropB') parentLinkObject: Data = new Data(0);

      build() {
      Column({ space: 15 }) {
      Button(`Parent from LocalStorage ${this.parentLinkNumber}`)// 由于 LocalStorage 中 PropA 已经被初始化,因此 this.parentLinkNumber 的值为 47
      .onClick(() => {
      this.parentLinkNumber += 1;
      })

      Button(`Parent from LocalStorage ${this.parentLinkObject.code}`)// 由于 LocalStorage 中 PropB 已经被初始化,因此 this.parentLinkObject.code 的值为 50
      .onClick(() => {
      this.parentLinkObject.code += 1;
      })
      // @Component 子组件自动获得对 Parent 的 LocalStorage 实例的访问权限。
      Child()
      }
      }
      }
    • 为了让 LocalStorage 实例在多个视图中共享,则需要

      • 在所属的 UIAbility 中创建 LocalStorage 实例,并调用 windowStage.loadContent 传入该实例。
      • 之后在页面 UI 的根组件中通过 LocalStorage.getShared 即可获取到通过 loadContent 共享的 LocalStorage 实例。
      • 注意:LocalStorage.getShared() 只在模拟器或者实机上才有效,在 Previewer 预览器中使用不生效。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // src/main/ets/entryability/EntryAbility.ets
      import { UIAbility } from '@kit.AbilityKit';
      import { window } from '@kit.ArkUI';

      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);
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      // src/main/ets/pages/Index.ets

      // 通过 getShared 接口获取 stage 共享的 LocalStorage 实例
      @Entry({ storage: LocalStorage.getShared() })
      @Component
      struct Index {
      // 可以使用 @LocalStorageLink/Prop 与 LocalStorage 实例中的变量建立联系
      @LocalStorageLink('PropA') propA: number = 1;
      pageStack: NavPathStack = new NavPathStack();

      build() {
      Navigation(this.pageStack) {
      Row(){
      Column() {
      Text(`${this.propA}`)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      Button("To Page")
      .onClick(() => {
      this.pageStack.pushPathByName('Page', null);
      })
      }
      .width('100%')
      }
      .height('100%')
      }
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      // src/main/ets/pages/Page.ets
      // Page.ets

      @Builder
      export function PageBuilder() {
      Page()
      }

      // Page 组件获得了父亲 Index 组件的 LocalStorage 实例
      @Component
      struct Page {
      @LocalStorageLink('PropA') propA: number = 2;
      pathStack: NavPathStack = new NavPathStack();

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

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

      Button("Back Index")
      .onClick(() => {
      this.pathStack.pop();
      })
      }
      .width('100%')
      }
      }
      .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
      })
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // src/main/resources/base/profile/route_map.json
      // 使用 Navigation 时作页面跳转时必须的配置(组件形式的页面跳转)
      {
      "routerMap": [
      {
      "name": "Page",
      "pageSourceFile": "src/main/ets/pages/Page.ets",
      "buildFunction": "PageBuilder",
      "data": {
      "description" : "LocalStorage example"
      }
      }
      ]
      }
      1
      2
      3
      4
      // src/main/module.json
      {
      "routerMap": "$profile:route_map"
      }
    • 非根节点的自定义组件可以通过构造参数来传递 LocalStorage 实例。根据自定义组件成员属性的数量和初始化方式不同,传参方式不同,

      • 自定义组件定义了成员属性,且需要通过父组件初始化对应的属性。LocalStorage 实例必须要放在第二个参数位置传递。

        1
        Child({ count: this.count }, localStorage)
      • 自定义组件定义了成员属性,且不需要通过父组件初始化对应的属性。LocalStorage 实例仍然必须要放在第二个参数位置传递,第一个参数需要传 {}

        1
        Child({}, localStorage)
      • 自定义组件没有定义成员属性。可以只传入一个 LocalStorage 实例作为入参。

        1
        Child(localStorage)
    • 通过 Navigation 组件可以实现跳转到不同页面时,绑定不同的 LocalStorage 实例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      let localStorageA: LocalStorage = new LocalStorage();
      localStorageA.setOrCreate('PropA', 'PropA');

      let localStorageB: LocalStorage = new LocalStorage();
      localStorageB.setOrCreate('PropB', 'PropB');

      let localStorageC: LocalStorage = new LocalStorage();
      localStorageC.setOrCreate('PropC', 'PropC');

      @Entry
      @Component
      struct MyNavigationTestStack {
      @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack(); // Navigation 路由栈

      @Builder
      PageMap(name: string) {
      if (name === 'pageOne') {
      // 传递不同的LocalStorage实例
      pageOneStack({}, localStorageA)
      } else if (name === 'pageTwo') {
      pageTwoStack({}, localStorageB)
      } else if (name === 'pageThree') {
      pageThreeStack({}, localStorageC)
      }
      }

      build() {
      Column({ space: 5 }) {
      // Navigation 组件是路由导航的根视图容器,一般作为 Page 页面的根容器使用,
      // 其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation 的子组件)
      // 或非首页显示(NavDestination 的子组件),首页和非首页通过路由进行切换。
      Navigation(this.pageInfo) { // 绑定路由栈 pageInfo 到 Navigation 组件。
      Column() {
      Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
      .width('80%')
      .height(40)
      .margin(20)
      .onClick(() => {
      this.pageInfo.pushPath({ name: 'pageOne' }); // 将 name 指定的 NavDestination 页面信息入栈
      })
      }
      }
      .title('NavIndex')
      .navDestination(this.PageMap)
      .mode(NavigationMode.Stack)
      .borderWidth(1)
      }
      }
      }

      @Component
      struct pageOneStack {
      @Consume('pageInfo') pageInfo: NavPathStack;
      @LocalStorageLink('PropA') PropA: string = 'Hello World';

      build() {
      NavDestination() {
      Column() {
      NavigationContentMsgStack()
      // 显示绑定的 LocalStorage 中 PropA 的值 'PropA'
      Text(`${this.PropA}`)
      Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
      .width('80%')
      .height(40)
      .margin(20)
      .onClick(() => {
      this.pageInfo.pushPathByName('pageTwo', null);
      })
      }.width('100%').height('100%')
      }.title('pageOne')
      .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
      })
      }
      }

      @Component
      struct pageTwoStack {
      @Consume('pageInfo') pageInfo: NavPathStack;
      @LocalStorageLink('PropB') PropB: string = 'Hello World';

      build() {
      NavDestination() {
      Column() {
      NavigationContentMsgStack()
      // 如果绑定的 LocalStorage 中没有 PropB,显示本地初始化的值 'Hello World'
      Text(`${this.PropB}`)
      Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
      .width('80%')
      .height(40)
      .margin(20)
      .onClick(() => {
      this.pageInfo.pushPathByName('pageThree', null);
      })

      }
      .width('100%').height('100%')
      }
      .title('pageTwo')
      .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
      })
      }
      }

      @Component
      struct pageThreeStack {
      @Consume('pageInfo') pageInfo: NavPathStack;
      @LocalStorageLink('PropC') PropC: string = 'pageThreeStack';

      build() {
      NavDestination() {
      Column() {
      NavigationContentMsgStack()

      // 如果绑定的 LocalStorage 中没有 PropC,显示本地初始化的值 'pageThreeStack'
      Text(`${this.PropC}`)
      Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
      .width('80%')
      .height(40)
      .margin(20)
      .onClick(() => {
      this.pageInfo.pushPathByName('pageOne', null);
      })

      }
      .width('100%')
      .height('100%')
      }
      .title('pageThree')
      .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
      })
      }
      }

      @Component
      struct NavigationContentMsgStack {
      @LocalStorageLink('PropA') PropA: string = 'Hello'; // 不同的页面中访问的是不同的 LocalStorage 实例

      build() {
      Column() {
      Text(`${this.PropA}`)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
      }
      }
      }
AppStorage - 应用全局 UI 状态存储
  1. 解释:AppStorage 是在应用启动的时候会被创建的单例,是应用状态数据的中心存储。AppStorage 可以和 UI 组件同步,且可以在应用业务逻辑中被访问。AppStorage 支持应用的主线程内多个 UIAbility 实例间的状态共享。

    • @StorageProp(key) 是和 AppStorage 中 key 对应的属性建立单向数据同步,允许本地改变,但是对于 @StorageProp,本地的修改永远不会同步回 AppStorage 中,相反,如果 AppStorage 给定 key 的属性发生改变,改变会被同步给 @StorageProp,并覆盖掉本地的修改。
    • @StorageLink(key) 是和 AppStorage 中 key 对应的属性建立双向数据同步。本地修改发生,该修改会被写回 AppStorage 中。AppStorage 中的修改发生后,该修改会被同步到所有绑定 AppStorage 对应 key 的属性上,包括单向(@StorageProp 和通过 Prop 创建的单向绑定变量)、双向(@StorageLink 和通过 Link 创建的双向绑定变量)变量和其他实例(比如 PersistentStorage)。
    • AppStorage 是单例,它的所有 API 都是静态的,使用方法类似于 LocalStorage 中对应的非静态方法。
  2. 语法(类同 LocalStorage)

    1
    2
    @StorageProp("键值") 变量名: 变量类型 = 初始值;
    @StorageLink("键值") 变量名: 变量类型 = 初始值;
PersistentStorage - UI 状态的持久化存储
  1. 解释:LocalStorage 和 AppStorage 都是运行时的内存,而 PersistentStorage 可以实现持久化存储选定的 AppStorage 属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。

    • PersistentStorage 是应用程序中的可选单例对象,其提供的变量持久化的能力都需要依赖 AppStorage。

    • PersistentStorage 和 AppStorage 中的属性建立双向同步

    • 不要大量的数据持久化,因为 PersistentStorage 写入磁盘的操作是同步的,大量的数据本地化读写会同步在 UI 线程中执行,影响 UI 渲染性能。

  2. PersistentStorage API 语法(与 App Storage 类似,都是静态方法)

    • static persistProp<T>(key: string, defaultValue: T): void 将 AppStorage 中 key 对应的属性持久化到文件中。该接口的调用通常在访问 AppStorage 之前

      • 如果 PersistentStorage 文件中存在 key 对应的属性,在 AppStorage 中创建对应的 propName,并用在 PersistentStorage 中找到的 key 的属性初始化。
      • 如果 PersistentStorage 文件中没有查询到 key 对应的属性,则在 AppStorage 中查找 key 对应的属性。
        • 如果找到 key 对应的属性,则将该属性持久化。
        • 如果 AppStorage 中也没查找到 key 对应的属性,则在 AppStorage 中创建 key 对应的属性。用 defaultValue 初始化其值,并将该属性持久化。
    • static deleteProp(key: string): void persistProp 的逆向操作。将 key 对应的属性从 PersistentStorage 中删除,后续 AppStorage 的操作,对 PersistentStorage 不会再有影响。该操作会将对应的 key 从持久化文件中删除,如果希望再次持久化,可以再次调用 persistProp 接口。

    • static persistProps(props: PersistPropsOptions[]): void 行为和 persistProp 类似,不同在于可以一次性持久化多个数据,适合在应用启动的时候初始化。

      1
      2
      3
      4
      interface PersistPropsOptions {
      key: string; // 属性名
      defaultValue: number | string | boolean | Object
      }
    • static keys(): Array<string> 返回所有持久化属性的属性名的数组。

  3. 持久化数据的类型

    • 不支持

      • number, string, boolean, enum 等简单类型。

      • 可以被 JSON.stringify() 和 JSON.parse() 重构的对象(不包括对象中的成员方法)。

      • Map、Set、Date 类型,并可观察到这些类型的整体赋值和接口调用。

      • undefined、null。

      • 上述类型的联合类型。

    • 不支持

      • 嵌套对象(对象数组,对象的属性是对象等)。
  4. 持久化数据的约束

    • 避免持久化的情景:大型数据集、经常变化的变量。

    • 持久化的数据大小:小于 2kb。

    • 持久化的时机:持久化操作需要在 UI 实例初始化成功后(即 loadContent 传入的回调被调用时)才可以被调用,早于该时机调用会导致持久化失败。(原因:PersistentStorage 和 UI 实例相关联)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // EntryAbility.ets
      onWindowStageCreate(windowStage: window.WindowStage): void {
      windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
      return;
      }
      // 在这里进行持久化操作,此时 UI 实例初始化成功
      PersistentStorage.persistProp('aProp', 47);
      });
      }
  5. 使用说明

    • PersistentStorage 初始化、更新、重新启动数据变更分析

      • 初始化
        • 调用 persistProp 初始化 PersistentStorage,并查询属性 aProp 在 PersistentStorage 中是否存在,不存在。
        • 查询属性 aProp 在 AppStorage 中是否存在,不存在。
        • PersistentStorage 将属性 aProp 和值 47 写入磁盘, AppStorage 中 aProp 对应的值和其后续的更改将被持久化。
        • 在 Index 组件中创建状态变量 @StorageLink(‘aProp’) aProp,和 AppStorage 中 aProp 双向绑定。
      • 更新(点击事件)
        • @StorageLink(‘aProp’) aProp 改变,触发 Text 组件重新刷新。
        • @StorageLink(‘aProp’) aProp 的变化会被同步回 AppStorage 中。
        • AppStorage 中 aProp 属性的改变会同步到所有绑定该 aProp 的单向或者双向变量,这里没有。
        • AppStorage 中 aProp 的改变会触发 PersistentStorage,将新的改变写入本地磁盘。
      • 重新启动
        • 调用 persistProp 初始化 PersistentStorage,并查询属性 aProp 在 PersistentStorage 中是否存在,存在。
        • 将在 PersistentStorage 查询到的值写入 AppStorage 中。
        • 在 Index 组件里,@StorageLink 绑定的 aProp 为 PersistentStorage 写入 AppStorage 中的值。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      PersistentStorage.persistProp('aProp', 47); // 1. 初始化 PersistentStorage

      const aProp = AppStorage.get<number>('aProp'); // 2. 在组件外部从 AppStorage 中访问 PersistentStorage 初始化的属性

      @Entry
      @Component
      struct Index {
      @State message: string = 'Hello World';
      @StorageLink('aProp') aProp: number = 48; // 2. 在组件内部从 AppStorage 中访问 PersistentStorage 初始化的属性

      build() {
      Row() {
      Column() {
      Text(this.message)
      // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
      Text(`${this.aProp}`)
      .onClick(() => {
      this.aProp += 1;
      })
      }
      }
      }
      }
    • 在初始化 PersistentStorage 之前使用 AppStorage 接口访问其中的属性会使得持久化的数据丢失。(特指应用非首次运行时

      • 这里的 PersistentStorage 初始化特指 persistProp 和 persistProps。
      • 这里的 AppStorage 接口特指 setOrCreate 等修改 AppStorage 中存储的属性的 API。
      1
      2
      3
      4
      // 由于应用非首次运行时 aProp 已经是持久化属性,这里 setOrCreate 的调用会使得持久化数据丢失(上一次退出应用时的值)
      let aProp = AppStorage.setOrCreate('aProp', 47);
      // 这里的 48 对应 persisProp 的 defaultValue 参数,由于 setOrCreate 已经修改了持久化数据为 47,因此此时持久化数据 aProp 的值为 47
      PersistentStorage.persistProp('aProp', 48);
Environment - 设备环境查询
  1. 解释:Environment 是 ArkUI 框架在应用程序启动时创建的单例对象,它提供了读取系统某些环境变量(应用程序运行的设备的环境参数)的能力,并将其值写入 AppStorage 里,所以开发者需要通过 AppStorage 才能获取环境变量的值。Environment 的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型,因此组件中通过 @StorageProp 单向同步访问环境变量,程序中通过 AppStorage.prop 访问环境变量的单向同步对象。

  2. Environment 支持的参数:accessibilityEnabled、colorMode、fontScale、fontWeightScale、layoutDirection、languageCode。

  3. Environment API 语法(与 App Storage 类似,都是静态方法)

    • static envProp<S>(key: string, value: S): boolean 将 Environment 的内置环境变量 key 存入 AppStorage 中。如果系统中未查询到 Environment 环境变量 key 的值,则使用默认值 value,存入成功,返回 true。如果 AppStorage 中已经有对应的 key,则返回false。建议在程序启动的时候调用该接口

    • static envProps(props: EnvPropsOptions[]): void 和 envProp 类似,不同点在于参数为数组,可以一次性初始化多个数据。建议在应用启动时调用,将系统环境变量批量存入 AppStorage 中。

      1
      2
      3
      4
      interface EnvPropsOptions {
      key: string;
      defaultValue: number | string | boolean
      }
    • static keys(): Array<string> 返回环境变量的属性 key 的数组。

  4. 使用说明

    • Environment 在 UI 中使用 @StorageProp 访问,在应用逻辑中使用 AppStorage.prop 访问

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 将设备languageCode存入AppStorage中
      Environment.envProp('languageCode', 'en');

      @Entry
      @Component
      struct Index {
      @StorageProp('languageCode') languageCode: string = 'en';

      build() {
      Row() {
      Column() {
      // 输出当前设备的languageCode
      Text(this.languageCode)
      }
      }
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 将设备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!');
      }
    • PersistentStorage 需要在 UIAbility 中的 onWindowStageCreate 的 windowStage.loadContent 中传入的回调中调用,以确保其在 UI 实例初始化成功后调用。由于 Environment 和 UIContext 相关联,需要在 UIContext 明确的时候才可以调用。可以通过在 runScopedTask 里明确上下文,否则将导致无法查询到设备环境数据。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // EntryAbility.ets
      import { UIAbility } from '@kit.AbilityKit';
      import { window } from '@kit.ArkUI';

      export default class EntryAbility extends UIAbility {
      onWindowStageCreate(windowStage: window.WindowStage) {
      windowStage.loadContent('pages/Index');
      let window = windowStage.getMainWindow()
      window.then(window => {
      let uicontext = window.getUIContext()
      uicontext.runScopedTask(() => {
      Environment.envProp('languageCode', 'en');
      })
      })
      }
      }

其他状态管理

@Watch - 状态变量变更通知
  1. 解释:@Watch 为状态变量设置回调函数,用于监听状态变量可观察到的变化,当状态变量变化时,@Watch 指定的回调方法将被调用@Watch在 ArkUI 框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。

  2. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    @Entry
    @Component
    struct Index {
    /*
    * @Watch(callbackStr)
    * - callbackStr:必须的常量字符串,表示所修饰的状态变量更新时要执行的回调函数。类型为 (changedPropertyName? : string) => void。
    * - 可装饰变量:状态变量,如 @State、@Prop、@Link 变量等。
    * - 装饰器顺序:无顺序,建议 @State、@Prop、@Link 等装饰器在 @Watch 装饰器之前,以保持整体风格的一致。
    * - 回调触发时机:所装饰的状态变量发生变化后同步执行。
    */
    @State @Watch('onCountChange') count: number = 0;

    /*
    * (changedPropertyName? : string) => void 自定义组件的成员函数,作为 @Watch 监测状态变量变化的回调函数。
    * - 如果 @Watch 回调函数中改变了其他的状态变量,也会引起状态变更和 @Watch 的执行。
    * - 为了避免循环的产生,建议不要在 @Watch 的回调方法里修改当前装饰的状态变量。
    * - 因为 @Watch 设计的用途是为了快速的计算,所以不建议在 @Watch 函数中调用 async await,异步行为可能会导致重新渲染速度的性能问题。
    * - 在第一次初始化的时候,@Watch 回调函数不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用 @Watch 函数。
    * - 自定义组件在多个状态变量绑定同一个 @Watch 的回调方法的时候,可以通过 changedPropertyName 进行不同的
    * 逻辑处理将属性名作为字符串输入参数,不返回任何内容。
    */
    onCountChange(changedPropertyName: string) {
    console.log(`状态变量 ${changedPropertyName} 被更新为 ${this.count}`);
    }

    build() {
    Column() {
    Text(`count: ${this.count}`);
    Button(`count += 3`)
    .onClick(() => {
    this.count += 3;
    })
    }
    }
    }
  3. 使用说明

    • @Watch 回调函数调用的时机是状态变量真正变化的时间。假设父组件有两个 @State 变量 aStatebState,子组件有一个以 aState 为数据源的 @Prop/@ObjectLink 变量 cProp 和一个以 bState 为数据源的 @Link 变量 dLink,同时这四个变量都设置了相应的 @Watch 回调函数。如果此时在父组件中通过事件先后同时改变 aStatebState,那么这四个 @Watch 回调函数的执行顺序是:@Watch-aState -> @Watch-bState -> @Watch-dLink -> @Watch-cProp。这是因为,
      • @Link 的状态更新是同步的,状态变化会立刻触发 @Watch 回调。
      • @ObjectLink 的更新依赖于父组件的刷新,当父组件刷新并将更新后的变量传递给子组件时,@Watch 回调才会触发,因此触发顺序略晚于 @Link
$$ - 内置组件状态的双向同步
  1. 解释:$$ 运算符有两种功能,

    • @Builder 函数中按引用传递参数时,使用 $$ 作为参数名称。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      interface Temp {
      param1: string;
      param2: number;
      }

      @Builder function customBuilder($$: Temp) {
      Column(){
      Text(`param1: ${$$.param1}`);
      Text(`param2: ${$$.param2}`);
      }
      }
    • 使用系统内置组件时,提供基础类型变量、@State@Link@Prop 变量的引用,使得变量和系统内置组件的状态保持同步。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // xxx.ets
      @Entry
      @Component
      struct TextInputExample {
      @State text: string = ''
      controller: TextInputController = new TextInputController()

      build() {
      Column({ space: 20 }) {
      Text(this.text)
      // 这里使用 $$this.text,建立了 TextInput 组件内输入的值和 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)
      }
      }
  2. $$ 支持的系统内置组件的参数/属性

@Track - 属性级更新
  1. 解释:@Track 用于修饰 class 的实例属性。一般而言,如果 class 实例是状态变量,其一个属性的变化会触发所有属性关联的 UI 更新,但如果是 @Track 所装饰的属性发生变化,则只会触发该属性关联的 UI 更新,即 @Track 装饰器可以避免冗余更新

  2. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    class Logger {
    @Track message: string;
    @Track date: Date;

    constructor(message: string, date: Date) {
    this.message = message;
    this.date = date;
    }
    }

    @Entry
    @Component
    struct AddLog {
    /* 状态变量是 class 实例,同时其属性被 @Track 修饰,那么被 @Track 修饰的属性发生变化时,仅该属性关联的 UI 触发更新。 */
    @State logger:Logger = new Logger('-----logger-----', new Date());

    /* 这里使用 isRender 的返回值设置 Text 组件的 fontSize,从而实现对渲染的监测 */
    isRender(reRenderedPropName: string) {
    console.log(`prop ${reRenderedPropName} is re-rendered`);
    return 20;
    }

    build() {
    Row() {
    Column() {
    Text(this.logger.message)
    .fontSize(this.isRender('message'))
    .fontWeight(FontWeight.Bold)
    Text(this.logger.date.toTimeString())
    .fontSize(this.isRender('date'))
    .fontWeight(FontWeight.Bold)
    Button('change date')
    .onClick(() => {
    this.logger.date = new Date();
    })
    }
    .width('100%')
    }
    .height('100%')
    }
    }
  3. 使用注意:如果 class 类中使用了 @Track 装饰器,则未被 @Track 装饰器装饰的属性不能在 UI 中使用,如果使用,会发生运行时报错,但可以在非 UI 中使用。

    • UI 中使用:绑定在组件,初始化子组件等
    • 非 UI 中使用:事件回调函数、生命周期函数。
freezeWhenInactive - 组件冻结
  1. 解释:通过 @Component({ freezeWhenInactive: true }),可以激活组件冻结机制

    • ArkTs 中将组件分为两种状态 activeinactive
    • 开启组件冻结机制后,当组件所依赖的状态变量更新后,框架仅对处于 active 状态的组件进行更新,从而提高复杂 UI 场景下的刷新效率。
    • 当开启了组件冻结的组件状态从 inactive 变为 active 时,框架会对其执行必要的刷新操作,确保 UI 的正确展示。
  2. 组件冻结的使用场景

    场景 active inactive
    页面路由 当前栈顶页面 非栈顶不可见页面
    TabContent 当前显示的 TabContent 中的自定义组件 未显示的 TabContent 组件
    LazyForEach 当前显示的 LazyForEach 中的自定义组件 缓存节点的自定义组件
    Navigation 当前显示的 NavDestination 中的自定义组件 未显示的 NavDestination 组件
    组件复用 复用池上树的节点 进入复用池的组件
  3. 使用说明:BuilderNode 可以通过命令式动态挂载组件,而组件冻结又是强依赖父子关系来通知是否开启组件冻结。如果父组件使用组件冻结,且组件树的中间层级上又启用了 BuilderNode,则 BuilderNode 的子组件将无法被冻结

MVVM

  1. MVVM:即 Model-View-ViewModal 架构模式,其将应用分为 Model、View 和 ViewModel 三个核心部分,实现数据、视图与逻辑的分离。ArkUI 的 UI 开发模式即 MVVM 模式。

    • Model:数据 + 逻辑。
    • View:界面 + 交互。
    • ViewModal:Modal 和 View 的桥梁。
      • 一个 View 对应一个 ViewModel;
      • ViewModel 监控 Model 数据的变化,通知 View 更新 UI;
      • ViewModel 处理用户交互事件并转换为数据操作。
  2. ArkUI 开发模式

    • View:页面组件(对应某个页面)、业务组件(关联了 ViewModal 中的数据)、通用组件(没关联 ViewModal 中的数据)。
    • ViewModal:页面数据(按页面组织的数据)。
    • Model:本地(NativeC++,此时会使用非 UI 线程模型,因此需要有线程切换的能力,即切换到 UI 线程)、远端(Restful)。
  3. MVVM 文件结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    src
    |-- ets
    |---- pages 存放页面组件(页面布局,实现页面跳转,前后台事件处理等)
    |---- views 存放业务组件(被页面引用,用于构建页面)
    |---- shares 存放通用组件(多项目共享组件)
    |---- service 数据服务(按照页面组织数据 + 对每个页面的数据进行烂加载)
    |-- app.ts 服务入口
    |-- LoginViewModel 登录页 ViewModel
    |-- xxxModel 其他页 ViewModel

状态管理优秀开发实践

  1. 当子组件不需要发生本地改变时,使用 @ObjectLink 代替 @Prop 以减少不必要的深拷贝,从而减少性能开销。

  2. 不要通过状态变量强行刷新非状态变量关联组件,会导致较差的性能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @Component
    struct Index {
    @State needsUpdate: boolean = true;
    realStateArr: Array<number> = [4, 1, 3, 2];

    updateUIArr(param: Array<number>): Array<number> {
    const triggerAGet = this.needsUpdate;
    return param;
    }

    build(){
    Column({ space: 20 }) {
    /*
    当 needsUpdate 改变时,ForEach 组件刷新。间接实现了,通过 needsUpdates 强制 realStateArr 相关的组件刷新
    */
    ForEach(
    this.updateUIArr(this.realStateArr),
    (item: Array<number>) => {
    Text(`${item}`)
    })
    Text("add item")
    .onClick(() => {
    // 不会触发 UI 视图更新
    this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1);
    // 触发UI视图更新
    this.needsUpdate = !this.needsUpdate;
    })
    }
    }

    }
  3. 建议每个状态变量关联的组件数少于 20 个,从而减少不必要的组件刷新。

  4. 使用复杂对象作为状态变量时,应该控制其关联的组件数,否则会导致冗余刷新,即某个成员属性的变化会导致该对象关联的所有组件刷新,即使这些组件没有直接使用到该更改的成员属性。‼️

    注意:数组类型的状态变量也存在冗余刷新的问题,即一个元素的变化,会导致所有元素相关联的 UI 组件刷新。

    • 多组件关联同一对象的不同属性的场景中,可以利用 @State 变量只监测其第一层属性变化或直接赋值的特性,通过将复杂对象拆分为多个子对象来优化刷新性能。每个子对象适配不同的组件,这样:1️⃣ 子组件接收复杂对象的子对象,并监测该子对象第一层属性的变化或直接赋值。2️⃣ 父组件对该子对象的第一层属性变化和直接赋值也可以被监测到。这种拆分策略将复杂对象任一属性的变更导致所有关联组件刷新的情况,优化为仅变更属性对应的子对象相关组件刷新。通过以下拆分原则和实现要求,有效减少冗余刷新。

      • 拆分原则
        • 将多个作用于同一组件的属性分为一个子对象。
        • 将经常同时使用的属性分为一个子对象。
        • 相对独立或可能被多个组件使用的属性应单独拆分为一个子对象。
      • 实现要求
        • 复杂对象和子对象对应的类都需要用 @Observed 装饰。
        • 每个子对象的类都需要用 @Observed 装饰。
        • 子组件使用 @ObjectLink 变量来接收子对象。

      注意:API 11+ 时,推荐优先使用 @Track 装饰器解决该问题。

    • 多组件关联同一数据的场景中,为了精准控制组件刷新,可以采用以下两种策略,

      • 使用 @Watch 装饰器:在子组件中,通过 @Watch 装饰器可以实现更高效的 UI 刷新控制。子组件接收数据源,但不直接用于 UI 显示,而是定义一个新的 @State 变量来管理与数据源相关的 UI 刷新。设置 @Watch 回调函数来监视数据源的变化,仅在满足特定条件时才更新 @State 变量,从而触发 UI 刷新。这样,数据源的变化可以灵活地控制 UI 刷新逻辑,减少不必要的刷新,提升组件性能。子组件可以通过 @Link@Consume 接收父组件的数据,并使用 @Watch 回调来监视和控制更新,确保 UI 仅在必要时刷新。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        @Component
        struct ListItemComponent {
        @Prop item: string;
        @Prop index: number;
        @Link @Watch('onCurrentIndexUpdate') currentIndex: number; // 被多组件关联的数据
        @State color: Color =
        Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue; // 子组件自定义的 @State 变量,用于刷新与 currentIndex 相关的 UI 页面

        isRender(): number { // 用于检查 UI 有没有被刷新
        console.info(`ListItemComponent ${this.index} Text is rendered`);
        return 50;
        }

        onCurrentIndexUpdate() {
        this.color = Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue;
        }

        build() {
        Column() {
        Text(this.item)
        .fontSize(this.isRender())
        .fontColor(this.color)
        .onClick(() => {
        this.currentIndex = this.index;
        })
        }
        }
        }
      • 事件驱动更新:在复杂组件关系或跨层级的场景中,可以使用 Emitter 自定义事件发布订阅机制来实现事件驱动更新,数据源发生变化时,触发相应的事件,所有订阅该事件的组件在接收到通知后,会根据变化的具体值判断是否需要刷新组件。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        // in ButtonComponent(数据更新)
        Button(`下标是${this.value}的倍数的组件文字变为红色`)
        .onClick(() => {
        let event: emitter.InnerEvent = {
        eventId: 1,
        priority: emitter.EventPriority.LOW
        };
        let eventData: emitter.EventData = {
        data: {
        value: this.value
        }
        };
        // 发送 eventId 为 1 的事件,事件内容为 eventData
        emitter.emit(event, eventData);
        this.value++;
        })
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        // in ListItemComponent(UI 渲染,通过事件订阅回调,控制状态变量更新,从而控制 UI 渲染)
        @State color: Color = Color.Black;
        aboutToAppear(): void {
        let event: emitter.InnerEvent = {
        eventId: 1
        };
        // 收到 eventId 为 1 的事件后执行该回调
        let callback = (eventData: emitter.EventData): void => {
        if (eventData.data?.value !== 0 && this.index % eventData.data?.value === 0) {
        this.color = Color.Red;
        }
        };
        // 订阅 eventId 为 1 的事件
        emitter.on(event, callback);
        }
        build() {
        Column() {
        Text(this.myItem)
        .fontSize(this.isRender())
        .fontColor(this.color)
        }
        }
  5. 应用开发过程中,可以使用 HiDumper 查看状态变量关联的组件数,进行性能优化。

  6. 避免在 forwhile 等循环中频繁读取状态变量

  7. 需要更新状态变量的一段逻辑中,建议先使用临时变量进行结果计算,最后再将结果赋值给状态变量

  8. 在使用 ForEach 或 LazyForEach 渲染对象数组时,建议结合自定义组件进行使用。对于对象数组中的每个元素,应该将其作为自定义组件的数据源。

  9. 减少使用 LazyForEach 的重建机制来刷新 UI,建议使用状态变量控制 UI 的重新渲染。

7.3 状态管理 V2

@ComponentV2 - V2 自定义组件

1
2
3
4
5
6
7
8
9
10
11
/*
* 1. 解释:@ComponentV2 装饰器用于装饰自定义组件,使得被装饰的组件可以使用 V2 版本的状态变量装饰器。
* > 注:被 @ComponentV2 装饰器装饰的自定义组件称之为 @ComponentV2 组件。
* 2. 说明
* - @ComponentV2 与 @Component 不能同时装饰同一个自定义组件(struct)。
* - @ComponentV2 自定义组件与 @Component 自定义组件一般保持相同的行为。
* - V2 装饰器:@Local、@Param、@Once、@Event、@Provider、@Consumer 等。
* - 与 @Component 组件的比较,
* > 不支持:组件复用、LocalStorage 等;
* > 支持:组件冻结(可选的 boolean 类型参数 freezeWhenInactive)。
*/

@ObservedV2/@Trace - 类属性的深度观测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 1. 解释:@ObservedV2 装饰器与 @Trace 装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力。
* > 注:被 @ObservedV2 装饰器装饰的类称之为 @ObservedV2 类;被 @Trace 装饰器装饰的属性称之为 @Trace 属性
* 2. 可观测情况
* - @ObservedV2 类的 @Trace 属性变化时,仅会通知与该属性相关联的组件进行刷新(最小化刷新);@ObservedV2 类的非 @Trace 属性在 UI 中
* 无法感知到变化,因而也无法触发刷新。
* - @ObservedV2 类的 @Trace 属性在其子类中的修改也可以在 UI 中被观测到。
* - @ObservedV2 类的 @Trace 静态属性的修改也可以在 UI 中被观测到。
* - @ObservedV2 类的实例在 UI(组件的 build 函数中)中初始化时,@ObservedV2 类的实例的 @Trace 属性被关联到该组件上,之后实例变量如
* 果被整体赋值替换,其对应的 @Trace 属性的修改不会再引起 UI 刷新,因为最新的 @Trace 属性无法关联到该组件上。
* - @ObservedV2 类的 @Trace 内置类型的属性为了可以被观测到,除了可以直接赋值外,应使用以下可观测变化的 API,
* > Array:push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort
* > Map:set、clear、delete
* > Set:add、clear、delete
* > Date:setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear,
* setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds
* 3. 说明
* - @ObservedV2 装饰器与 @Trace 装饰器需要配合使用,单独使用 @ObservedV2 装饰器或 @Trace 装饰器没有任何作用。
* - @ObservedV2 类(及其子类)无法和 @State 等 V1 装饰器混用,如 @ObservedV2 类(及其子类)作为组件的 @State 变量的类型是非法的。
* - @ObservedV2、@Trace 不能与 @Observed、@Track 混合使用,如 @ObservedV2 类中的 @Track 属性是非法的。
* - @ObservedV2 类的实例目前不支持使用 JSON.stringify 进行序列化。
* 4. 状态管理 V1 中如何实现深度观测?@Observed + @ObjectLink + 自定义组件。(每个自定义组件中的 @ObjectLink 变量与 @State 变量类似,
* 只能观测到本身以及第一层的变化,多层嵌套对象的监测则需要多层嵌套组件)
*/

@Local - 组件内部状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
* 1. 解释:@Local 装饰器用于装饰变量,使得被装饰的变量具有观测变化的能力。
* > 注:被 @Local 装饰器装饰的变量称之为 @Local 变量。
* 2. 可观测情况
* (1) @Local 变量变化时,会刷新使用该变量的组件。
* (2) @Local 变量可观测的数据类型及对应的更改为,
* i. 简单类型(number、boolean、string):整体赋值。
* ii. 对象类型(Object、class 等):整体赋值。
* iii. 数组类型(Array):整体赋值、数组元素项的修改(多维数组的元素更改也可以被监测到‼️)。
* iv. 内嵌类型(Array、Set、Map、Date 等):整体赋值、API 调用。
* - Array:push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort
* - Map:set、clear、delete
* - Set:add、clear、delete
* - Date:setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds、setUTCFullYear、
* setUTCMonth、setUTCDate、setUTCHours、setUTCMinutes、setUTCSeconds、setUTCMilliseconds
* v. 其他类型:undefined、null、以及所有支持类型的联合类型。
* (3) @Local 和 @ObservedV2 & @Trace 的结合使用,允许对对象属性、嵌套类的对象属性、对象数组的对象属性的变化进行观测。
* 3. 使用说明
* (1) @Local 变量只允许本地初始化。
* (2) @Local 变量允许作为数据源初始化子组件的 @Param 变量。
* 4. @Local 和 @State 的对比
* (1) 语义:@Local 和 @State 都表示组件状态,但由于 @Local 只能被本地初始化,而 @State 变量允许从外部初始化,因此 @Local 装饰器更能
* 准确表达组件内部状态不能被外面修改的语义。
* (2) 父组件初始化:@Local 变量不允许外部初始化;@State 变量允许可选的外部初始化。
* (3) 可观测范围:@Local 变量只能观测变量本身,需要结合 @ObservedV2 和 @Trace 实现深度观测;@State 变量可观测变量本身及一层的成员属性,
* 无法实现深度观测。
* (4) 共同点:都没有参数;都可以作为数据源初始化子组件的状态变量,并与其保持同步。
* 5. 存在的问题
* (1) 当将复杂的常量重复赋值给 @Local 变量时,会导致刷新。这是因为在状态管理V2中,@Trace、@Local等装饰的对象(Date、Map、Set、Array)
* 被包装成代理,以便监测 API 调用引起的变化。因此,当重新赋值时,@Local 变量是 Proxy 类型,而用于赋值的常量是原始的 Date、Map、Set
* 或 Array 类型。由于类型不匹配,系统检测到不等,从而触发赋值和刷新。为了解决这个问题,可以使用 @ohos.arkui.StateManagement 提供
* 的 UIUtils.getTarget 方法获取 @Local 变量的原始对象,与用于赋值的常量进行相等后再决定是否赋值,二者相同时不执行赋值,也就不会触
* 发刷新了。
* > 状态管理 V1 中也存在复杂常量赋值给 @State 变量触发刷新的问题,但是 class 常量重新也会触发刷新,而 @Local 变量不存在该问题。
* (2) 由于 animateTo 和 V2 刷新机制暂不兼容,在 animateTo 执行动画前额外的修改并不会生效。为了让这些修改生效,可以在 animateTo 调用
* 前,使用 animateToImmediately 将额外的修改强制刷新,在使用 animateTo,从而使得动画达到预期的效果。
* ```typescript
* animateTo(value: AnimateParam, event: () => void): void // 显式动画接口,指定由于闭包代码导致的状态变化插入过渡动效。
* animateToImmediately(value: AnimateParam , event: () => void): void // 来提供显式动画立即下发功能。
* // value:动画效果相关参数;event:动效的闭包函数,在闭包函数中导致的状态变化系统会自动插入过渡动画。
* ```
*/


@Param - 组件外部输入(到内部)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* 1. 解释:@Param 装饰器用于装饰变量,使得被装饰的变量可以接收组件外部输入,并与数据源保持同步(父组件 -> 子组件)。
* > 注:被 @Param 装饰器装饰的变量称之为 @Param 变量。
* 2. 可观测情况
* (1) @Param 变量变化时,会刷新使用该变量的组件。
* (2) @Param 变量可接收的数据源:普通变量、状态变量、常量、函数返回值等
* (3) @Param 变量可观测的数据类型为及对应的修改,
* i. 简单类型(number、boolean、string):数据源整体赋值。
* ii. 对象类型(Object、class 等):数据源整体赋值。
* iii. 数组类型(Array):数据源整体赋值、数组元素项的修改(多维数组的元素更改也可以被监测到‼️)。
* iv. 内嵌类型(Array、Set、Map、Date 等):数据源整体赋值、API 调用。
* - Array: push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort
* - Set:add、clear、delete
* - Map:set、clear、delete
* - Date:setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds、setUTCFullYear、
* setUTCMonth、setUTCDate、setUTCHours、setUTCMinutes、setUTCSeconds、setUTCMilliseconds
* v. 其他类型:undefined、null、以及所有支持类型的联合类型。
* (4) 数据源的状态变量(@Local 或 @Param)和 @ObservedV2 & @Trace 的结合使用,允许对对象属性、嵌套类的对象属性、对象数组的对象属性的
* 变化进行观测。
* 3. 使用说明
* (1) @Param 变量允许本地初始化,也允许从外部初始化。本地初始化和外部初始化同时存在时,以外部初始化的值为准。
* (2) @Param 变量允许作为数据源初始化子组件的 @Param 变量。
* (3) @Param 和 @Require 可以结合使用,表示 @Param 变量只允许外部初始化。
* (4) @Param 变量的数据源是状态变量(@Local 或 @Param)时,数据源的修改会同步给 @Param 变量(父组件 -> 子组件)。
* (5) @Param 变量的数据源是复杂类型时(如类对象),@Param 变量会接收数据源的引用,此时组件中对复杂类型的修改(如类对象的属性)会同步到数
* 据源(子组件 -> 父组件)。
* > 注:@Param 变量与其父组件的数据源并不是严格单向同步。
* > 思:为了确保 @Param 的语义准确,即作为子组件的状态变量用于接收外部传入的数据,不建议对复杂类型的 @Param 变量进行修改。
* (6) @Param 变量不允许在组件内部直接修改变量本身,即整体赋值式更改。
* 4. @Param 和 @Local 的对比
* (1) @Param 表示从外部传入的状态。
* (2) @Local 表示组件内部状态。
* 5. 状态管理 V1 的可接收外部传入状态的装饰器对比
* (1) @State:初始化时获得数据源的引用,数据源改变后无法同步到子组件的 @State 变量。
* (2) @Prop:和数据源保持单向同步,但由于 @Prop 变量是数据源的深拷贝,对于复杂类型的数据源,深拷贝性能较差。
* (3) @Link:和数据源保持双向同步,但要求数据源也是状态变量。
* (4) @ObjectLink:接收数据源传递的被 @Observed 所装饰的类成员变量。
* 6. 使用场景:子组件的 @Param 变量接收父组件的 @Local 或 @Param 变量传递的数据,并与之变化保持同步。
*/

@Once - 初始化同步一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* 1. 解释:@Once 装饰器是辅助装饰器,需结合 @Param 装饰器一起装饰变量,使得被装饰的变量只允许从外部初始化一次,并不与数据源保持同步。
* > 注:被 @Once @Param 装饰器装饰的变量称之为 @Once @Param 变量。
* 2. 可观测情况:@Once @Param 变量可观测情况与 @Local 变量类似。
* 3. 使用说明
* (1) @Once 装饰器必须且只能搭配 @Param 装饰器一起使用。
* (2) @Once 装饰器对数据源的变化做拦截,数据源的变更不会同步到 @Once @Param 变量。(与 @Param 变量不同)。
* (3) @Once @Param 变量允许在组件内部直接修改变量本身,即整体赋值式更改(与 @Param 变量不同)。
* (4) 当 @Once @Param 变量的数据源是复杂类型(如类对象)时,@Once @Param 变量会接收数据源的引用。在这种情况下,组件中对复杂类型的修改
* (如类对象的属性)会同步到数据源,数据源的修改也会同步到 @Once @Param 变量(父组件 <-> 子组件)。然而,当数据源或 @Once @Param
* 变量的引用整体发生变更时,后续对类对象属性的修改将不会再进行同步。
* > 注:@Once @Param 变量与其父组件的数据源并不是严格不会同步,为了语义的准确性,个人认为,不建议对引用对象的属性进行修改。
* 4. @Once @Param 和 @Local、@Param 的对比
* (1) @Once @Param 和 @Local 变量可以在本地整体修改;@Param 变量不能在本地整体修改;
* (2) @Once @Param 和 @Param 变量允许外部初始化;@Local 变量不允许外部初始化。
* 5. 使用场景:子组件的 @Once @Param 变量仅接收父组件传递的数据源用于初始化,后续与数据源的变化不保持同步。
*/

@Event - 组件内部输出(到外部)

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* 1. 解释:@Event 装饰器用于装饰函数,向子组件提供整体修改 @Param 变量的能力。@Event 所装饰的函数由父组件提供,与子组件的 @Param 变量配
* 合实现数据的双向同步。
* > 注:被 @Event 装饰器装饰的函数称之为 @Event 函数。
* 2. 使用说明
* (1) 数据更新逻辑:@Event 调用更新父组件数据源(@Local 或 @Param),通过数据同步机制,将数据源的修改同步回子组件的 @Param 变量。
* (2) 数据更新时机:@Event 调用 --> 父组件数据源立即同步变化 --> 子组件 @Param 变量异步变化(因为子组件的值的变化由父组件决定,同时父
* 组件变化同步回子组件的过程是异步的)。
* (3) @Event 函数:默认值为 () => void;参数和返回值可以自行指定;箭头函数;可以由外部初始化,也可以自行设置默认值。
* 3. 与 @Param 变量的语义区别
* (1) @Param 变量:组件的输入,受父组件影响。
* (2) @Event 函数:组件的输出,影响父组件。
*/

@Provider/@Consumer - 跨层级双向同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* 1. 解释:@Provider 和 @Consumer 用于跨组件层级数据双向同步。@Provider 和 @Consumer 变量通过可选的变量别名进行双向绑定,变量别名默认
* 为变量名。
* > 注:被 @Provider/@Consumer 装饰器装饰的变量称之为 @Provider/@Consumer 变量。
* 2. 语法
* (1) @Provider 数据提供方:所有子组件都可以通过 @Consumer 指定相同的变量别名来和 @Provider 提供的数据建立双向绑定。
* @Provider(alias?: string) varName : varType = initValue
* (2) @Consumer 数据消费方:可以通过 @Consumer 指定相同的变量别名来和最近的父组件的 @Provider 提供的数据建立双向绑定。
* @Consumer(alias?: string) varName : varType = initValue
* 3. 可装饰类型
* (1) number、string、boolean、class、Array、Date、Map、Set 等类型;
* (2) 箭头函数。(@Consumer 变量所在组件可以通过接收的回调函数修改对应 @Provider 变量所在组件的变量,该行为可称为:父组件向子组件注册
* 回调函数 ‼️)
* 4. 使用说明
* (1) 由于 @Provider/@Consumer 变量的使用会粘合组件,为了保持组件的独立性,应减少使用,
* (2) @Provider/@Consumer 变量都禁止从父组件初始化,且必须本地初始化,找不到对应的 @Consumer/@Provider 变量时就使用本地默认值。
* (3) @Provider/@Consumer 变量都允许初始化子组件的 @Param 变量。
* (4) @Provider/@Consumer 变量的监测能力等同于 @Trace 变量,监测到的变化会同步给对应的 @Consumer/@Provider 变量。
* (5) @Consumer 变量在组件树中从父组件开始,向上寻找与之相同的变量别名的 @Provider 变量。
* 3. @Provider/@Consumer 和 @Provide/@Consume 的对比
* (1) @Consume(r) 变量本地初始化:V2 允许;V1 禁止。
* (2) 可装饰 function(箭头函数)类型:V2 允许;V1 禁止。
* (3) 可观察变更:V2 自身赋值,嵌套场景需配合 @ObservedV2/@Trace;V1 自身赋值和第一层变化,嵌套场景需配合 @Observed/@ObjectLink。
* (4) 用于匹配的 key:V2 变量别名(默认属性名为变量别名);V1 优先变量别名,其次属性名。
* (5) @Provide(r) 从父组件初始化:V2 禁止;V1 允许。
* (6) @Provide(r) 重载:V2 默认允许;V1 默认禁止,需要以 @Provide({allowOverride : '别名'}) 的形式实现重载。
* (7) @Provider/@Consumer 变量必须带括号使用。
*/

@Monitor - 状态变量监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
* 1. 解释:@Monitor 装饰器用于装饰函数,用于对指定的状态变量进行监听。
* > 注:被 @Monitor 装饰器装饰的函数称之为 @Monitor 函数。
* 2. 语法
* @Monitor("propertyName1", "propertyName2", ...) functionName(monitor: IMonitor) {}
* (1) @Monitor 参数:字符串类型的数据名。同时监听多个数据时,参数之间用逗号隔开。
* > 数据:状态变量("person")、对象属性("person.age")、多维数组中的元素("per2Arr.0.1.name")、嵌套对象("info.person.age") 或
* 对象数组的属性("per1Arr[0].name")
* (2) @Monitor 函数参数
* ```typescript
* interface IMonitor {
* dirty: Array<string>; // 发生变化的属性名构成的字符串数组。
* value<T>(path?: string): IMonitorValue<T>; // 可以根据指定属性名(path)获取到其变化信息。属性名(path)未指定时返回
* // @Monitor 函数监听顺序中第一个改变的属性的变化信息。
* }
* interface IMonitorValue<T> {
* before: T; // 指定的被监听属性变化之前的值。
* now: T; // 指定的被监听属性变化之后的值(当前值)。
* path: string // 指定的被监听属性的属性名。
* }
* ```
* (3) @Monitor 函数体的一般范式
* monitor.dirty.forEach((path: string)) => {
* const before = monitor.value(path)?.before;
* const now = monitor.value(path)?.now;
* console.log(`数据 ${path} 被更新:${before} --> ${now}`);
* }
* 3. 可监测情况
* (1) 自定义组件中的 @Monitor 函数(被监测的数据一定是状态变量或状态变量的内层数据)
* i. 监测基本类型(string、number、boolean):整体赋值。
* ii. 监测类对象:整体赋值。
* iii. 监测 @Observed 类对象:整体赋值、@Trace 属性变化。
* (2) @ObservedV2 类中的 @Monitor 函数(被监测的数据一定是 @Trace 属性或 @Trace 属性的内层数据)
* i. 监测的属性为基本类型(string、number、boolean):整体赋值。
* ii. 监测的属性为类对象:整体赋值。
* iii. 监测的属性为 @Observed 类对象:整体赋值、@Trace 属性变化。
* iv. 监测的属性为 @Observed 类的子类所继承的属性:整体赋值,@Trace 属性变化。如果父类和子类中对同一数据进行了监测,父类的
* @Monitor 函数先于子类的 @Monitor 函数执行。
* (3) 通用监测能力
* i. 监测数据为数组:整体赋值、数组项的变化(多维数组、对象数组)、数组的长度变化。
* > 注:如果对数组某一项/某一项的属性进行监测,那么数组整体赋值后,如果这一项前后没变(=== 严格比较),则对应的 @Monitor 函数就
* 不会执行。‼️
* ii. 监测数据为内置类型(Array、Map、Date、Set):不支持对 API 调用所引起的变化进行监听。‼️
* iii. 监测数据为 @Observed 类对象的 @Trace 属性:整体赋值(如果此时 @Trace 属性没有变化,@Monitor 函数不会被执行)、@Trace
* 属性变化。
* 4. 使用说明
* (1) @Monitor 函数在自定义组件中只可以监听 @Local、@Param、@Provider、@Consumer、@Computed 状态变量。
* (2) @Monitor 函数可以在自定义组件中使用,对状态变量进行监听;也可以在 @ObservedV2 类中使用,对 @Trace 属性进行监听,非 @Trace 属
* 性无法被监听。
* (3) @Monitor 函数在被监听的数据变化时执行,其采用严格相等(===)来判断数据是否变化。
* (4) @Monitor 函数在一次事件中最多只会被调用一次,‼️
* i. 当 @Monitor 函数监听单个数据时,即使在一次事件中多次更改同一数据,系统仅会比较该数据的初始值和最终值来判断是否发生变化,从而决
* 定是否执行 @Monitor 函数。
* ii. 当 @Monitor 函数监听多个数据时,即使在一次事件中多个数据都发生变化,@Monitor 回调方法最多也只会被触发一次。
* (5) @Monitor 函数可以深度监听,如嵌套类、多维数组、对象数组中成员属性变化,但此时要求变更的属性是 @ObservedV2 类中的 @Trace 属性。
* (6) @Monitor 函数可以在父子类中对同一个属性进行监听,或在父子组件中对同一个状态变量进行监听,被监听的数据变化后,父子类或父子组件中的
* @Monitor 函数都会被执行,父类/组件的 @Monitor 函数先于子类/组件的 @Monitor 函数执行。‼️
* (7) 当一个类中存在对一个属性的多次监听(多个 @Monitor 函数)时,只有最后一个定义的监听方法会生效。‼️
* (8) @Monitor 装饰器的参数可以为字符串字面量、const 常量、enum 枚举值。如果使用变量作为参数,@Monitor 函数仅会监听初始化时变量对应
* 的数据,之后对该属性的修改不会改变 @Monitor 所监测的数据。
* (9) 不要在 @Monitor 函数中更改被监听的属性,会无限循环。
* 5. @Monitor 函数的生效和失效时间
* (1) @ComponentV2 自定义组件中,
* i. 生效:所监测的状态变量初始化后
* ii. 失效:所检测的状态变量所属组件销毁时
* (2) @ObservedV2 类中,
* i. 生效:@ObservedV2 类实例化后
* ii. 失效:@ObservedV2 类实例销毁时(由于类实例的销毁依赖于垃圾回收机制,因此可能会存在类实例所在的自定义组件销毁,但是类实例还没
* 销毁,此时 @Monitor 函数不会失效 - 借助垃圾回收机制去取消 @Monitor 的监听是不稳定的)
* (3) @ObservedV2 类中失效的正确处理方式,
* i. 方式一:将 @Monitor 定义在自定义组件中。因为自定义组件在销毁时,状态管理框架会手动取消 @Monitor 的监听。(个人认为最好)
* ii. 方式二:在使用到 @ObservedV2 类的组件的 aboutToDisappear 回调中将所监听的对象置空(undefined)。
* > 假设监听的是 info.age,那么可以在组件销毁前 info = undefined。
* 6. @Monitor 和 @Watch 的对比
* (1) 参数:@Monitor 监听的数据名(状态变量名或属性名);@Watch 回调函数名。
* (2) 可监测数据数:@Monitor 多个;@Watch 一个。
* (3) 可获取监测数据变化前的值:@Monitor 支持;@Watch 不支持。
* (4) 监测能力:@Monitor 深层("a.b.c",可能需要配合 @ObservedV2/@Trace 使用);@Watch 一层。
* (5) 可用范围:@Monitor @ComponentV2 自定义组件 + @ObservedV2 类;@Watch @Component 自定义组件。
* (6) 可检测数据:@Monitor 状态变量 + @ObservedV2 类的 @Trace 属性;@Watch 状态变量。
*/

/*
* @Monitor会在状态变量初始化完成之后生效,但是此时不会立即执行,状态变量改变后再执行
* @Monitor 取消监听时,置空时所监听变量的父对象,如监听 info.age,则置空 info = undefined
*
* 参数 回调方法名。 监听状态变量名、属性名。
监听目标数 只能监听单个状态变量。 能同时监听多个状态变量。
监听能力 跟随状态变量观察能力(一层)。 跟随状态变量观察能力(深层)。
能否获取变化前的值 不能获取变化前的值。 能获取变化前的值。
监听条件 监听对象为状态变量。 监听对象为状态变量或为@Trace装饰的类成员属性。
使用限制 仅能在@Component装饰的自定义组件中使用。 能在@ComponentV2装饰的自定义组件中使用,也能在@ObservedV2装饰的类中使用。
* */


@Computed - 计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* 1. 解释:@Computed 装饰器用于装饰 getter,使得被装饰的 getter 成为状态变量,并支持对其所依赖的状态变量进行监测。
* > 注:被 @Computed 装饰器装饰的 getter 称之为计算属性。
* 2. 语法
* @Computed
* get varName() {
* // 所依赖的状态变量变更 --> 计算属性重新计算 --> 对应的 UI 刷新
* return value;
* }
* 3. 使用说明
* (1) 数据传递:禁止从父组件初始化;允许作为子组件 @Param 变量的数据源,此时存在单向同步关系(非严格,如果计算属性是 @Observed 类对象,
* 那么子组件的 @Param 变量对该类对象 @Trace 属性的修改会同步到父组件)(这里的单向同步指的是子组件不能通过父组件传递的 @Event 回
* 调修改整体赋值修改数据源)。
* (2) 触发时机:所处组件初始化时;所依赖的状态变量改变时(由于计算属性也是状态变量,计算属性可依赖计算属性的修改触发计算)。
* (3) 可修改性:只读,不允许赋值。因此,计算属性不允许使用双向绑定语法 !!。
* (4) 非法行为:改变参与计算的状态变量(不允许)。
* (5) 使用场合:自定义组件中(所依赖的状态变量改变触发计算);@Observed 类中(所以来的 @Trace 属性改变触发计算)。
* (6) 什么时候不建议使用:计算属性中是简单计算(如 a + b);计算属性在视图中只使用一次。
* (7) @Monitor 可检测性:可以使用 @Monitor 函数监测计算属性的修改。
*/

AppStorageV2 - 应用全局 UI 状态存储

  1. 解释:AppStorageV2 是应用 UI 启动时创建的单例对象,提供应用状态数据的中心存储。

    1
    import { AppStorageV2 } from '@kit.ArkUI';
  2. 语法

    • connect:创建或获取储存的数据

      1
      2
      3
      4
      5
      static connect<T extends object>(
      type: TypeConstructorWithArgs<T>, // 类型(未指定 key 时,以 type.name 作为 key)
      keyOrDefaultCreator?: string | StorageDefaultCreator<T>, // key or 默认数据的构造器
      defaultCreator?: StorageDefaultCreator<T> // 默认数据的构造器(以 keyOrDefaultCreator 指定的默认数据的构造器为准,但是 keyOrDefaultCreator 未指定或指定的默认数据的构造器非法时,则使用 defaultCreator 指定的默认数据的构造器)
      ): T | undefined; // 创建或获取数据成功,返回 T 类型的数据;否则返回 undefined
      • 如果确保数据已经存储在 AppStorageV2 中,则可省略默认构造器,获取存储的数据;否则必须指定默认构造器,不然会导致应用异常;
      • 同一个 keyconnect 不同类型的数据会导致应用异常;
      • key 由字母、数字、下划线组成,长度不超过 255。
      • 关联 @Observed 对象时,由于该类型的 name 属性未定义,需要指定 key 或者自定义 name 属性。(@ObservedV2 对象可以直接使用以获取 AppStorageV2 中的数据,其默认有 name 属性)
    • remove:删除指定 key 的储存数据

      1
      2
      3
      static remove<T>(
      keyOrType: string | TypeConstructorWithArgs<T> // 如果指定 key 创建或获取数据,则提供 key 删除数据;如果指定 type 创建或获取数据,则提供 type 删除数据(此时以 type.name 为 key)
      ): void;
      • 删除 AppStorageV2 中不存在的 key 会报警告。
    • keys:返回所有 AppStorageV2 中的 key

      1
      static keys(): Array<string>; // 所有 AppStorageV2 中的 key。
  3. 使用限制

    • 只能在 UI 线程中使用。
    • 不支持 collections.Set、collections.Map 等类型;不支持非 buildin 类型,如 PixelMap、NativePointer、ArrayList 等 Native 类型。

PersistenceV2 - UI 状态的持久化存储

  1. 解释:PersistenceV2 是应用 UI 启动时创建的单例对象,提供应用状态数据的中心存储,并将最新数据在设备磁盘上进行持久化存储。

    1
    import { PersistenceV2 } from '@kit.ArkUI';
  2. 语法

    • connect:创建或获取储存的数据(同 AppStorageV2)

    • remove:删除指定 key 的储存数据(同 AppStorageV2)

    • keys:返回所有 AppStorageV2 中的 key(同 AppStorageV2)

    • save:手动持久化数据

      1
      2
      3
      static save<T>(
      keyOrType: string | TypeConstructorWithArgs<T> // 如果指定 key 创建或获取数据,则提供 key 删除数据;如果指定 type 创建或获取数据,则提供 type 删除数据(此时以 type.name 为 key)
      ): void;
      • 手动持久化当前内存中不处于 connect 状态的 key 是无意义的
    • notifyOnError:响应序列化或反序列化失败的回调

      1
      2
      3
      static notifyOnError(
      callback: PersistenceErrorCallback | undefined // 数据序列化或反序列化失败时的回调,undefined 表示取消该回调
      ): void;
      1
      type PersistenceErrorCallback = (key: string, reason: 'quota' | 'serialization' | 'unknown', message: string) => void;
  3. 使用限制

    • 只能在 UI 线程中使用。
    • 不支持 collections.Set、collections.Map 等类型;不支持非 buildin 类型,如 PixelMap、NativePointer、ArrayList 等 Native 类型。
    • 单个 key 支持的数据大小不超过 8k,否则会导致持久化失败。
    • 持久化的数据必须是 class 对象,不能是 Array、Set、Map、Date、Number 等。
    • 不支持持久化循环引用的对象。
    • PersistenceV2 关联 @ObservedV2 对象,那么当 @Trace 属性变化时,触发整个关联对象的自动持久化;非 @Trace 属性的变化则不会,如有需求,可使用 Persistence.save 手动持久化
    • V1 状态变量、@Observed 对象、普通变量的变化都不会触发自动持久化。
    • 大量持久化数据可能会导致页面卡顿。

@Type - 类属性的类型标记

  1. 解释:@Type 装饰器用于装饰 @ObservedV2 类的属性,与 PersistenceV2 结合使用,使得类属性在序列化时不丢失类型信息,便于类的反序列化。

  2. 语法

    1
    2
    3
    4
    5
    @ObservedV2
    class className {
    @Type(propertyType)
    @Trace propertyName: PropertyType = propertyValue;
    }
    • 装饰器参数 type:被装饰的属性的类型。
    • 可装饰类型:classArrayDateMapSet 等内嵌类型。
  3. 限制条件

    • 只能用在 @ObservedV2 装饰的类中,不能用在自定义组件中。
    • 不支持 collections.Set、collections.Map 等类型;不支持非 buildin 类型,如 PixelMap、NativePointer、ArrayList 等 Native 类型;不支持简单类型,如 string、number、boolean。
    • @Type 当前不支持带参数的构造函数。

!! - 双向绑定

  1. 解释:与 $$ 类似,!! 运算符也有两种功能,

    • 父组件中使用状态变量初始化子组件 @Param 参数时,添加 !! 运算符,表示同时初始化了子组件的 @Param 变量和 @Event 函数,从而实现父子组件数据的双向绑定。注意,@Param 变量名为 varName 时,对应的 @Event 变量名必须为 $varName

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      @Entry
      @ComponentV2
      struct Index {
      @Local value: number = 0;

      build() {
      Column() {
      Text(`${this.value}`)
      Button(`change value`).onClick(() => {
      this.value++;
      })
      Star({ value: this.value!! }) // 相当于 { value: this.value, $value: (val: number) => {this.value = val} }
      }
      }
      }

      @ComponentV2
      struct Star {
      @Param value: number = 0;
      @Event $value: (val: number) => void = (val: number) => {};

      build() {
      Column() {
      Text(`${this.value}`)
      Button(`change value `).onClick(() => {
      this.$value(10);
      })
      }
      }
      }
    • 使用系统内置组件时,提供基础类型变量(支持 V1 的 @State 和 V2 的 @Local)的引用,使得变量和系统内置组件的状态保持同步

  2. !! 支持的系统内置组件的参数/属性

freezeWhenInactive - 组件冻结

@Component 的组件冻结类似,但是不支持 lazyForEach

Repeat - 可复用的循环渲染

  1. 解释:Repeat 是一种需要与容器组件配合使用的组件,对数组类型数据进行循环渲染。该组件分为以下两种模式,

    • non-virtualScroll:初始化页面时加载列表中的所有子组件。
      • 适用场景:渲染短数据列表、组件全部加载。
      • ForEach 组件的区别:针对特定数组更新场景的渲染性能进行了优化;将组件生成函数中的索引管理职责转移至框架层面。
    • virtualScroll:初始化页面时根据容器组件的**有效加载范围(可视区域+预加载区域)**加载子组件。
      • 适用场景:渲染需要懒加载的长数据列表、通过组件复用优化性能表现。
  2. 语法

    • non-virtualScroll

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      struct Index{
      build(){
      List(){ // 容器组件,这里以 List 为例
      Repeat<string>(this.simpleList) // Repeat 渲染容器组件的特定子组件,这里是 ListItem
      .each((obj: RepeatItem<string>)=>{
      ListItem(){
      Text("index: " + obj.index)
      .fontSize(30)
      ChildItem({ item: obj.item })
      .margin({bottom: 20})
      }
      })
      .key((item: string) => item)
      }
      }
      }
    • virtualScroll(特有 templatetotalCountcachedCount 属性)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      struct Index{
      build(){
      List() { // 容器组件,这里以 List 为例
      Repeat<string>(this.dataArr) // Repeat 渲染容器组件的特定子组件,这里是 ListItem
      /* ListItem 渲染的默认模版*/
      .each((ri: RepeatItem<string>) => {
      ListItem() {
      Text('each_A_' + ri.item).fontSize(30).fontColor(Color.Red)
      }
      })
      .key((item: string, index: number): string => item) // 键值生成函数
      .virtualScroll({ totalCount: this.dataArr.length }) // 打开 virtualScroll 模式,totalCount 为期望加载的数据长度
      .templateId((item: string, index: number): string => { // 根据返回值寻找对应的模板子组件进行渲染
      return index <= 4 ? 'A' : (index <= 10 ? 'B' : ''); // 前 5 个节点模板为 A,接下来的 5 个为 B,其余为默认模板
      })
      /* ListItem 渲染的 A 模版 */
      .template('A', (ri: RepeatItem<string>) => {
      ListItem() {
      Text('ttype_A_' + ri.item).fontSize(30).fontColor(Color.Green)
      }
      }, { cachedCount: 3 }) // 'A' 模板的缓存列表容量为 3
      /* ListItem 渲染的 B 模版 */
      .template('B', (ri: RepeatItem<string>) => {
      ListItem() {
      Text('ttype_B_' + ri.item).fontSize(30).fontColor(Color.Blue)
      }
      }, { cachedCount: 4 }) // 'B' 模板的缓存列表容量为 4(不可见节点,由框架暂时保存和维护,便于复用)
      }
      .cachedCount(2) // 容器组件的预加载区域大小(组件树上,可见范围外预加载的节点)
      }
      }
  3. 使用说明

    • Repeat 一般与容器组件配合使用,子组件应当是允许包含在容器组件中的子组件。例如,Repeat 与 List 组件配合使用时,子组件必须为 ListItem 组件。
    • 当 Repeat 与自定义组件或 @Builder 函数混用时,必须将 RepeatItem 类型整体进行传参,组件才能监听到数据变化,如果只传递 RepeatItem.item 或 RepeatItem.index,将会出现 UI 渲染异常。
  4. More

getTarget - 获取代理数据的原始对象

这里关注一下两个问题,

  1. 语法

    1
    2
    import { UIUtils } from '@kit.ArkUI';
    const 原始对象 = UIUtils.getTarget(被代理对象);
  2. ArkTS 中哪些数据是被代理对象?

    • V1:@Observed 装饰的类实例;状态变量装饰器(@State@Prop 等)装饰的复杂类型对象(ClassMapSetDateArray)。
    • V2:状态变量装饰器如 @Trace@Local 装饰的 DateMapSetArray

makeObserved - 将非观察数据变为可观察数据

makeObserved 就是 getTarget 的反义方法,这里关注一下几个方面,

  1. 语法

    1
    2
    import { UIUtils } from '@kit.ArkUI';
    const 可观察数据 = UIUtils.getTarget(不可观察数据);
    • 支持的参数:非空的对象类型传参。

      • 未被 @Observed@ObserveV2 装饰的类

      • ArrayMapSetDate(可以观测其 API 带来的变化)

      • collections.Array, collections.Setcollections.Map(可以观测其 API 带来的变化)

        1
        mapCollect: collections.Map<string, Info> = UIUtils.makeObserved(new collections.Map<string, Info>([['a', new Info(10)], ['b', new Info(20)]]));
      • JSON.parse 返回的 Object

        1
        message: Record<string, number> = UIUtils.makeObserved<Record<string, number>>(JSON.parse(testJsonStr) as Record<string, number>);
      • @Sendable 装饰的类

        1
        @Local send: SendableData = UIUtils.makeObserved(new SendableData());
    • 不支持的参数

      • 非空的对象类型传参 => 返回自身
      • 非 Object 类型 => 报错
      • @ObservedV2@Observed 装饰的类的实例以及已经被 makeObserved 封装过的代理数据 => 返回自身
    • makeObserved 可以在 @Component@ComponentV2 组件中使用,但是不能和 V1 状态变量装饰器配合使用,允许和 V2 状态变量装饰器配合使用(包括 @Monitor@Computed

  2. 使用场景:makeObserved 接口提供主要应用于 @ObservedV2/@Trace 无法涵盖的场景。

    • class 的定义在三方包中
    • 当前类的成员属性不能被修改(如 @Sendable 类)
    • JSON.parse 返回的匿名对象

7.4 状态管理 V1、V2 混用及迁移

8. 渲染控制

8.1 if-else 条件渲染

  1. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct componentName {
    build(){
    Column(){
    if(条件语句){
    // UI 渲染 1
    }else if(条件语句){
    // UI 渲染 2
    }else{
    // UI 渲染 3
    }
    }
    }
    }
    • 条件语句可以为:状态变量常规变量TypeScript 表达式
    • 如果条件语句中有状态变量,那么当状态变量值变化时,渲染的内容会更新
    • if 语句允许嵌套使用。
  2. 使用注意:当 if-else 分支是用于进行数据保护时,如果在动画中改变了分支对应的条件语句为空,此时该分支由于动画效果不会立即消失,从而因数据访问异常导致崩溃

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    struct Index {
    // ...

    build() {
    Column() {
    if (this.data1) {
    // 如果在动画中增加/删除,会给 Text 增加默认转场
    // 对于删除时,增加默认透明度转场后,会延长组件的生命周期,Text 组件没有真正删除,而是等转场动画做完后才删除
    Text(this.data1.str)
    .id("1")
    } else if (this.data2) {
    // 如果在动画中增加/删除,会给 Text 增加默认转场
    Text(this.data2.str)
    .id("2")
    }

    Button("play with animation")
    .onClick(() => {
    animateTo({}, ()=>{
    // 在 animateTo 中修改 if 条件,在动画当中,会给 if 下的第一层组件默认转场
    if (this.data1) {
    this.data1 = undefined; // 此时会因为访问 this.data1.str 而导致崩溃
    this.data2 = new MyData("branch 1");
    } else {
    this.data1 = new MyData("branch 0");
    this.data2 = undefined; // 此时会因为访问 this.data2.str 而导致崩溃
    }
    })
    })
    }
    }
    }
    • 解决方式一:数据判空保护。

      1
      2
      3
      4
      5
      6
      7
      if (this.data1) {
      Text(this.data1?.str) // 通过 ?. 提供判空保护,避免出错
      .id("1")
      } else if (this.data2) {
      Text(this.data2?.str) // 通过 ?. 提供判空保护,避免出错
      .id("2")
      }
    • 解决方式二:给要被删除的组件添加 .transition(TransitionEffect.IDENTITY),避免系统的默认专场动画。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      if (this.data1) {
      Text(this.data1.str)
      .transition(TransitionEffect.IDENTITY) // 避免默认转场动画
      .id("1")
      } else if (this.data2) {
      Text(this.data2.str)
      .transition(TransitionEffect.IDENTITY) // 避免默认转场动画
      .id("2")
      }

8.2 forEach 循环渲染

  1. 解释:ForEach 接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在 ForEach 父容器组件中的子组件。注意确保 ForEach 接口中的每个循环渲染项的键值都是唯一的!对于基本数据类型数组,可以将其包装为对象数组,并为每个对象指定唯一的 id 标识。

  2. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    struct componentName {
    build(){
    Column(){
    ForEach(this.arr: T[],
    (item: T, index: number): void => {
    // itemGenerator
    // item 为 this.arr[index],这里可以根据 item 进行 UI 渲染
    }, (item: T, index: number): string => {
    // keyGenerator
    // 这里需要返回一个字符串,表示循环渲染项的唯一标识,最好是 item 中的 id
    return item.id;
    })
    .onMove((from: number, to: number) => { // 通过 forEach 的 .onMove 实现拖拽排序
    const temp = this.arr.splice(from, 1);
    this.arr.splice(to, 0, temp[0]);
    })
    }
    }
    }

8.3 lazyForEach 按需循环渲染

  1. 解释:LazyForEach 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了 LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

  2. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    /* BasicDataSource 实现了 IDataSource 接口,用于管理 listener 监听,以及通知 LazyForEach 数据更新 */
    class BasicDataSource<T> implements IDataSource {
    private listeners: DataChangeListener[] = []; // LazyForEach 必须使用 DataChangeListener 对象进行更新。
    private originDataArray: T[] = [];

    public totalCount(): number { // IDataSource 接口;要被子类重写
    return 0;
    }

    public getData(index: number): T { // IDataSource 接口;要被子类重写
    return this.originDataArray[index];
    }

    // 该方法为框架侧调用,为 LazyForEach 组件向其数据源处添加 listener 监听
    registerDataChangeListener(listener: DataChangeListener): void { // IDataSource 接口
    if (this.listeners.indexOf(listener) < 0) {
    console.info('add listener');
    this.listeners.push(listener);
    }
    }

    // 该方法为框架侧调用,为对应的 LazyForEach 组件在数据源处去除 listener 监听
    unregisterDataChangeListener(listener: DataChangeListener): void { // IDataSource 接口
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
    console.info('remove listener');
    this.listeners.splice(pos, 1);
    }
    }

    /* originDataArray 变化时,需要调用下述方法通知 LazyForEach 组件 */
    /* 当 LazyForEach 数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用 listener 对应的接口,通知 LazyForEach 做相应的更新 */

    // 通知 LazyForEach 组件需要重载所有子组件
    notifyDataReload(): void {
    this.listeners.forEach(listener => {
    listener.onDataReloaded();
    })
    }

    // 通知 LazyForEach 组件需要在 index 对应索引处添加子组件
    notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
    listener.onDataAdd(index);
    // 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
    })
    }

    // 通知 LazyForEach 组件在 index 对应索引处数据有变化,需要重建该子组件
    notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
    listener.onDataChange(index);
    // 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
    })
    }

    // 通知 LazyForEach 组件需要在 index 对应索引处删除该子组件
    notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
    listener.onDataDelete(index);
    // 写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);
    })
    }

    // 通知 LazyForEach 组件将 from 索引和 to 索引处的子组件进行交换
    notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
    listener.onDataMove(from, to);
    // 写法2:listener.onDatasetChange(
    // [{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}]);
    })
    }

    notifyDatasetChange(operations: DataOperation[]): void {
    this.listeners.forEach(listener => {
    listener.onDatasetChange(operations);
    })
    }
    }

    class MyDataSource<T> extends BasicDataSource<T> {
    private dataArray: T[] = [];

    public totalCount(): number {
    return this.dataArray.length;
    }

    public getData(index: number): T {
    return this.dataArray[index];
    }

    public getAllData(): T[] {
    return this.dataArray;
    }

    // 添加数据
    public pushData(data: T): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
    }

    // 删除数据
    public deleteData(index: number): void {
    this.dataArray.splice(index, 1);
    this.notifyDataDelete(index);
    }

    // 移动数据
    public moveData(from: number, to: number): void {
    const temp: T = this.dataArray[from];
    this.dataArray[from] = this.dataArray[to];
    this.dataArray[to] = temp;
    this.notifyDataMove(from, to);
    }

    // 移动数据(该回调仅适用于 lazyForEach 的 onMove 实现的拖拽排序所引发的数据移动)
    public moveDataWithoutNotify(from: number, to: number): void {
    const temp: T = this.dataArray[from];
    this.dataArray[from] = this.dataArray[to];
    this.dataArray[to] = temp;
    }

    // 改变数据
    // 如果 data 是一个对象,且修改的是 data 中的一个属性时,同时循环渲染项是比较复杂的 UI 结构,更高效的渲染更新方式是,
    // - 让 data 是 @Observed/@ObservedV2 对象,并将其传递给一个自定义组件,这个组件以 data 为数据源在 lazyForEach 中循环渲染,封装了复杂的 UI 结构
    // - 结合 @ObjectLink/@Trace 实现自定义组件中对 data 属性变化的监测,当 data 的属性修改后,直接进行对应的 UI 渲染,不走 lazyForEach 的逻辑
    public changeData(index: number, data: T): void {
    this.dataArray.splice(index, 1, data);
    this.notifyDataChange(index);
    }

    // 重载数据(当 this.dataArray 全部数据改变时调用该方法,这里只有同时没有数据操作)
    // 如果重载数据时 keyGenerator 的值不改变,则不会导致任何页面刷新
    public reloadData(): void {
    this.notifyDataReload();
    }

    // 精准批量修改数据
    // 传入 onDatasetChange 的 operations,其中每一项 operation 的 index 均从【修改前的原数组】内寻找。因此,operations中的 index 跟操作 Datasource 中的 index 不总是一一对应的,而且不能是负数。
    // 调用一次 onDatasetChange,一个 index 对应的数据只能被操作一次,若被操作多次,LazyForEach 仅使第一个操作生效。
    // 若本次操作集合中有 RELOAD 操作,则其余操作全不生效。
    pulic operateData(): void {
    this.dataArray.splice(4, 0, this.dataArray[1]);
    this.dataArray.splice(1, 1);
    let temp = this.dataArray[4];
    this.dataArray[4] = this.dataArray[6];
    this.dataArray[6] = temp
    this.dataArray.splice(8, 0, 'Hello 1', 'Hello 2');
    this.dataArray.splice(12, 2);
    this.notifyDatasetChange([
    { type: DataOperationType.MOVE, index: { from: 1, to: 3 } },
    { type: DataOperationType.EXCHANGE, index: { start: 4, end: 6 } },
    { type: DataOperationType.ADD, index: 8, count: 2 },
    { type: DataOperationType.DELETE, index: 10, count: 2 }]);
    }

    }

    @Entry
    @Component
    struct MyComponent {
    private data: MyDataSource<string> = new MyDataSource<string>();

    aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
    this.data.pushData(`Hello ${i}`)
    }
    }

    build() {
    List({ space: 3 }) {
    LazyForEach(
    this.data, // 继承了实现了 IDataSource 的数据
    (item: string, index: number) => { // itemGenerator
    ListItem() {
    // ...
    }
    },
    (item: string, index: number) => item.id) // keyGenerator
    }
    .onMove((from: number, to: number) => { // 通过 lazyForEach 的 .onMove 实现拖拽排序,此时修改数据源不需要调用 DataChangeListenr 的 onDataMove 通知数据源发生了数据移动
    this.moveDataWithoutNotify(from, to);
    })
    .cachedCount(5) // 表示只加载可视部分以及其前后少量数据用于缓冲
    }
    }
    • 删除失败:当删除数据时,仅调用 this.data.deleteData 可能并不会删除该数据对应的组件。因为删除某一个数据后,该数据的后续数据对应的组件仍然使用最初分配的 index,即组件对应的 itemGeneratorindex 没有发生变化,因此删除结果可能会与预期不符合。解决方式为,

      1
      2
      this.data.deleteData(index);
      this.data.reloadData(); // 增加该方法调用,用于重建该数据的后续数据项,以达到更新 index 的目的。同时为了确保重建的是该数据的后续数据项,需要使用 (item: T, index: number) => item.id + index.toString(); 作为数据项的键值。否则,重建的就是全部的数据项了。
    • List 闪烁:在 List 的 onScrollIndex 方法中调用 onDataReloaded 有屏幕闪烁的风险,如

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      // 假设 MyDataSource 中定义了 operateData 方法为
      class MyDataSource<T> extends BasicDataSource<T> {
      public operateData(): void{
      const totalCount = this.dataArray.length;
      const batch=5;
      for (let i = totalCount; i < totalCount + batch; i++) {
      this.dataArray.push(`Hello ${i}`)
      }
      this.notifyDataReload(); // 触发 lazyForEach 的数据源的重新加载
      }
      }

      // 假设 List 组件的 .onScrollIndex 的使用如下
      List(){
      // ...
      }
      .onScrollIndex((start, end, center) => {
      if(end === this.data.totoalCount() - 1){
      console.log('scroll to end');
      this.data.operateData();
      }
      })

      解决方式为,将 this.notifyDataReload(); 更改为 this.notifyDatasetChange([{type:DataOperationType.ADD, index: totalCount-1, count:batch}])