📖学习 ArkTS 语言
注:以下内容是对鸿蒙开发文档的入门章节的 ArkTS 部分的学习整理,仅供参考。
1. ArkTS 概述
-
ArkTS
- HarmonyOS 的应用开发语言
- 围绕应用开发在 TypeScript 基础上的进一步扩展
-
ArkTS Vs. TypeScript
-
强制使用静态类型
-
取消动态类型特性(如 any、unknown 类型的使用等)
-
禁止在运行时改变对象布局
-
限制运算符语义
-
不支持结构类型
-
与 JavaScript 无缝互通
-
支持 ArkUI 框架的声明式语法和其他特性(如自定义组件、状态管理、条件渲染、循环渲染等)
-
2. ArkTS 编程规范
规则分为两个级别:要求、建议。
2.1 代码风格
-
标识符命名【建议】
标识符 命名规则 示例 类名、枚举名、命名空间名 UpperCamelCase Worker 变量名、方法名、参数名 lowerCamelCase sendMsg 常量名、枚举值 全部大写,单词间使用下划线隔开 MAX_USER_SIZE 布尔变量名 避免使用否定的布尔变量名 isError、isFound -
代码格式
-
使用空格缩进,禁止使用 tab 字符(可以在 IDE 或代码编辑器中配置,将 Tab 键自动扩展为 2 个空格)【建议】
-
行宽不超过 120 个字符(除非命令、URL、error 信息等)【建议】
-
条件语句和循环语句的实现必须使用大括号(即使条件体/循环体只有一行代码)【建议】
-
表达式换行需保持一致性,运算符放行末【建议】
1
2
3
4if (userCount > MAX_USER_COUNT ||
userCount < MIN_USER_COUNT) {
doSomething();
} -
多个变量定义和赋值语句不允许写在一行【要求】
1
2
3
4
5
6
7
8let 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 编程实践
- 建议添加类属性的可访问修饰符【建议】
- 在 ArkTS 中,提供了 private, protected 和 public 可访问修饰符。
- 默认情况下一个属性的可访问修饰符为 public。
- 如果类中包含 private 属性,无法通过对象字面量初始化该类。
- 不建议省略浮点数小数点前后的 0【建议】
- 判断变量是否为 Number.NaN 时必须使用 Number.isNaN() 方法【要求】
- 数组遍历优先使用 Array 对象方法【要求】
- 对于数组的遍历处理,应该优先使用 Array 对象方法,如:forEach(), map(), every(), filter(), find(), findIndex(), reduce(), some()。
- 不要在控制性条件表达式中执行赋值操作【建议】
- 在 finally 代码块中,不要使用 return、break、continue 或抛出异常,避免 finally 块非正常结束【要求】
- 非正常结束的 finally 代码块会影响 try 或 catch 代码块中异常的抛出,也可能会影响方法的返回值。
- 所以要保证 finally 代码块正常结束。
- 避免使用 ESObject【建议】
- ESObject 主要用在 ArkTS 和 TS/JS 跨语言调用场景中的类型标注,在非跨语言调用场景中使用 ESObject 标注类型,会引入不必要的跨语言调用,造成额外性能开销。
- 使用 T[] 表示数组类型【建议】
3. TS 到 ArkTS 的适配规则
约束分为两个级别:错误、警告。
- 错误:必须要遵从的约束,否则会导致程序编译失败。
- 警告:推荐遵从的约束,未来可能会导致程序编译失败。
3.1 约束概述
- 强制使用静态类型:ArkTS 中禁止使用 any 类型,有如下优势,
- 开发者能够容易理解代码中使用了哪些数据结构
- 减少运行时的类型检查,有助于提升性能
- 禁止在运行时变更对象布局:ArkTS 禁止以下行为,
- 向对象中添加新的属性或方法。
- 从对象中删除已有的属性或方法。
- 将任意类型的值赋值给对象属性。
- 限制运算符的语义:如一元运算符
+
只能作用于数值类型。 - 不支持结构类型(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 | 使用具体的类型而非 any 或 unknown |
错误 | 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 类型(如 number 或 boolean )转换成引用类型时,请使用 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 语句中,只能标注 any 或 unknown 类型。由于 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 | 不支持生成器函数 | 错误 | 改用使用 async 或 await 机制进行并行任务处理。 |
arkts-no-is | 使用 instanceof 和 as 进行类型保护 |
错误 | 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 | 不支持 require 和 import 赋值表达式 |
错误 | 如 import m = require('mod') 就使用了 require 和 import 赋值表达式。 |
arkts-no-export-assignment | 不支持 export = ... 语法 |
错误 | 改用常规的 export 或 import 。 |
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.target 为 undefined 。 |
arkts-no-definite-assignment | 不支持确定赋值断言 | 警告 | 改为在声明变量的同时为变量赋值。如 let x!: number 就使用了确定赋值断言。 |
arkts-no-prototype-assignment | 不支持在原型上赋值 | 错误 | ArkTS 没有原型的概念,因此不支持在原型上赋值。此特性不符合静态类型的原则。 |
arkts-no-globalthis | 不支持 globalThis |
警告 | 由于 ArkTS 不支持动态更改对象的布局,因此不支持全局作用域和 globalThis。 |
arkts-no-utility-types | 不支持一些 utility 类型 | 错误 | ArkTS 仅支持 Partial 、Required 、Readonly 和 Record ,不支持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.apply 和 Function.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 代码更改
-
arkts-identifiers-as-prop-names
1
2
3
4
5
6
7
8
9
10
11
12
13let 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']
} -
arkts-no-any-unknown
1
2
3
4
5function printProperties(obj: any) { }
// 更改为
function printProperties(obj: Record<string, Object>) { } -
arkts-no-call-signature
1
2
3
4
5
6
7interface I {
(value: string): void;
}
// 更改为
type I = (value: string) => void -
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
23type 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);
}
} -
arkts-no-indexed-signatures
1
2
3
4
5function foo(data: { [key: string]: string }) { }
// 更改为
function foo(data: Record<string, string>) { } -
arkts-no-typing-with-this
1
2
3
4
5
6
7
8
9
10
11
12
13class C {
getInstance(): this {
return this;
}
}
// 更改为
class C {
getInstance(): C {
return this;
}
} -
arkts-no-inferred-generic-params
1
2
3
4
5let 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])); -
arkts-no-regexp-literals
1
2
3
4
5let regex: RegExp = /\s*/g;
// 更改为
let regexp: RegExp = new RegExp('\\s*','g'); // 在字符串中需要对反斜杠进行转义,因此这里使用了双反斜杠 \\。 -
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
-
arkts-no-obj-literals-as-types
1
2
3
4
5type Person = { name: string, age: number }
// 更改为
interface Person { name: string, age: number } -
arkts-no-mapped-types
1
2
3
4
5
6
7
8
9
10
11
12class C {
a: number = 0
b: number = 0
c: number = 0
}
type OptionsFlags = {
[Property in keyof C]: string
}
// 更改为
type OptionsFlags = Record<keyof C, string> -
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'); -
arkts-no-special-imports
1
2
3
4
5import type {A, B, C, D } from '***'
// 更改为
import {A, B, C, D } from '***' -
arkts-no-side-effects-imports
1
2
3
4
5import 'module'
// 更改为
import('module')
4.2 拷贝实现
-
浅拷贝
1
2
3
4
5
6
7function shallowCopy(obj: object): object {
let newObj: Record<string, Object> = {};
for (let key of Object.keys(obj)) {
newObj[key] = obj[key];
}
return newObj;
} -
深拷贝
1
2
3
4
5
6
7
8
9
10
11function 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 编程实践 - 高性能
- 使
const
声明不变的变量。 number
类型变量避免整型和浮点型混用。- 数值计算避免溢出
- 加法、减法、乘法、指数运算等:避免大于 INT32_MAX 或小于 INT32_MIN。
- &(and)、>>>(无符号右移)等:避免大于 INT32_MAX。
- 循环中提取常量,减少属性访问次数。
- 建议使用参数传递函数外的变量。
- 避免使用可选参数,改用必选参数并考虑参数默认值的使用。
- 数值数组推荐使用 TypedArray。
- 避免使用稀疏数组。
- 避免使用联合类型数组。
- 避免频繁抛出异常。
6. UI 范式基本语法
6.1 ArkTS 的基本组成
- 装饰器: 用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如
@Component
表示自定义组件,@Entry
表示该自定义组件为入口组件,@State
表示组件中的状态变量,状态变量变化会触发 UI 刷新。 - UI 描述:以声明式的方式来描述 UI 的结构,例如
build()
方法中的代码块。 - 自定义组件:可复用的 UI 单元,可组合其他组件。
- 系统组件:ArkUI 框架中默认内置的基础和容器组件,可直接被开发者调用,比如
Column
、Text
、Divider
、Button
。 - 属性方法:组件可以通过链式调用配置多项属性,如
fontSize()
、width()
、height()
、backgroundColor()
等。 - 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在
Button
后面的onClick()
。 - 扩展语法
- @Builder/@BuilderParam:特殊的封装 UI 描述的方法,细粒度的封装和复用 UI 描述。
- @Extend/@Styles:扩展内置组件和封装属性样式,更灵活地组合内置组件。
- stateStyles:多态样式,可以依据组件的内部状态的不同,设置不同样式。
6.2 声明式 UI 描述
-
组件创建
1
2
3
4
5
6// 无参数
Divider()
// 有参数(参数可以是常量、变量或表达式)
Image('https://xyz/test.jpg')
Image('https://' + this.imageUrl) -
属性配置:以
“.”
链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行。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) -
事件配置:以
“.”
链式调用的方式配置系统组件支持的事件,建议每个事件方法单独写一行。1
2
3
4Button('Click me')
.onClick(() => {
this.myText = 'ArkUI';
}) -
子组件配置:在尾随闭包
"{...}"
中为组件添加子组件的 UI 描述。1
2
3
4
5
6
7
8Column() {
Text('Hello')
.fontSize(100)
Divider()
Text(this.myText)
.fontSize(100)
.fontColor(Color.Red)
}只有容器组件才支持子组件配置。
6.3 自定义组件
组件语法
-
组件:在 ArkUI 中,UI 显示的内容均为组件,由框架直接提供的称为系统组件;由开发者定义的称为自定义组件,其具有以下特点,
-
可组合:组合使用系统组件、及其属性和方法。
-
可重用:可以被其他组件重用。
通过
export
导出该自定义组件,并在使用的页面通过import
导入该自定义组件,并在build()
函数中多次创建,实现自定义组件的重用。 -
数据驱动 UI 更新:状态变量的改变,驱动 UI 刷新。
-
-
语法
1
2
3
4
5
6
7
8
struct 自定义组件名 {
build() {
// 自定义组件的 UI 描述
}
}-
struct
:关键字,通过struct 自定义组件名 {...}
构成自定义组件。- 自定义组件名、类名、函数名不能和系统组件名相同。
-
@component
:该装饰器仅能装饰struct
关键字声明的数据结构,使其具备组件化的能力,需要实现build
方法描述 UI。-
从 API version 11 开始,
@Component
可以接受一个可选的bool
类型参数freezeWhenInactive
,表示是否开启组件冻结。1
2
3freezeWhenInactive: true }) ({
struct MyComponent {
}
-
-
build()
:函数,定义自定义组件的声明式 UI 描述。 -
@Entry
:该装饰器装饰的自定义组件将作为 UI 页面的入口。-
在单个 UI 页面中,最多可以使用
@Entry
装饰一个自定义组件。 -
从API version 10开始,@Entry 可以接受一个可选的
options
参数。该参数结构为,1
2
3
4
5
6type options = LocalStorage | EntryOptions;
interface EntryOptions {
routerName?: string, // 表示作为命名路由页面的名字。
storage?: LocalStorage, // 页面级的 UI 状态存储。
useSharedStorage?: boolean // 是否使用 LocalStorage.getShared() 接口返回的 LocalStorage 实例对象,默认值 false。该字段的优先级高于 storage 字段。
}1
2
3
4'myPage' }) ({ routeName :
struct MyComponent {
}
-
-
@Resuable
:该装饰器装饰的自定义组件具备可复用能力。
-
-
成员函数/变量
- 约束:自定义组件的成员函数/变量为私有的,且不建议声明成静态函数。
- 初始化:自定义组件的成员变量可以本地初始化,也可以从父组件通过参数传递初始化。
-
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
struct ParentComponent {
doSomeCalculations() {
}
doSomeRender() {
Text(`Hello World`)
}
build() {
Column() {
// 反例:不能调用没有用@Builder装饰的方法
this.doSomeCalculations();
// 正例:可以调用
this.doSomeRender();
}
}
} -
不允许使用
switch
语法,改用if
。 -
不允许使用表达式,改用
if
。 -
不允许直接改变状态变量。因为 ,所以不能在自定义组件的
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// 反例
Array<...> = [ ... ]; arr :
ForEach(this.arr.sort().filter(...),
item => {
...
})
// 正确的执行方式为:filter返回一个新数组,后面的sort方法才不会改变原数组this.arr
ForEach(this.arr.filter(...).sort(),
item => {
...
})
-
-
-
通用样式:自定义组件通过
“.”
链式调用的形式设置通用样式。其本质上是给自定义组件套了一个不可见的容器组件,而这些样式是设置在容器组件上的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ChildComponent {
build() {
Button(`Hello World`)
}
}
struct MyComponent {
build() {
Row() {
ChildComponent()
.width(200)
.height(300)
.backgroundColor(Color.Red)
}
}
}
生命周期
- 自定义组件:
@Component
装饰的 UI 单元,可以组合多个系统组件实现 UI 的复用,可以调用组件的生命周期。 - 页面:应用的 UI 页面。可以由一个或者多个自定义组件组成,
@Entry
装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry
。只有被@Entry
装饰的组件才可以调用页面的生命周期。

组件生命周期
组件生命周期,即 @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 | // xxx.ets |
属性访问限定符的使用限制
当属性访问限定符的使用违反以下限制时,ArkTS 会产生告警日志。
变量类型 | 初始化限制 |
---|---|
private /@StorageLink /@StorageProp /@LocalStorageLink /@LocalStorageProp /@Consume 变量 |
被外部初始化 ❌ 使用本地值进行初始化 ✅ |
@State /@Prop /@Provide /@BuilderParam /常规成员变量(不涉及更新的普通变量) |
被外部初始化 ✅ 使用本地值进行初始化 ✅ |
@Link /@ObjectLink /@Require 变量 |
被外部初始化 ✅ 使用本地值进行初始化 ❌ |
由于
struct
没有继承能力,上述所有的这些变量使用protected
修饰时,会有编译告警日志提示。
6.4 装饰器(UI 描述复用)
@Builder 装饰器 - 自定义构建函数
-
解释:
@Builder
装饰器是 ArkUI 提供的一种轻量的 UI 元素复用机制,开发者可以将重复使用的 UI 元素抽象成一个被@Builder
所装饰的方法,以在build
方法里调用。@Builder
装饰的函数也称为“自定义构建函数”。 -
语法 - 定义及使用
-
私有自定义构建函数:在组件内部定义的被
@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
struct BuilderDemo {
message: string = "Hello World";
// 私有自定义函数(无参数) - 定义
showTextBuilder() {
Text(this.message)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
// 私有自定义函数(有参数) - 定义
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// 全局自定义函数(无参数) - 定义
function showTextBuilder() {
Text('Hello World')
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
// 全局自定义函数(有参数) - 定义
function showTextValueBuilder(param: string) {
Text(param)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
struct BuilderDemo {
build() {
Column() {
// 全局自定义函数(无参数) - 使用
showTextBuilder()
// 全局自定义函数(有参数) - 使用
showTextValueBuilder('Hello @Builder')
}
}
}- 如果不涉及组件状态变化,建议使用全局的自定义构建方法。
- 全局自定义构建函数允许在
build
方法和其他自定义构建函数中调用。
-
-
语法 - 参数传递
-
按值传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function overBuilder(paramA1: string, param2: boolean) {
Row() {
Text(`UseStateVarByValue: ${paramA1}, ${param2}`)
}
}
struct Parent {
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
28interface Tmp {
paramA1: string;
}
function overBuilder(params: Tmp) {
Row() {
Text(`UseStateVarByReference: ${params.paramA1} `)
}
}
struct Parent {
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';
})
}
}
} -
使用说明
- 参数的类型必须与参数声明的类型一致,不允许
undefined
、null
和返回undefined
、null
的表达式。 @Builder
修饰的函数内部不允许改变参数值,否则会框架会抛出运行时错误。@Builder
修饰的函数内部的 UI 语法遵循 UI 语法规则。- 只有传入一个参数,且参数需要直接传入对象字面量才会按引用传递该参数,其余传递方式均为按值传递。‼️
- 按引用传递参数时,传递的参数可为状态变量,此时状态变量的改变会引起
@Builder
修饰的函数内的 UI 刷新。 - 按引用传递参数时,ArkUI 提供
$$
作为按引用传递参数的范式。
- 参数的类型必须与参数声明的类型一致,不允许
-
@LocalBuilder 装饰器 - 维持组件父子关系
-
解释:
@LocalBuilder
装饰器在私有@Builder
的基础上,解决了组件的父子关系和状态管理的父子关系保持一致的问题。在父组件中定义的函数,如果被@Builder
装饰并传递给子组件调用,那么函数体中的this
会指向子组件。然而,如果函数被@LocalBuilder
装饰,那么函数体中的this
始终指向父组件,此时组件的父子关系和状态管理的父子关系始终保持一致。 -
语法 - 定义及使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Parent {
label: string = 'Hello';
MyBuilderFunction() {
// ...
} // @LocalBuilder 函数的定义
build() {
Column() {
this.MyBuilderFunction(); // @LocalBuilder 函数的使用
}
}
}@LocalBuilder
函数的定义及使用注意同[私有自定义构建函数](#@Builder 装饰器 - 自定义构建函数)。@LocalBuilder
函数只能在所属组件内声明,不允许全局声明。@LocalBuilder
装饰器不能与其他内置装饰器或自定义装饰器一起使用。@LocalBuilder
不能装饰自定义组件内的静态方法。
-
语法 - 参数传递:分为按值传递和按引用传递。
@LocalBuilder
函数的参数传递语法和注意同[自定义构建函数](#@Builder 装饰器 - 自定义构建函数)。- 若子组件调用父组件的
@LocalBuilder
函数,传入的参数发生变化,不会引起@LocalBuilder
方法内的 UI 刷新。‼️【文档描述不清】
- 若子组件调用父组件的
-
@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
48class LayoutSize {
size: number = 0;
}
struct Parent {
label: string = 'parent';
layoutSize: LayoutSize = { size: 0 };
/*
* 函数被 @LocalBuilder 修饰,函数体 this 指向父组件(或者说定义的组件),
* 函数被 @Builder 修饰,函数体 this 指向子组件(或者说调用的组件)
*/
// @Builder
componentBuilder($$: LayoutSize) {
Text(`${'this :' + this.label}`);
Text(`${'size :' + $$.size}`);
}
build() {
Column() {
Child({ contentBuilder: this.componentBuilder });
}
}
}
struct Child {
label: string = 'child';
contentBuilder: ((layoutSize: LayoutSize) => void); // 父组件传递的 @LocalBuilder / @Builder 函数
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 函数
-
解释:
@BuilderParam
装饰器用于装饰指向@Builder
函数的变量。这里的
@Builder
函数也包括@LocalBuilder
函数。 -
语法 - 变量初始化:被
@BuilderParam
所装饰的变量只允许通过@Builder
函数进行初始化。-
本地初始化:私有或全局自定义构建函数进行初始化。
1
2
3
4
5
6
7
8
9
10
11function overBuilder() {}
struct Child {
doNothingBuilder() {};
// 私有自定义构建函数初始化 @BuilderParam 变量
customBuilderParam: () => void = this.doNothingBuilder;
// 全局自定义构建函数初始化 @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
struct Child {
customBuilder() {};
customBuilderParam: () => void = this.customBuilder;
build() {
Column() {
this.customBuilderParam()
}
}
}
struct Parent {
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
14build() {
Column() {
// Child() 后紧跟的 {} 形成尾随闭包,作为参数传递给 @BuilderParam customBuilderParam: () => void
Child(){
Column(){
Text(`Parent builder `); // 这里的 this 始终指向父组件
}
.backgroundColor(Color.Yellow)
.onClick(()=>{
this.text = 'changeHeader';
})
}
}
}
-
被
@BuilderParam
所装饰的变量的类型可以声明为() => void
。 -
-
@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
struct Child {
label: string = `Child`;
customBuilder() {};
customChangeThisBuilder() {};
customBuilderParam: () => void = this.customBuilder;
customChangeThisBuilderParam: () => void = this.customChangeThisBuilder;
build() {
Column() {
this.customBuilderParam()
this.customChangeThisBuilderParam()
}
}
}
struct Parent {
label: string = `Parent`;
componentBuilder() {
Text(`${this.label}`)
}
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
-
解释:
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);
} -
语法
1
2
3let builderVar: WrappedBuilder<[string, number]> = wrapBuilder(MyBuilder); // builderVar.build('hello', 123) 的方式复用 UI 描述
let builderArr: WrappedBuilder<[string, number]>[] = [wrapBuilder(MyBuilder)]; // builderArr[0].build('hello', 123) 的方式复用 UI 描述 -
注意事项
wrapBuilder
方法只能以全局@Builder
函数作为参数。WrappedBuilder
对象的builder()
属性方法只能在struct
内部使用。- 无法重复定义
WrappedBuilder
变量,意思是一旦将一个变量或对象的属性赋值为WrappedBuilder
对象,后续的再赋值则不会起作用,只生效第一次定义的WrappedBuilder
。
6.5 装饰器(样式)
@Style - 定义组件重用样式
-
解释:
@Style
装饰器所修饰的方法包含多条样式,可以在组件声明的位置直接调用。@Styles
方法不能有参数。@Styles
方法内不支持使用逻辑组件,如if
。逻辑组件内的属性不生效。@Styles
只能在当前文件内使用,不支持export
。 推荐使用AttributeModifier
实现export
功能。
-
语法 - 定义
-
组件
@Style
1
2
3
4
5
6
struct FancyUse {
functionName() {
/* ... */
}
}- 组件内的
@Styles
可以通过this
访问组件的常量和状态变量。 - 组件内的
@Styles
的优先级高于全局@Styles
。
- 组件内的
-
全局
@Style
1
function functionName() { /* ... */ }
-
-
使用示例
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 封装的样式
function globalFancy () {
.width(150)
.height(100)
.backgroundColor(Color.Pink)
}
struct FancyUse {
heightValue: number = 100;
// 定义在组件内的 @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 - 定义扩展组件样式
-
解释:
@Extend
装饰器所修饰的方法也包含多条样式,可以在组件声明的位置直接调用。@Styles
装饰器是为了实现样式的重用,而Extend
装饰器是用于扩展原生组件的样式。 -
语法
1
UIComponentName) function functionName { ... } (
@Extend
函数支持封装指定组件的私有属性、私有事件和自身定义的全局方法。@Extend
装饰的函数支持参数,参数可以为function
,作为 Event 事件的句柄;参数也可以为状态变量,状态变量改变会触发 UI 的渲染。@Extend
仅支持在全局定义,不支持在组件内部定义。@Extend
只能在当前文件内使用,不支持export
。 推荐使用AttributeModifier
实现export
功能。
-
使用示例
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 组件扩展的样式
Text) (
function fancyText(weightValue: number, color: Color) {
.fontStyle(FontStyle.Italic)
.fontWeight(weightValue)
.backgroundColor(color)
}
struct FancyUse {
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 - 多态样式
-
解释:
stateStyles
是组件的属性方法,可以依据组件的内部状态的不同,快速设置不同样式,即支持多态样式。 -
语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17.stateStyles({
focused: { // 获焦态(仅支持通过外接键盘的 tab 键、方向键触发)
/* ... */
},
normal: { // 正常态
/* ... */
},
pressed: { // 按压态
/* ... */
},
disabled: { // 不可用态
/* ... */
},
selected: { // 选中态
/* ... */
}
})focused
、normal
、pressed
、disabled
、selected
字段的取值不仅可以是样式,也可以是@Styles
函数、组件内的常规变量和状态变量。
-
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct CompWithInlineStateStyles {
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 - 动态、跨文件样式
-
解释:
AttributeModifier
是 ArkUI 引入的新的样式机制,支持通过Modifier
对象动态修改属性,与@Styles
和@Extend
相比,AttributeModifier
支持跨文件导出、多态样式和业务逻辑等。 -
语法
-
自定义指定组件的
AttributeModifier
1
2
3
4
5
6
7
8
9
10
11
12
13declare 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
3declare class CommonMethod<T> {
attributeModifier(modifier: AttributeModifier<T>): T;
}
-
-
使用示例
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'
struct attributeDemo {
modifier: MyButtonModifier = new MyButtonModifier();
build() {
Row() {
Column() {
Button("Button")
.attributeModifier(this.modifier)
}
.width('100%')
}
.height('100%')
}
}
6.6 装饰器(其他)
@AnimatableExtend - 定义可动画属性
-
解释:
@AnimatableExtend
装饰器所修饰的函数可以变为组件的属性方法。通过这个属性方法,我们可以将组件的不可动画属性转变为可动画属性。在动画执行期间,利用逐帧回调函数,在每一帧中计算并调整不可动画属性的值,使这些属性也能够实现动画效果。此外,还可以在该属性方法中修改组件的可动画属性,从而实现逐帧的布局效果。- 可动画属性:如果一个属性方法在
animation
属性方法前调用,改变这个属性的值可以使animation
属性的动画效果生效,这个属性称为可动画属性。比如height
、width
等。 - 不可动画属性:如果一个属性方法在
animation
属性方法前调用,改变这个属性的值不能使animation
属性的动画效果生效,这个属性称为不可动画属性。比如 Polyline 组件的points
属性等。
- 可动画属性:如果一个属性方法在
-
语法
1
2
3UIComponentName) function functionName(value: typeName) { (
.propertyName(value)
}@AnimatableExtend
函数仅支持定义在全局,不支持在组件内部定义。@AnimatableExtend
函数的参数类型必须 为number
类型或者实现AnimatableArithmetic<T>
接口的自定义类型。为了让不可动画属性变得可动画,则需要让对应的数据结构(如数组、结构体、颜色等)实现AnimatableArithmetic<T>
接口后,再定义@AnimatableExtend
函数。@AnimatableExtend
函数体内只能调用@AnimatableExtend
括号内组件UIComponentName
的属性方法。
-
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23Text) (
function animatableWidth(width: number) {
.width(width)
}
struct AnimatablePropertyExample {
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 - 校验构造传参
-
解释:
@Require
装饰器装饰@Prop
、@State
、@Provide
、@BuilderParam
和普通变量(无状态装饰器修饰的变量),校验这些变量是否需要构造传参。 -
使用示例
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
struct Index {
message: string = 'Hello World';
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
})
}
}
}
struct Child {
buildFunction() {
Column() {
Text('initBuilderParam')
.fontSize(30)
}
}
regular_value: string = 'Hello';
state_value: string = "Hello";
provide_value: string = "Hello";
buildTest: () => void;
initBuildTest: () => void = this.buildFunction;
initMessage: string = 'Hello';
message: string;
build() {
Column() {
Text(this.initMessage)
.fontSize(30)
Text(this.message)
.fontSize(30)
this.initBuildTest();
this.buildTest();
}
.width('100%')
.height('100%')
}
}
@Resuable - 组件复用
-
解释:
@Builder
装饰器所装饰的自定义组件被标记为可复用组件,常与@Component
装饰器结合使用。当可复用组件从组件树上被移除时,组件和其对应的 JSView 对象都会被放入复用缓存中,后续创建新自定义组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。@Reusable
装饰器仅用于自定义组件。@Resuable
组件中不支持使用ComponentContent
。@Reusable
组件不支持嵌套使用,即在一个可复用组件中使用另一个可复用组件。@Reusable
组件用于渲染的变量需要被@State
所修饰,否则组件复用会存在 UI 无法更新的问题。@Reusable
组件使用时,可以通过属性方法.reuseId(id: str)
的方式为复用组件分配复用组,此时相同reuseId
的组件会在同一个复用组中复用。
-
生命周期函数
Hook 时机 类型 aboutToReuse
可复用组件:复用缓存 -> 节点树 aboutToReuse(params: Record<string, ESObject>): void
aboutToRecycle
可复用组件:节点树 -> 复用缓存 aboutToRecycle(): void
@Reusable
组件复用时,需要在aboutToReuse
Hook 中更新@State
变量,其中params
为组件的构造参数。 -
组件复用场景
-
标准型:复用组件之间布局完全相同。
1
2
3
4
5
6
7
8
9
10
11
12
13// 复用组件
export struct CardView {
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// 列表渲染复用组件
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// 复用组件
struct ReusableComponent {
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// 列表渲染复用组件
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// 复用组件
struct ChildComponentA { // 类似定义 ChildComponentB、ChildComponentC、ChildComponentD
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// 列表渲染复用组件
struct MyComponent {
private data: MyDataSource = new MyDataSource();
itemBuilderOne(item: string) {
Column() {
ChildComponentA({ item: item })
ChildComponentB({ item: item })
ChildComponentC({ item: item })
}
}
itemBuilderTwo(item: string) {
Column() {
ChildComponentA({ item: item })
ChildComponentC({ item: item })
ChildComponentD({ item: item })
}
}
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 状态管理概述
-
状态管理:在自定义组件中,变量需要通过装饰器进行装饰,才能成为状态变量。状态变量的变化会触发 UI 的重新渲染和更新。如果不使用状态变量,UI 仅会在初始化时进行渲染,而在后续的操作中将不会自动刷新。
状态管理的功能仅支持在 UI 主线程使用,不能在子线程、worker、taskpool 中使用。
-
基本概念
- 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起 UI 的渲染更新
- 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算,常规变量值的改变永远不会引起 UI 的刷新
- 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据,通常指父组件传给子组件的数据。
- 命名参数机制:父组件将指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。如
CompA({ aProp: this.aProp })
。 - 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖。
- 初始化子组件:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。
- 本地初始化:在变量声明的时候赋值,作为变量的默认值。
-
状态管理 V1
-
装饰器分类 - 状态变量的影响范围层面
-
管理组件内状态的装饰器:同一个组件树上(即同一个页面内)组件内或不同组件层级的状态。
装饰器 装饰对象 作用 @State
变量 变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。
变量数值改变时,会引起相关组件的渲染刷新。@Prop
变量 变量可以和父组件建立单向同步关系。
变量是可变的,但修改不会同步回父组件。
复杂类型是数据源的深拷贝。@Link
变量 变量可以和父组件建立双向同步关系。
变量的修改会同步给父组件中建立双向数据绑定的数据源,
父组件的更新也会同步给子组件中的该变量。@Provide/@Consume
变量 变量用于跨组件层级(多层组件)同步状态变量。
变量通过alias
(别名)或者属性名绑定。@Observed
class
class
为需要观察多层嵌套场景的class
。仅可观察第一层属性
单独使用@Observed
没有任何作用,需要和@ObjectLink
、@Prop
联用。@ObjectLink
变量 变量接收 @Observed
装饰的class
的实例,用于观察多层嵌套场景,和父组件的数据源构建双向同步。 -
管理应用级状态的装饰器:不同页面,甚至不同 UIAbility 的状态。
装饰器 作用 @StorageLink/@LocalStorageLink
实现应用和组件状态的双向同步 @StorageProp/@LocalStorageProp
实现应用和组件状态的单向同步 AppStorage
是LocalStorage
的一个特殊的单例,作为应用级的数据库,与进程绑定。可以通过@StorageProp
和@StorageLink
装饰器将其与组件进行联动。LocalStorage
是用于存储应用程序声明的应用状态的内存“数据库”,通常用于页面级的状态共享。通过@LocalStorageProp
和@LocalStorageLink
装饰器,可以将其与UI
进行联动。
-
-
装饰器分类 - 数据的传递形式和同步类型层面
- 只读的单向传递
- 可变更的双向传递
-
其他功能
@Watch
:用于监听所装饰的状态变量的变化。$$
运算符:给内置组件提供 TS 变量的引用,使得 TS 变量和内置组件的内部状态保持同步。
-
-
状态管理 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
监听事件。 -
其他功能
!!
运算符:双向绑定语法糖。
-
-
状态管理 V1 Vs. 状态管理 V2
状态管理 V1 状态管理 V2 使用代理观察数据 数据本身就是可观察的 状态变量依赖 UI,
一个视图的更改不会通知其他视图更新。状态变量独立于 UI,
更改数据会触发视图更新。无法深度观测和监听,
只能感知对象第一层属性变化。支持深度观测和监听,
且不影响性能。存在冗余更新的问题。 支持精准更新和最小化更新。 装饰器使用限制多,不易用。 装饰器易用性高、拓展性强。 组件中状态变量的输入输出不明确,
不利于组件化。组件中状态变量的输入输出明确,
有利于组件化。
7.2 状态管理 V1
组件状态管理
@State - 组件内状态
-
解释:
@State
所装饰的变量拥有了状态属性,称之为状态变量。@State
变量改变时,会触发其直接绑定的 UI 组件的刷新。@State
变量也是大部分状态变量的数据源。- 单向数据同步:
@State
变量与子组件中的@Prop
变量之间。 - 双向数据同步:
@State
变量与子组件中的@Link
、@ObjectLink
变量之间。
- 单向数据同步:
-
语法
1
变量名: 变量类型 = 初始值;
-
允许装饰的变量类型,
Object
、class
、string
、number
、boolean
、enum
及其数组。Date
。Map
、Set
、联合类型。(API11+)undefined
、null
。Length
、ResourceStr
、ResourceColor
。(ArkUI 提供)
-
初始化规则,
-
本地初始化:必须。
-
从父组件初始化:可选。父组件传入的非
undefined
值,会覆盖本地初始化的值;否则以本地初始化的值为准。 -
允许的父组件的数据源类型、子组件被初始化的参数类型,
-
-
访问控制:不支持组件外访问。
-
数据同步:不与父组件中任何类型的变量同步。
-
-
UI 更新原理 - 按需更新
- 观察到状态变量的修改
- 查询依赖该状态变量的组件,并执行组件的更新方法,组件更新渲染
-
可以被观察到的状态变化(假设
value
为对应类型的@State
变量)-
类型为
string
、number
、boolean
的状态变量:直接修改1
this.value = 123; // 支持
-
类型为
Object
、class
的状态变量:直接修改,属性赋值(仅支持第一层属性的修改)。1
2
3this.value = new Person(); // 支持
this.value.content = "hello"; // 支持
this.value.info.msg = "world"; // 不支持 ‼️对于嵌套监测场景,可以使用
@Observed
装饰class
,此时class
属性的变化都可以被框架观察到,但是注意,组件还是最多只支持状态变量第一层属性的修改引发的 UI 渲染。 -
类型为
Array
的状态变量:直接修改,添加、删除、更新数组元素。不支持元素的属性赋值。1
2
3
4
5this.value = [new Person()]; // 支持
this.value[0] = new Person(); // 支持
this.value.pop(); // 支持
this.value.push(new Person()); // 支持
this.value[0].content = "hello"; // 不支持 ‼️ -
类型为
Date
、Map
、Set
的状态变量:直接修改,Date
/Map
/Set
接口的调用。
-
-
使用说明
-
构造函数中状态变量的变更无效‼️:在
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
struct Index {
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
struct PlayDetailPage {
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
40class DataObj {
name: string = 'default name';
constructor(name: string) {
this.name = name;
}
}
struct Index {
list: DataObj[] = [new DataObj('a'), new DataObj('b'), new DataObj('c')];
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 类型,二者类型不同,会触发状态变量值的更新,进而触发页面刷新
})
}
}
}
struct ConsumerChild {
'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
class DataObj {
name: string = 'default name';
constructor(name: string) {
this.name = name;
}
}
// ... -
修正方式二:使用
@ohos.arkui.StateManagement
提供的UIUtils
的getTarget()
方法,获取对应状态变量的原始对象,相同的对象赋值时,经原始对象对比后再确定是否继续赋值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { UIUtils } from '@ohos.arkui.StateManagement';
// ...
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
53class Balloon {
volume: number;
constructor(volume: number) {
this.volume = volume;
}
static increaseVolume(balloon:Balloon) {
balloon.volume += 2;
}
}
struct Index {
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 - 父子单向同步
-
解释:
@Prop
所装饰的变量和父组件建立了单向的同步关系(父组件 -> 子组件)。@Prop
变量允许在本地修改,但修改后的变化不会同步回父组件。- 父组件数据源更改时,
@Prop
变量都会更新,并且会覆盖本地所有更改。
-
语法
1
变量名: 变量类型 [= 初始值];
-
允许装饰的变量类型:同
@State
。但注意,@Prop
的类型必须和数据源的类型相同。 -
初始化规则,
-
本地初始化:可选。
-
从父组件初始化:如果本地有初始化,则可选,否则必须。
-
允许的父组件的数据源类型、子组件被初始化的参数类型,
-
-
访问控制:不支持组件外访问。
-
数据同步:单向同步,对父组件状态变量值的修改,将同步给子组件,
@Prop
变量,子组件@Prop
变量的修改不会同步到父组件的状态变量上。 -
数据拷贝:深拷贝,在拷贝的过程中除了基本类型、
Map
、Set
、Date
、Array
外,都会丢失类型。 -
嵌套传递:在组件复用场景,建议
@Prop
深度嵌套数据不要超过 5 层。嵌套太多会导致深拷贝占用的空间过大和垃圾回收,引起性能问题,建议使用@ObjectLink
。
-
-
UI 更新原理
@Prop
变量更新,更新渲染仅停留在当前组件(子)@Prop
数据源更新,@Prop
变量被父组件的数据源重置,进而触发更新渲染(父 -> 子)
-
可以被观察到的状态变化:同
@State
。但注意,数据源的@Link
、@Prop
或@State
变量对@Prop
变量的同步机制是相同的,即单向同步机制。 -
使用说明
-
@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
118let nextId: number = 1;
class BookInfo {
public publishedAt: Date;
public author: string;
constructor(publishedAt: Date, author: string) {
this.publishedAt = publishedAt;
this.author = author;
}
}
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;
}
}
struct BookCard {
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 })
}
}
struct BookInfoCard {
bookInfo: BookInfo;
bookId: number;
build() {
Column() {
Text(`book Id: ${this.bookId}`)
Text(`published At: ${this.bookInfo.publishedAt.toDateString()}, created By: ${this.bookInfo.author}`)
}
}
}
struct AuthorCard {
name: string = '';
build() {
Column() {
Text(`author Card: ${this.name}`)
}
}
}
struct Library {
books: Book[] = [new Book("C#", 765), new Book("JS", 652), new Book("TS", 765)];
basicAuthor: BookInfo = new BookInfo(new Date(), 'Potter');
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
变量代替。
-
@Link - 父子双向同步
-
解释:
@Link
所装饰的变量和父组件建立了双向数据绑定(父组件 <-> 子组件)。 -
语法
1
变量名: 变量类型;
-
允许装饰的变量类型:同
@State
。但注意,@Link
的类型必须和数据源的类型相同(父组件@State: T
, 子组件@Link: T
)。 -
初始化规则,
-
本地初始化:禁止。
-
从父组件初始化:必须。
@Link
变量aLink
从父组件@State
变量aState
的初始化语法可以简写为Comp({aLink: $aState})
。@Link
变量仅允许被状态变量初始化,不能用常量初始化。 -
允许的父组件的数据源类型、子组件被初始化的参数类型,
-
-
访问控制:不支持组件外访问。
-
数据同步:双向同步。父组件中的状态变量可以与子组件
@Link
建立双向同步,当其中一方改变时,另外一方能够感知到变化。@Link
变量可以与父组件@State
,@StorageLink
和@Link
建立双向绑定。 -
可用范围:
@Link
装饰器不能在@Entry
装饰的自定义组件中使用。
-
-
UI 更新原理
- 初始渲染时,父组件将
@State
变量的包装类通过构造函数传递给子组件,子组件的@Link
变量的包装类拿到父组件的@State
变量后,将@Link
变量的包装类的this
指针注册到父组件的@State
变量上。即初始渲染后,- 父组件的
@State
变量有子组件@Link
变量包装类的this
指针, - 子组件有父组件
@State
变量的包装类。
- 父组件的
@Link
数据源更新,父组件对所有依赖其变更的@State
变量的系统组件(elementId
)和状态变量(如@Link
包装类)进行遍历更新。子组件中所有依赖@Link
变量的系统组件都被通知更新。(父 -> 子)@Link
变量更新,子组件调用父组件@State
包装类的set
方法,将更新的数值同步回子组件。子组件@Link
变量和父组件@State
变量分别遍历依赖其的系统组件,进行相应的 UI 刷新。(子 -> 父)
- 初始渲染时,父组件将
-
可以被观察到的状态变化:同
@State
。 -
使用说明
-
双向同步时单向修改本地变量:结合使用
@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
*/
struct Parent {
sourceNumber: number = 0;
build() {
Column() {
Text(`父组件的 sourceNumber:` + this.sourceNumber)
Child({ sourceNumber: this.sourceNumber })
Button('父组件更改 sourceNumber')
.onClick(() => {
this.sourceNumber++;
})
}
.width('100%')
.height('100%')
}
}
struct Child {
memberMessage: string = 'Hello World';
'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
class Info {
public age: number = 0;
constructor(age: number) {
this.age = age;
}
}
struct LinkChild {
testNum: number;
build() {
Text(`LinkChild testNum ${this.testNum}`)
}
}
struct LinkChild2 {
testNum: Info;
build() {
Text(`LinkChild testNum ${this.testNum.age}`)
}
}
struct Parent {
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 - 组件后代双向同步
-
解释:
@Provide
和@Consume
,用于祖先组件与后代组件的双向数据绑定,用于状态数据的多层级传递。@Provide
所装饰的变量位于祖先组件中,作为被提供给后代组件的状态变量;@Consume
所装饰的变量位于后代组件中,用于**消费(绑定)**祖先组件提供的状态变量。@Provide
和@Consume
可以通过相同的变量名或者相同的别名(alias)绑定,建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常。@Provide
变量和@Consume
变量是一对多的关系。不允许在同一个自定义组件及其子组件中声明多个同名或者同别名的@Provide
变量,@Provide
变量的属性名或别名需要唯一且确定,如果声明多个同名或者同别名的@Provide
变量,会发生运行时报错。
-
语法
1
2
3
4
5
6
7// 基于相同变量名提供和消费状态变量
// 祖先组件中 变量名: 变量类型 = 初始值;
// 后代组件中 变量名: 变量类型;
// 基于相同别名提供和消费状态变量
'别名') 变量名1: 变量类型 = 初始值; // 祖先组件中 (
'别名') 变量名2: 变量类型; // 后代组件中 (-
装饰器参数:可选的常量字符串,表示变量别名。如果指定了别名,则通过别名来绑定变量,否则,通过变量名绑定变量。
-
允许装饰的变量类型:同
@State
。但注意,@Provide
变量和@Consume
变量的类型必须相同。 -
初始化规则
- @Provide @Consume 本地初始化 必须。 禁止。 从父组件初始化 可选。 禁止。
仅允许通过相同的变量名和别名从@Provide
初始化。允许的父组件的数据源类型
子组件被初始化的参数类型 -
访问控制:不支持组件外访问。
-
数据同步:双向同步。组件组件中的
@Provide
变量和后代组件中的@Consume
组件建立双向同步,当其中一方改变时,另外一方能够感知到变化。双向同步操作与@State
和@Link
的组合相同。 -
重写参数:
@Provide
支持allowOverride
参数,此时@Provide
变量可以被重写。1
2'别名'}) 变量名: number = 10; // 重写通过别名提供的 @Provide 变量 ({allowOverride :
'变量名'}) 变量名: number = 10; // 重写通过变量名提供的 @Provide 变量 ({allowOverride :1
2
3interface ProvideOptions {
allowOverride?: string // 是否允许 @Provide 重写。允许在同一组件树下通过 allowOverride 重写同名的 @Provide。如果开发者未配置 allowOverride,定义了同名的 @Provide,运行时会报错。
}
-
-
UI 更新原理
- 初始渲染时,
@Provide
变量被以map
的形式传递给@Provide
变量所属组件的所有子组件,子组件中如果使用@Consume
变量,则在map
中以该变量名或别名为键,查找对应的@Provide
变量,- 如果查找到,
@Consume
变量会进行自身初始化,并保存该@Provide
变量的引用,同时将自身注册给@Provide
变量。 - 如果查找不到,框架会抛出 JS Error。
- 如果查找到,
@Provide
变量更新后,会遍历更新所有依赖它的系统组件(elementId
)和状态变量(@Consume
)。子组件中所有依赖@Consume
的系统组件(elementId
)都会被通知更新.。@Consume
变量更新后,调用预先保存的@Provide
变量的更新方法,将更新的数值同步回@Provide
。
- 初始渲染时,
-
可以被观察到的状态变化:同
@State
。 -
使用说明
- 尾随闭包初始化
@BuilderParam
时的@Provide
变量未定义问题‼️:当在一个组件中使用尾随闭包来初始化@BuilderParam
变量,并同时定义@Provide
变量时,可能会遇到@BuilderParam
变量生成的子组件无法通过@Consume
访问@Provide
定义的变量的问题。这是因为在调用this.builder()
时,this
指向的是当前组件的父组件,而不是当前组件本身。因此,子组件无法找到当前组件中定义的@Provide
变量。【文档描述不清】- 为了避免此问题,建议不要在同一个组件中同时使用
@BuilderParam
和@Provide
。通过将它们分开使用,可以确保子组件正确访问@Provide
定义的变量。
- 为了避免此问题,建议不要在同一个组件中同时使用
- 类的静态函数和组件的实例方法中直接修改状态变量的属性无效‼️:同
@State
的使用说明的同名问题,是没有数据代理导致的问题。
- 尾随闭包初始化
@Observed/@ObjectLink - 嵌套对象/数组双向同步
-
解释:
@Observed
和@ObjectLink
,用于嵌套场景的观察,用于弥补装饰器仅能观察一层的能力限制。所谓嵌套场景,即二维数组、对象数组或属性为class
的class
。@Observed
所装饰的类可以通过new
创建实例,该实例属性的变化可以被监测到;@ObjectLink
所装饰的类可以接收被@Observed
所装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定,该实例可以是来自数组状态变量的元素,对象状态变量的属性。- 数据双向同步:
@Observed
+@ObjectLink
- 数据单向同步:
@Observed
+@Prop
- 数据双向同步:
-
语法
1
2
3
4
5
6
7
8
9
10
class 类名 {
public 属性名1: 属性类型1;
public 属性名2: 属性类型2;
constructor(属性名1: 属性类型1, 属性名2: 属性类型2) {
this.属性名1 = 属性类型1;
this.属性名2 = 属性类型2;
}
}1
变量名: 变量类型;
-
允许装饰的变量类型:
@Observed
用于装饰class
。@ObjectLink
用于装饰被@Observed
装饰的class
实例,包含继承Date
、Array
、Map
、Set
的class
实例,支持和undefined
或null
组成的联合类型。 -
初始化规则
-
本地初始化:禁止。
-
从父组件初始化:必须。用于初始化的数据源必须同时满足,
- 类型必须是
@Observed
装饰的class
。 - 必须是
@State
,@Link
,@Provide
,@Consume
或者@ObjectLink
装饰的class
的属性或数组项。
- 类型必须是
-
允许的父组件的数据源类型、子组件被初始化的参数类型,
-
-
数据同步:双向同步。
-
访问控制:
@ObjectLink
变量是只读的,其属性可变,但不能直接被赋值,否则会导致运行时报错(可以在父组件中通过整体替换来实现)。@ObjectLink
变量相当于指向数据源的指针(引用)。 -
可用范围:
@ObjectLink
装饰器不能在@Entry
装饰的自定义组件中使用。 -
潜在问题:使用
@Observed
装饰class
会改变class
原始的原型链,因此@Observed
和其他类装饰器装饰同一个class
可能会带来问题。 -
@ObjectLink
Vs.@Prop
:@ObjectLink
变量存储对数据源的引用,与数据源建立双向同步关系,@Prop
变量存储对数据源的深拷贝,与数据源建立单向同步。
-
-
可以观察到的状态变化:被
@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
class ObservedArray<T> extends Array<T> {
// new ObservedArray<T>(arr: T[]) 创建的数组的元素的增删改都可以被监测到,基于此可以实现二维数组 [new ObservedArray<T>(arr: T[])],数组项或数组项的数组项的增删改都可以被监测到。
constructor(args: T[]) {
super(...args);
}
}
class DateClass extends Date {
constructor(args: number | string) {
super(args);
}
}
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;
}
}
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;
}
} -
UI 更新原理
- 初始渲染时,被
@Observed
装饰的class
的实例会被不透明的代理对象包装,class
上的属性的setter
和getter
方法被代理。子组件的@ObjectLink
变量接收被@Observed
装饰的class
的实例,@ObjectLink
变量的包装类会将自己注册给@Observed
class。 @Observed
类的实例的属性更改,执行相应的setter
和getter
方法,遍历依赖它的@ObjectLink
包装类,通知数据更新。
- 初始渲染时,被
-
使用说明
-
通过组件嵌套实现嵌套对象属性的深度监测:假设现在有数据结构
ParentCounter
如下,其中入口组件Index
有@State
变量counter
的类型为Array<ParentCounter>
。counter
的数据深度为3
,第一层访问到数组元素ParentCounter
实例,第二层访问到counter
和childCounter
实例,第三层访问到childCounter
实例的counter
。@State
变量只能检测到第一层数据的修改,即数组元素的增删改。为了监测到每一层数据,需要封装两个组件ParentComp
和ChildComp
,其中ParentComp
是ChildComp
的父组件。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
23let nextId = 1;
class ChildCounter {
counter: number;
constructor(counter: number) {
this.counter = counter;
}
}
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
struct ParentComp {
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)
}
}
}
struct ChildComp {
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
struct Index {
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
class RenderClass {
waitToRender: boolean = false;
constructor() {
setTimeout(() => {
this.waitToRender = true;
console.log("更改 waitToRender 的值为:" + this.waitToRender);
}, 1000)
}
}
struct Index {
'renderClassChange') renderClass: RenderClass = new RenderClass(); (
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
class Person {
name: string = '';
age: number = 0;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
class Info {
person: Person;
constructor(person: Person) {
this.person = person;
}
}
struct Parent {
'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
}
})
}
}
}
struct Child {
'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 状态存储
-
解释: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 垃圾回收。
- 应用程序可以创建多个 LocalStorage 实例,LocalStorage 实例可以在页面内共享,也可以通过
-
语法
1
2"键值") 变量名: 变量类型 = 初始值; (
"键值") 变量名: 变量类型 = 初始值; (-
装饰器参数:必须的常量字符串,用于绑定 LocalStorage 中对应的属性,初始化
@LocalStorageXxx
变量。因为无法保证 LocalStorage 一定存在给定的key
,@LocalStorageXxx
变量一定需要本地初始化。 -
允许装饰的变量类型:同
@State
。但是,变量类型必须被指定,建议和 LocalStorage 中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 -
初始化规则,
-
本地初始化:必须。
@LocalStorageXxx
变量只能从 LocalStorage 中key
对应的属性初始化,如果没有对应key
的话,将使用本地默认值初始化,并存入 LocalStorage 中。 -
从父组件初始化:禁止。
-
允许的父组件的数据源类型、子组件被初始化的参数类型,
@LocalStorageProp
@LocalStorageLink
-
-
访问控制:不支持组件外访问。
-
数据同步
@LocalStorageProp
@LocalStorageLink
单向同步。
同步方向为:LocalStorage 的对应属性到组件的状态变量。组件本地的修改是允许的,但是 LocalStorage 中给定的属性一旦发生变化,将覆盖本地的修改。双向同步。
同步方向为:LocalStorage 的对应属性到自定义组件,从自定义组件到 LocalStorage 对应属性。
-
-
可以观察到的状态变化:同
@State
变量。 -
UI 更新原理
@LocalStorageProp
@LocalStorageLink
-
LocalStorage API
-
构造函数
- 语法:
constructor(initializingProperties?: Object)
- 说明:可选使用
initializingProperties
包含的属性和数值初始化 LocalStorage。initializingProperties
不能为undefined
。
1
2let 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
返回属性名称。
-
-
使用说明
-
将
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
61class 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));
struct Child {
// @LocalStorageLink 变量装饰器与 LocalStorage 中的 'PropA' 属性建立双向绑定
'PropA') childLinkNumber: number = 1; (
// @LocalStorageLink 变量装饰器与 LocalStorage 中的 'PropB' 属性建立双向绑定
'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) // 写法一
// 写法二 ({storage})
struct Parent {
// @LocalStorageLink 变量装饰器与 LocalStorage 中的 'PropA' 属性建立双向绑定
'PropA') parentLinkNumber: number = 1; (
// @LocalStorageLink 变量装饰器与 LocalStorage 中的 'PropB' 属性建立双向绑定
'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 实例
storage: LocalStorage.getShared() }) ({
struct Index {
// 可以使用 @LocalStorageLink/Prop 与 LocalStorage 实例中的变量建立联系
'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
export function PageBuilder() {
Page()
}
// Page 组件获得了父亲 Index 组件的 LocalStorage 实例
struct Page {
'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"
} - 在所属的 UIAbility 中创建 LocalStorage 实例,并调用
-
非根节点的自定义组件可以通过构造参数来传递 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
151let 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');
struct MyNavigationTestStack {
'pageInfo') pageInfo: NavPathStack = new NavPathStack(); // Navigation 路由栈 (
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)
}
}
}
struct pageOneStack {
'pageInfo') pageInfo: NavPathStack; (
'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;
})
}
}
struct pageTwoStack {
'pageInfo') pageInfo: NavPathStack; (
'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;
})
}
}
struct pageThreeStack {
'pageInfo') pageInfo: NavPathStack; (
'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;
})
}
}
struct NavigationContentMsgStack {
'PropA') PropA: string = 'Hello'; // 不同的页面中访问的是不同的 LocalStorage 实例 (
build() {
Column() {
Text(`${this.PropA}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
}
}
-
AppStorage - 应用全局 UI 状态存储
-
解释: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 中对应的非静态方法。
-
语法(类同 LocalStorage)
1
2"键值") 变量名: 变量类型 = 初始值; (
"键值") 变量名: 变量类型 = 初始值; (
PersistentStorage - UI 状态的持久化存储
-
解释:LocalStorage 和 AppStorage 都是运行时的内存,而 PersistentStorage 可以实现持久化存储选定的 AppStorage 属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。
-
PersistentStorage 是应用程序中的可选单例对象,其提供的变量持久化的能力都需要依赖 AppStorage。
-
PersistentStorage 和 AppStorage 中的属性建立双向同步。
-
不要大量的数据持久化,因为 PersistentStorage 写入磁盘的操作是同步的,大量的数据本地化读写会同步在 UI 线程中执行,影响 UI 渲染性能。
-
-
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
4interface PersistPropsOptions {
key: string; // 属性名
defaultValue: number | string | boolean | Object
} -
static keys(): Array<string>
返回所有持久化属性的属性名的数组。
-
-
持久化数据的类型
-
不支持
-
number, string, boolean, enum 等简单类型。
-
可以被 JSON.stringify() 和 JSON.parse() 重构的对象(不包括对象中的成员方法)。
-
Map、Set、Date 类型,并可观察到这些类型的整体赋值和接口调用。
-
undefined、null。
-
上述类型的联合类型。
-
-
不支持
- 嵌套对象(对象数组,对象的属性是对象等)。
-
-
持久化数据的约束
-
避免持久化的情景:大型数据集、经常变化的变量。
-
持久化的数据大小:小于 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);
});
}
-
-
使用说明
-
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
23PersistentStorage.persistProp('aProp', 47); // 1. 初始化 PersistentStorage
const aProp = AppStorage.get<number>('aProp'); // 2. 在组件外部从 AppStorage 中访问 PersistentStorage 初始化的属性
struct Index {
message: string = 'Hello World';
'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 - 设备环境查询
-
解释:Environment 是 ArkUI 框架在应用程序启动时创建的单例对象,它提供了读取系统某些环境变量(应用程序运行的设备的环境参数)的能力,并将其值写入 AppStorage 里,所以开发者需要通过 AppStorage 才能获取环境变量的值。Environment 的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型,因此组件中通过 @StorageProp 单向同步访问环境变量,程序中通过 AppStorage.prop 访问环境变量的单向同步对象。
-
Environment 支持的参数:accessibilityEnabled、colorMode、fontScale、fontWeightScale、layoutDirection、languageCode。
-
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
4interface EnvPropsOptions {
key: string;
defaultValue: number | string | boolean
} -
static keys(): Array<string>
返回环境变量的属性 key 的数组。
-
-
使用说明
-
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');
struct Index {
'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 - 状态变量变更通知
-
解释:
@Watch
为状态变量设置回调函数,用于监听状态变量可观察到的变化,当状态变量变化时,@Watch
指定的回调方法将被调用。@Watch
在 ArkUI 框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。 -
语法
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
struct Index {
/*
* @Watch(callbackStr)
* - callbackStr:必须的常量字符串,表示所修饰的状态变量更新时要执行的回调函数。类型为 (changedPropertyName? : string) => void。
* - 可装饰变量:状态变量,如 @State、@Prop、@Link 变量等。
* - 装饰器顺序:无顺序,建议 @State、@Prop、@Link 等装饰器在 @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;
})
}
}
} -
使用说明
@Watch
回调函数调用的时机是状态变量真正变化的时间。假设父组件有两个@State
变量aState
、bState
,子组件有一个以aState
为数据源的@Prop/@ObjectLink
变量cProp
和一个以bState
为数据源的@Link
变量dLink
,同时这四个变量都设置了相应的@Watch
回调函数。如果此时在父组件中通过事件先后同时改变aState
和bState
,那么这四个@Watch
回调函数的执行顺序是:@Watch-aState
->@Watch-bState
->@Watch-dLink
->@Watch-cProp
。这是因为,@Link
的状态更新是同步的,状态变化会立刻触发@Watch
回调。@ObjectLink
的更新依赖于父组件的刷新,当父组件刷新并将更新后的变量传递给子组件时,@Watch
回调才会触发,因此触发顺序略晚于@Link
。
$$ - 内置组件状态的双向同步
-
解释:
$$
运算符有两种功能,-
@Builder
函数中按引用传递参数时,使用$$
作为参数名称。1
2
3
4
5
6
7
8
9
10
11interface Temp {
param1: string;
param2: number;
}
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
struct TextInputExample {
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)
}
}
-
@Track - 属性级更新
-
解释:
@Track
用于修饰class
的实例属性。一般而言,如果class
实例是状态变量,其一个属性的变化会触发所有属性关联的 UI 更新,但如果是@Track
所装饰的属性发生变化,则只会触发该属性关联的 UI 更新,即@Track
装饰器可以避免冗余更新。 -
语法
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
41class Logger {
message: string;
date: Date;
constructor(message: string, date: Date) {
this.message = message;
this.date = date;
}
}
struct AddLog {
/* 状态变量是 class 实例,同时其属性被 @Track 修饰,那么被 @Track 修饰的属性发生变化时,仅该属性关联的 UI 触发更新。 */
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%')
}
} -
使用注意:如果 class 类中使用了
@Track
装饰器,则未被@Track
装饰器装饰的属性不能在 UI 中使用,如果使用,会发生运行时报错,但可以在非 UI 中使用。- UI 中使用:绑定在组件,初始化子组件等
- 非 UI 中使用:事件回调函数、生命周期函数。
freezeWhenInactive - 组件冻结
-
解释:通过
@Component({ freezeWhenInactive: true })
,可以激活组件冻结机制。- ArkTs 中将组件分为两种状态
active
和inactive
。 - 开启组件冻结机制后,当组件所依赖的状态变量更新后,框架仅对处于
active
状态的组件进行更新,从而提高复杂 UI 场景下的刷新效率。 - 当开启了组件冻结的组件状态从
inactive
变为active
时,框架会对其执行必要的刷新操作,确保 UI 的正确展示。
- ArkTs 中将组件分为两种状态
-
组件冻结的使用场景
场景 active inactive 页面路由 当前栈顶页面 非栈顶不可见页面 TabContent 当前显示的 TabContent 中的自定义组件 未显示的 TabContent 组件 LazyForEach 当前显示的 LazyForEach 中的自定义组件 缓存节点的自定义组件 Navigation 当前显示的 NavDestination 中的自定义组件 未显示的 NavDestination 组件 组件复用 复用池上树的节点 进入复用池的组件 -
使用说明:BuilderNode 可以通过命令式动态挂载组件,而组件冻结又是强依赖父子关系来通知是否开启组件冻结。如果父组件使用组件冻结,且组件树的中间层级上又启用了 BuilderNode,则 BuilderNode 的子组件将无法被冻结。
MVVM
-
MVVM:即 Model-View-ViewModal 架构模式,其将应用分为 Model、View 和 ViewModel 三个核心部分,实现数据、视图与逻辑的分离。ArkUI 的 UI 开发模式即 MVVM 模式。
- Model:数据 + 逻辑。
- View:界面 + 交互。
- ViewModal:Modal 和 View 的桥梁。
- 一个 View 对应一个 ViewModel;
- ViewModel 监控 Model 数据的变化,通知 View 更新 UI;
- ViewModel 处理用户交互事件并转换为数据操作。
-
ArkUI 开发模式
- View:页面组件(对应某个页面)、业务组件(关联了 ViewModal 中的数据)、通用组件(没关联 ViewModal 中的数据)。
- ViewModal:页面数据(按页面组织的数据)。
- Model:本地(NativeC++,此时会使用非 UI 线程模型,因此需要有线程切换的能力,即切换到 UI 线程)、远端(Restful)。
-
MVVM 文件结构
1
2
3
4
5
6
7
8
9src
|-- ets
|---- pages 存放页面组件(页面布局,实现页面跳转,前后台事件处理等)
|---- views 存放业务组件(被页面引用,用于构建页面)
|---- shares 存放通用组件(多项目共享组件)
|---- service 数据服务(按照页面组织数据 + 对每个页面的数据进行烂加载)
|-- app.ts 服务入口
|-- LoginViewModel 登录页 ViewModel
|-- xxxModel 其他页 ViewModel
状态管理优秀开发实践
-
当子组件不需要发生本地改变时,使用
@ObjectLink
代替@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
struct Index {
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;
})
}
}
} -
建议每个状态变量关联的组件数少于 20 个,从而减少不必要的组件刷新。
-
当使用复杂对象作为状态变量时,应该控制其关联的组件数,否则会导致冗余刷新,即某个成员属性的变化会导致该对象关联的所有组件刷新,即使这些组件没有直接使用到该更改的成员属性。‼️
注意:数组类型的状态变量也存在冗余刷新的问题,即一个元素的变化,会导致所有元素相关联的 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
struct ListItemComponent {
item: string;
index: number;
'onCurrentIndexUpdate') currentIndex: number; // 被多组件关联的数据 (
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 渲染)
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)
}
}
-
-
-
应用开发过程中,可以使用 HiDumper 查看状态变量关联的组件数,进行性能优化。
-
避免在
for
、while
等循环中频繁读取状态变量。 -
需要更新状态变量的一段逻辑中,建议先使用临时变量进行结果计算,最后再将结果赋值给状态变量。
-
在使用 ForEach 或 LazyForEach 渲染对象数组时,建议结合自定义组件进行使用。对于对象数组中的每个元素,应该将其作为自定义组件的数据源。
-
减少使用 LazyForEach 的重建机制来刷新 UI,建议使用状态变量控制 UI 的重新渲染。
7.3 状态管理 V2
@ComponentV2 - V2 自定义组件
1 | /* |
@ObservedV2/@Trace - 类属性的深度观测
1 | /* |
@Local - 组件内部状态
1 | /* |
@Param - 组件外部输入(到内部)
1 | /* |
@Once - 初始化同步一次
1 | /* |
@Event - 组件内部输出(到外部)
1 | /* |
@Provider/@Consumer - 跨层级双向同步
1 | /* |
@Monitor - 状态变量监听
1 | /* |
@Computed - 计算属性
1 | /* |
AppStorageV2 - 应用全局 UI 状态存储
-
解释:AppStorageV2 是应用 UI 启动时创建的单例对象,提供应用状态数据的中心存储。
1
import { AppStorageV2 } from '@kit.ArkUI';
-
语法
-
connect
:创建或获取储存的数据1
2
3
4
5static 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 中,则可省略默认构造器,获取存储的数据;否则必须指定默认构造器,不然会导致应用异常;
- 同一个
key
,connect
不同类型的数据会导致应用异常; key
由字母、数字、下划线组成,长度不超过 255。- 关联
@Observed
对象时,由于该类型的name
属性未定义,需要指定key
或者自定义name
属性。(@ObservedV2
对象可以直接使用以获取 AppStorageV2 中的数据,其默认有name
属性)
-
remove
:删除指定key
的储存数据1
2
3static remove<T>(
keyOrType: string | TypeConstructorWithArgs<T> // 如果指定 key 创建或获取数据,则提供 key 删除数据;如果指定 type 创建或获取数据,则提供 type 删除数据(此时以 type.name 为 key)
): void;- 删除 AppStorageV2 中不存在的
key
会报警告。
- 删除 AppStorageV2 中不存在的
-
keys
:返回所有 AppStorageV2 中的key
1
static keys(): Array<string>; // 所有 AppStorageV2 中的 key。
-
-
使用限制
- 只能在 UI 线程中使用。
- 不支持 collections.Set、collections.Map 等类型;不支持非 buildin 类型,如 PixelMap、NativePointer、ArrayList 等 Native 类型。
PersistenceV2 - UI 状态的持久化存储
-
解释:PersistenceV2 是应用 UI 启动时创建的单例对象,提供应用状态数据的中心存储,并将最新数据在设备磁盘上进行持久化存储。
1
import { PersistenceV2 } from '@kit.ArkUI';
-
语法
-
connect
:创建或获取储存的数据(同 AppStorageV2) -
remove
:删除指定key
的储存数据(同 AppStorageV2) -
keys
:返回所有 AppStorageV2 中的key
(同 AppStorageV2) -
save
:手动持久化数据1
2
3static save<T>(
keyOrType: string | TypeConstructorWithArgs<T> // 如果指定 key 创建或获取数据,则提供 key 删除数据;如果指定 type 创建或获取数据,则提供 type 删除数据(此时以 type.name 为 key)
): void;- 手动持久化当前内存中不处于
connect
状态的key
是无意义的
- 手动持久化当前内存中不处于
-
notifyOnError
:响应序列化或反序列化失败的回调1
2
3static notifyOnError(
callback: PersistenceErrorCallback | undefined // 数据序列化或反序列化失败时的回调,undefined 表示取消该回调
): void;1
type PersistenceErrorCallback = (key: string, reason: 'quota' | 'serialization' | 'unknown', message: string) => void;
-
-
使用限制
- 只能在 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 - 类属性的类型标记
-
解释:
@Type
装饰器用于装饰@ObservedV2
类的属性,与PersistenceV2
结合使用,使得类属性在序列化时不丢失类型信息,便于类的反序列化。 -
语法
1
2
3
4
5
class className {
(propertyType)
propertyName: PropertyType = propertyValue;
}- 装饰器参数
type
:被装饰的属性的类型。 - 可装饰类型:
class
、Array
、Date
、Map
、Set
等内嵌类型。
- 装饰器参数
-
限制条件
- 只能用在
@ObservedV2
装饰的类中,不能用在自定义组件中。 - 不支持 collections.Set、collections.Map 等类型;不支持非 buildin 类型,如 PixelMap、NativePointer、ArrayList 等 Native 类型;不支持简单类型,如 string、number、boolean。
@Type
当前不支持带参数的构造函数。
- 只能用在
!! - 双向绑定
-
解释:与
$$
类似,!!
运算符也有两种功能,-
父组件中使用状态变量初始化子组件
@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
struct Index {
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} }
}
}
}
struct Star {
value: number = 0;
$value: (val: number) => void = (val: number) => {};
build() {
Column() {
Text(`${this.value}`)
Button(`change value `).onClick(() => {
this.$value(10);
})
}
}
} -
使用系统内置组件时,提供基础类型变量(支持 V1 的
@State
和 V2 的@Local
)的引用,使得变量和系统内置组件的状态保持同步。
-
freezeWhenInactive - 组件冻结
与 @Component
的组件冻结类似,但是不支持 lazyForEach
。
Repeat - 可复用的循环渲染
-
解释:
Repeat
是一种需要与容器组件配合使用的组件,对数组类型数据进行循环渲染。该组件分为以下两种模式,- non-virtualScroll:初始化页面时加载列表中的所有子组件。
- 适用场景:渲染短数据列表、组件全部加载。
- 与
ForEach
组件的区别:针对特定数组更新场景的渲染性能进行了优化;将组件生成函数中的索引管理职责转移至框架层面。
- virtualScroll:初始化页面时根据容器组件的**有效加载范围(可视区域+预加载区域)**加载子组件。
- 适用场景:渲染需要懒加载的长数据列表、通过组件复用优化性能表现。
- non-virtualScroll:初始化页面时加载列表中的所有子组件。
-
语法
-
non-virtualScroll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct 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(特有
template
、totalCount
、cachedCount
属性)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
31struct 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) // 容器组件的预加载区域大小(组件树上,可见范围外预加载的节点)
}
}
-
-
使用说明
- Repeat 一般与容器组件配合使用,子组件应当是允许包含在容器组件中的子组件。例如,Repeat 与 List 组件配合使用时,子组件必须为 ListItem 组件。
- 当 Repeat 与自定义组件或 @Builder 函数混用时,必须将 RepeatItem 类型整体进行传参,组件才能监听到数据变化,如果只传递 RepeatItem.item 或 RepeatItem.index,将会出现 UI 渲染异常。
getTarget - 获取代理数据的原始对象
这里关注一下两个问题,
-
语法
1
2import { UIUtils } from '@kit.ArkUI';
const 原始对象 = UIUtils.getTarget(被代理对象); -
ArkTS 中哪些数据是被代理对象?
- V1:
@Observed
装饰的类实例;状态变量装饰器(@State
、@Prop
等)装饰的复杂类型对象(Class
、Map
、Set
、Date
、Array
)。 - V2:状态变量装饰器如
@Trace
、@Local
装饰的Date
、Map
、Set
、Array
。
- V1:
makeObserved - 将非观察数据变为可观察数据
makeObserved
就是 getTarget
的反义方法,这里关注一下几个方面,
-
语法
1
2import { UIUtils } from '@kit.ArkUI';
const 可观察数据 = UIUtils.getTarget(不可观察数据);-
支持的参数:非空的对象类型传参。
-
未被
@Observed
或@ObserveV2
装饰的类 -
Array
、Map
、Set
和Date
(可以观测其 API 带来的变化) -
collections.Array
,collections.Set
和collections.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
send: SendableData = UIUtils.makeObserved(new SendableData());
-
-
不支持的参数
- 非空的对象类型传参 => 返回自身
- 非 Object 类型 => 报错
- 被
@ObservedV2
、@Observed
装饰的类的实例以及已经被makeObserved
封装过的代理数据 => 返回自身
-
makeObserved
可以在@Component
、@ComponentV2
组件中使用,但是不能和 V1 状态变量装饰器配合使用,允许和 V2 状态变量装饰器配合使用(包括@Monitor
、@Computed
)。
-
-
使用场景:
makeObserved
接口提供主要应用于@ObservedV2/@Trace
无法涵盖的场景。class
的定义在三方包中- 当前类的成员属性不能被修改(如
@Sendable
类) JSON.parse
返回的匿名对象
7.4 状态管理 V1、V2 混用及迁移
8. 渲染控制
8.1 if-else 条件渲染
-
语法
1
2
3
4
5
6
7
8
9
10
11
12
13struct componentName {
build(){
Column(){
if(条件语句){
// UI 渲染 1
}else if(条件语句){
// UI 渲染 2
}else{
// UI 渲染 3
}
}
}
}- 条件语句可以为:状态变量、常规变量、TypeScript 表达式。
- 如果条件语句中有状态变量,那么当状态变量值变化时,渲染的内容会更新。
if
语句允许嵌套使用。
-
使用注意:当
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
32struct 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
7if (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
9if (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 循环渲染
-
解释:ForEach 接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在 ForEach 父容器组件中的子组件。注意确保 ForEach 接口中的每个循环渲染项的键值都是唯一的!对于基本数据类型数组,可以将其包装为对象数组,并为每个对象指定唯一的
id
标识。 -
语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19struct 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 按需循环渲染
-
解释:LazyForEach 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了 LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
-
语法
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 }]);
}
}
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
,即组件对应的itemGenerator
的index
没有发生变化,因此删除结果可能会与预期不符合。解决方式为,1
2this.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}])
。
-