状态管理 V1 使用代理观察数据,创建状态变量时,会同时创建一个代理观察者,该观察者可以感知代理变化,但无法精准观测到实际数据变化。V1 侧重于组件层级的状态管理,V1 若要实现深度观察能力,需通过 @ObjectLink 逐层拆解嵌套类,并配合自定义组件使用。
状态管理 V2 则增强了对数据对象的深度观察与管理能力,使数据本身可观察,更新数据时,会触发相应视图的更新,UI刷新更加高效,可灵活地控制数据与状态,提升应用性能。
| 特性 | 状态管理 V1 | 状态管理 V2 |
|---|---|---|
| 独立于 UI | 状态变量不能独立于 UI | 状态变量独立于 UI,更新数据会触发相应视图的更新 |
| 深度观测 | 无法深度观测和深度监听,只能感知对象属性第一层的变化 | 支持对象的深度观测和深度监听,且不影响观测性能 |
| 精准更新 | 更新对象中属性时,存在冗余更新的问题 | 支持对象中属性级精准更新 |
| 易用性 | 装饰器间配合使用限制多,不易用,不利于组件化 | 装饰器易用性高、拓展性强,有利于组件化 |
@Local:组件内部状态
状态管理 V1 @State 装饰的变量,能够从外部初始化,即无法确保 @State 装饰变量的初始值一定为组件内部定义的值,这不利于自定义组件内部状态的管理
使用 @Local 装饰 @ComponentV2 自定义组件中的变量,可使变量具有观察变化的能力
- @Local 装饰器只能在 @ComponentV2 装饰的自定义组件中使用
- @Local 装饰的变量表示组件内部的状态,不允许从外部传入初始化
| @Local 装饰器 | 说明 |
|---|---|
| 装饰器参数 | 无 |
| 可装饰的变量类型 | Object、class、string、number、boolean、enum 等基本类型以及 Array、Date、Map、Set 等内置类型 |
| 支持 null、undefined 以及联合类型 | |
| 装饰变量的初始值 | 必须本地初始化,不允许外部传入初始化 |
| 装饰变量的传递 | @Local 装饰的变量可传递给子组件 @Param 装饰的变量,且可同步变化 |
| 观察能力 | 仅限于被装饰的变量本身 (装饰 @ObservedV2 对象能深度观察,装饰普通对象这能观察其整体赋值) |
观察变化
当装饰的变量为 boolean、string、number 时,可观察到对变量赋值的变化
当装饰的变量为对象时
- 普通对象,仅可观察到对类对象整体赋值的变化,无法直接观察到类成员属性的变化
@ObservedV2和@Trace装饰的类和属性,或makeObserved转换的可观察对象,可观察到类成员属性的变化
API19 开始,支持 @Local 和状态管理 V1 的 @Observed 装饰器同时使用,需遵守混用规则
当装饰简单类型数组时,可观察到数组整体或数组项的变化
当装饰的变量为嵌套类或对象数组时,@Local 对深层对象属性的观察,依赖于 @ObservedV2 与 @Trace 装饰器
当装饰 Array、Date、Map、Set 时,可观察到变量整体赋值以及 API 调用带来的变化
@Local 与 @State 对比
| 用法 | @State | @Local |
|---|---|---|
| 参数 | 无 | 无 |
| 初始化 | 可从外部初始化 | 不允许外部初始化 |
| 观察能力 | 能观察变量本身以及一层的成员属性,无法深度观测 | 能观测变量本身,深度观测依赖 @ObservedV2 与 @Trace 装饰器 |
| 数据传递 | 可作为数据源和子组件中状态变量同步 | 可作为数据源和子组件中@Param装饰的状态变量同步 |
@Param: 组件外部输入
状态管理 V1 存在多种可接受外部传入的装饰器,常用的有 @State、@Prop、@Link、@ObjectLink,这些装饰器使用有限制且不易区分,不当使用会导致性能问题
- 对于复杂类型 (如:对象),@Param 会接受数据源的引用;在组件内可修改类对象中的属性,且修改会同步回数据源
| @Param 装饰器 | 说明 |
|---|---|
| 装饰器参数 | 无 |
| 可装饰的变量类型 | Object、class、string、number、boolean、enum 等基本类型以及 Array、Date、Map、Set 等内置类型 |
| 支持 null、undefined 以及联合类型 | |
| 能否本地修改 | 不可,若需修改值,需搭配@Once修改子组件的本地值,或通过@Event修改 @Param 数据源的值 |
| 同步类型 | 由父到子组件的单向同步 (但是对于对象的属性值的修改,会同步回父组件) |
| 装饰变量的初始值 | 允许本地初始化,若不在本地初始化则需与@Require一起使用,则必须从外部传入初始值 |
| 观察能力 | 仅限于被装饰的变量本身 (装饰 @ObservedV2 对象能深度观察,装饰普通对象这能观察其整体赋值) |
@Param 装饰的变量,不可本地修改 (整体赋值),单向同步
但是对于复杂类型 (如:对象),@Param 接收数据源的引用,可本地修改属性值,且在子组件修改对象中的属性值,会同步回父组件,且会触发父组件与子组件 UI 的刷新
- @Param 装饰器只能在 @ComponentV2 装饰的自定义组件中使用
- @Param 支持本地和外部传入值的初始化,当存在外部传入值时,优先使用外部传入的值初始化
- @Param 装饰的变量在子组件中无法被直接修改,但,若装饰的变量是对象类型,在子组件中可以修改对象的属性
【示例】
@ObservedV2classInfo{@Tracename:string='assassin';@Traceage:number=20;constructor(name:string,age:number){this.name=name;this.age=age;}}// 父组件@Entry@ComponentV2exportstruct Comp{@Localinfo:Info=newInfo('assassin',20);build(){Column({space:20}){Text('parent info: '+this.info.name+', age: '+this.info.age)// 传递对象实例到子组件ChildComp({param:this.info})}}}@ComponentV2exportstruct ChildComp{// param 虽然在本地初始化了,但是父组件传递了值,其会覆盖本地初始值@Paramparam:Info=newInfo('child',10);build(){Column({space:20}){Text('child: '+this.param.name+', age: '+this.param.age)Button('change age').onClick(()=>{// 在子组件修改 @Param 装饰变量的属性值,会同步回父组件,即父、子组件会同步刷新 UIthis.param.age++;})}}}@Once: 初始化同步一次
若要实现仅从外部初始化一次且不接受后续同步变化的能力,需搭配使用 @Once 和 @Param 装饰器。
@Once 装饰的变量在初始化时接受外部传入值进行初始化,后续数据源更改不会同步给子组件
- @Once 不影响 @Param 的观测能力,仅针对数据源的变化做拦截
- @Once 必须搭配 @Param 使用,且不可搭配其它装饰器,搭配使用后,可在本地修改 @Param 变量的值
@Once 和 @Param 搭配装饰的状态变量为对象时
- 若只修改属性值,属性值的改变是双向同步的
- 若父组件 new 了新的实例,则不会同步给子组件
- 若子组件 new 了新的实例,则后续对象属性值的改变,也不会同步回父组件
@Event: 规范组件输出
@Event 用于装饰组件对外输出的方式,主要用于配合 @Param 实现数据的双向同步。
由于 @Param 装饰的变量在本地无法更改,使用 @Event 装饰器装饰回调方法并调用,可实现子组件向父组件要求更新数据源的变量,再通过 @Local 的同步机制,将修改同步回 @Param 装饰的变量,以达到主动更新 @Param 装饰变量的效果。
@Param 标志着组件的输入,表明该变量受父组件影响,而 @Event 标志着组件的输出,可以通过该方法影响父组件。
@Event 只能在 @ComponentV2 装饰的自定义组件中使用,若装饰非方法类型的变量,不会有任何作用。
| @Event 装饰器 | 说明 |
|---|---|
| 装饰器参数 | 无 |
| 可装饰的变量类型 | 回调方法,如:()=>void、(x: number)=>boolean等 |
| 可传入的函数类型 | 箭头函数 |
| 初始化 | 优先用外部传入的值,否则使用本地默认值,若没有初始化,会自动生成一个空的函数作为默认的回调 |
需要注意的是,使用 @Event 修改父组件的值是立刻生效的,但父组件将变化同步回子组件的过程是异步的,即在调用完 @Event 的方法后,子组件内的值不会立刻变化。
这是因为 @Event 将子组件实际的变化能力交由父组件处理,在父组件实际决定如何处理后,将最终值在渲染前同步回子组件
@Entry@ComponentV2exportstruct Comp{@Localcount:number=0;build(){Column({space:20}){Text('parent count: '+this.count)ChildComp({count:this.count,changeFactory:()=>{this.count++;}})}}}@ComponentV2exportstruct ChildComp{@Paramcount:number=10;@EventchangeFactory:()=>void;build(){Column({space:20}){Text('child: '+this.count)Button('child change').onClick(()=>{// param 被 @Param 装饰,不可在本地修改,需通过 @Event 装饰的回调函数,通知父组件// this.count ++; //编译报错this.changeFactory();})}}}
- @ComponentV2 内的回调函数,是私有的,不接受外部传入,若不用 @Event,则需使用 @Param 装饰,父组件才可传值
- @Param 装饰的回调函数,则失去了 @Event 提供的类型约束、默认空函数兜底、编译校验
@Provider 和 @Consumer: 跨组件层级双向同步
@Provider 和 @Consumer 用于跨组件层级数据双向同步,只能在 @ComponentV2 中使用。
- @Provider,即数据提供方,其所有的子组件都可以通过 @Consumer 绑定相同的 key 来获取 @Provider 提供的数据
- @Consumer,即数据消费方,可通过绑定同样的 key 获取其最近父节点的 @Provider 的数据 (@Provider 可以重名),必须本地初始化,若查找不到对应的 @Provider,则使用本地默认值
@Provider 装饰器
| @Provider 装饰器 | 说明 |
|---|---|
| 装饰器参数 | aliasName,别名,作为与 @Comsumer 匹配的 key,缺省时默认为属性名 |
| 支持类型 | 自定义组件中成员变量,number、string、boolean、class、Array、Date、Map、Set 等类型,支持箭头函数 |
| 初始化 | 必须本地初始化,禁止从父组件初始化 |
| 观察能力 | 能力等同于 @Trace,变化会同步给对应的 @Consumer |
@Consumer 装饰器
| @Consumer 装饰器 | 说明 |
|---|---|
| 装饰器参数 | aliasName,别名,向上查找 @Provider 匹配的 key,缺省时默认为属性名 |
| 支持类型 | 自定义组件中成员变量,number、string、boolean、class、Array、Date、Map、Set 等类型,支持箭头函数 |
| 初始化 | 必须本地初始化,禁止从父组件初始化 |
| 观察能力 | 能力等同于 @Trace,变化会同步给对应的 @Provider |
aliasName 是用于 @Provider 和 @Consumer 进行匹配的唯一指定 key,缺省时默认为属性名
- @Provider 和 @Consumenr 装饰的数据类型需一致
- @Provider 和 @Consumenr 强依赖自定义组件层级,@Provider 可以重名,@Consumer 以最近父节点的 @Provider 的数据初始化,即 @Consumer 会因为所在组件的父组件不同,而被初始化为不同的值
- @Provider 和 @Consumenr 相当于把组件粘合在一起了,从组件独立的角度,应减少使用
- @Provider 和 @Consumenr 只支持本地初始化,禁止从父组件初始化,但可用于初始化子组件中 @Param 装饰的变量
V2 的 @Provider/@Consumer 和 V1 的 @Provide/@Consume 对比
| 能力 | V2 装饰器 @Provider/@Consumer | V1 装饰器 @Provide/@Consume |
|---|---|---|
@Consume(r) | 必须本地初始化,当找不到 @Provider 时使用本地默认值 | API20 之前,禁止本地初始化;API 20 开始,支持设置默认值,若没有设置默认值,且找不到对应的 @Provide 时,会抛出异常 |
@Provide(r) | 不允许从父组件初始化 | 允许从父组件初始化 |
| 默认开启重载,即@Provider 可以重名,@Consumer 向上查找最近的 @Provider | 默认关闭,即不允许有同名的 @Provide,若需重载,需搭配allowOverride | |
| 匹配的 key | alias是唯一匹配的 key,缺省时默认属性名为 alias | alias和属性名都可为 key,优先匹配 alias,匹配不到则匹配属性名 |
| 观察能力 | 仅能观察自身赋值变化,若需观察嵌套场景,需配合@Trace使用 | 观察第一层变化,若需观察嵌套场景,需配合@Observed和@ObjectLink一起使用 |
| 是否支持 function | 支持 | 不支持 |
@Provider/@Consumer 装饰复杂类型
@Provider 和 @Consumer 只能观察到数据本身的变化,若需观察复杂数据类型的属性变化,可配置 @Trace 一起使用,或通过 makeObserved 将非可观察数据变为可观察数据
@ObservedV2classInfo{@Tracename:string='assassin';@Traceage:number=20;constructor(name:string,age:number){this.name=name;this.age=age;}}@Entry@ComponentV2exportstruct Comp{@Provider('user')info:Info=newInfo('Assassin',20)build(){Column({space:20}){Text('parent: '+this.info.name+'_'+this.info.age)Button('parent change').onClick(()=>{// Info 被 @ObservedV2 装饰,且 age 属性被 @Trace 装饰,其变化可触发 UI 刷新this.info.age++;})// 子组件ChildComp()}}}@ComponentV2exportstruct ChildComp{// 父组件有 'user',使用 @Provider 的值初始化@Consumer('user')info:Info=newInfo('child',10);build(){Column({space:20}){Text('child: '+this.info.name+'_'+this.info.age)Button('child change').onClick(()=>{// @Provider 和 @Consumenr 双向同步this.info.age++;})}}}@Provider/@Consumer 装饰箭头函数
@Entry@ComponentV2exportstruct Comp{@LocalchildX:number=0;@LocalchildY:number=0;@Provider()//aliasName 缺省,使用变量名作为 aliasNameonDrag:(x:number,y:number)=>void=(x:number,y:number)=>{console.log(`onDrag event x:${x}, y:${y}`);this.childX=x;this.childY=y;}build(){Column({space:20}){Text(`child position, x:${this.childX}, y:${this.childY}`)ChildComp()}}}@ComponentV2exportstruct ChildComp{// 子组件通过调用回调函数,将拖拽的坐标信息同步回父组件@Consumer()onDrag:(x:number,y:number)=>void=(x:number,y:number)=>{}build(){Button('drag').draggable(true).onDragStart((event:DragEvent)=>{// 当前预览器上不支通用拖拽事件this.onDrag(event.getDisplayX(),event.getGlobalDisplayX());})}}跨 BuilderNode 下 @Provider 和 @Consumer 双向同步
从 API 23 开始,支持跨 BuilderNode 配对 @Provider 和 @Consumer
- 默认情况下,@Provider/@Consumer 可能无法跨越 BuilderNode 边界进行同步,需在创建 BuilderNode 时,通过配置 BuildOptions 中的
enableProvideConsumeCrossing: true,以允许状态同步跨越 BuilderNode 边界,支持跨 BuilderNode 配对 @Provider 和 @Consumer - BuilderNode 内部定义的 @Consumer 必须设置一个合法的默认值,应避免 @Consumer 装饰对象实例,否则会导致重复创建、独立实例而无法同步
- BuilderNode 节点需通过 NodeController 添加到组件树后,其内部的 @Consumer 才会尝试向上匹配最近的 @Provide,建立双向同步关系;若匹配不到,则 @Consumer 使用默认值
- 调用
removeChild移除子节点后,子节点从组件树卸载,子组件内的 @Consumer 会再次视图查找对应的 @Provider,若组件树卸载后无法找到匹配的 @Provider,则断开和 @Provider 的双向同步关系,@Consumer 装饰的变量恢复成默认值 - 调用
dispose释放 BuilderNode 节点,该节点销毁,会触发子组件的 aboutToDisappear 回调
import{BuilderNode,FrameNode,NodeController,UIContext}from"@kit.ArkUI";@ComponentV2struct TestNode{@Consumer()content:string='default value';// 设置字符串默认值,而非对象实例// 监听 content 的变化@Monitor('content')consumerWatch(){console.info(`consumer change${this.content}`)}// 节点卸载后,生命周期回调aboutToDisappear():void{console.log('TestNode aboutToDisappear');}build(){Column({space:20}){Text('Consumer: '+this.content)Button('Consumer change').onClick(()=>{this.content+='con_'})}}}@BuilderfunctionbuildText(){TestNode()}letglobalBuilderNode:BuilderNode<[]>|null=null;classTextNodeControllerextendsNodeController{privaterootNode:FrameNode|null=null;privateuiContext:UIContext|null=null;constructor(){super();}makeNode(context:UIContext):FrameNode|null{this.rootNode=newFrameNode(context);this.uiContext=context;returnthis.rootNode;}addBuildNode():void{if(globalBuilderNode==null&&this.uiContext){globalBuilderNode=newBuilderNode(this.uiContext);// 构建 BuilderNode,TestNode 作为子组件// enableProvideConsumeCrossing 设为 true,支持 @Provider/@Consumer 跨组件同步globalBuilderNode.build(wrapBuilder<[]>(buildText),undefined,{enableProvideConsumeCrossing:true});}if(this.rootNode&&globalBuilderNode){// 添加子组件this.rootNode.appendChild(globalBuilderNode.getFrameNode());}}// 移除子节点 (TestNode 组件)removeBuilderNode():void{if(this.rootNode&&globalBuilderNode){this.rootNode.removeChild(globalBuilderNode.getFrameNode());}}// 释放 BuilderNode 子节点 (TestNode),随后该节点销毁,触发子节点的 aboutToDisappear 回调disposeNode():void{if(this.rootNode&&globalBuilderNode){globalBuilderNode.dispose();}}}@Entry@ComponentV2exportstruct ProviderComp{@Provider()content:string='content';// 监听 content 的变化@Monitor('content')providerWatch(){console.info(`provider change${this.content}`)}nodeController:TextNodeController=newTextNodeController();build(){Column({space:20}){Text(`Provider:${this.content}`)// 添加 BuilderNode, @Consumer 与 @Consumer 建立双向同步Button('add child node').onClick(()=>{this.nodeController.addBuildNode();})// 移除 BuilderNode,@Consumer 与 @Provider 断开连接,恢复默认值Button('remove child node').onClick(()=>{this.nodeController.removeBuilderNode();})// 释放 BuilderNode 子节点 (TestNode),随后该节点销毁,触发子节点的 aboutToDisappear 回调Button('dispose child node').onClick(()=>{this.nodeController.disposeNode();})// @Provider/@Consumer双向同步更新Button('change Provider').onClick(()=>{this.content+='Pro_';})// 子节点NodeContainer(this.nodeController)}.width('100%')}}