21 — 状态管理 V1 装饰器体系
一、引言
ArkUI 声明式开发的核心是"状态驱动 UI":状态变化自动触发依赖该状态的组件刷新。在 HarmonyOS 5.0 之前的版本,这套能力由状态管理 V1 装饰器承担,包括 @State、@Prop、@Link、@Provide、@Consume、@ObjectLink、@Watch 等。本项目(multi-short-video)已全面升级为 V2 装饰器体系(见文章 22),但掌握 V1 仍然是必要的:一方面,存量工程与历史三方库大量使用 V1;另一方面,V1 的"单向数据流 + 就近管理"思想是理解 V2 演进的地基。本文结合项目场景,逐一定位 V1 各装饰器的职责与传递规则,并指出常见状态不同步问题的根因。
二、@State:组件内状态的最小单元
@State 装饰的变量由组件自身管理,其变化会触发该组件 build 函数的重新执行。以视频页features/multishortvideoadaptivevideo/src/main/ets/view/AdaptiveVideo.ets为例,页面需要跟踪当前视频下标、评论弹层开关、播放状态等;该项目文件使用 V2 语法,等价的 V1 写法如下:
// 对照示例:AdaptiveVideo.ets 相关状态的 V1 等价写法@Entry@Componentstruct AdaptiveVideo{@StatecurIndex:number=0;@StateshowComment:boolean=false;@StatecurrentState:string='idle';....onAnimationStart((index:number,targetIndex:number)=>{this.curIndex=targetIndex;// 直接赋值即可触发刷新this.currentTime=0;})}要点:@State 只作用于组件内部,且初始化只能使用本地初始值,不能依赖父组件传入的参数。在 Swiper 切换动画回调中直接修改this.curIndex,V1 会自动最小化刷新绑定该变量的 UI 节点。
三、@Prop 与 @Link:父子组件的单向与双向同步
父子组件传参是状态管理最频繁的场景。V1 中:
- @Prop 建立单向同步:父组件数据变化 → 子组件 @Prop 同步更新;子组件内部修改 @Prop 不会回传父组件。适合"只读展示"型参数。
- @Link 建立双向同步:父子共享同一数据源,任一侧修改都会同步到另一侧。适合"需要子组件回写"的参数。
以播放器组件AdaptiveAVPlayer.ets为例,父组件 AdaptiveVideo 需要把当前播放下标、视频源、拖拽目标时间传给子组件,V1 等价写法:
// 对照示例:AdaptiveAVPlayer.ets 入参的 V1 等价写法@Componentexportstruct AdaptiveAVPlayer{@PropcurrentIndex:number=-1;// 父传子,单向同步@Propindex:number=0;@PropcurrentSource:string='';@PropseekToTime:number=-1;// 拖拽进度由父组件写入...@Linkduration:number;// 与父组件共享时长,双向同步}需要注意:@Prop 的深拷贝特性决定了它对嵌套对象只同步一层;@Link 则要求变量类型一致且不能就地初始化(由父组件传入)。项目中将"是否正在拖拽、目标时间"等状态统一放在父组件 AdaptiveVideo,通过 @Prop 单向下发,避免了子组件随意回写导致的职责混乱。
四、@Provide 与 @Consume:跨层级共享
当状态需要在多层组件间传递时,逐层 @Prop 会形成"属性钻透"。V1 提供 @Provide/@Consume 实现跨层级共享:祖先组件 @Provide 提供数据,任意层级后代 @Consume 消费同名数据,中间层无需感知。本项目首页products/default/src/main/ets/view/Index.ets将深色模式、路由栈、侧栏开关声明在根部,供视频页、评论页等多层组件消费,V1 等价写法:
// 对照示例:Index.ets 根组件的 V1 等价写法@Entry@Componentstruct Index{@Provide('isDark')isDark:boolean=false;@Provide('pathStack')pathStack:NavPathStack=newNavPathStack();@Provide('showSideComment')showSideComment:boolean=false;@Provide('showSideIndividual')showSideIndividual:boolean=false;...}消费侧(如 AdaptiveVideo.ets 的等价写法):
@Consume('showSideComment')showSideComment:boolean;@Consume('pathStack')pathStack:NavPathStack;当视频页切换页签需要收起侧栏时,直接修改this.showSideComment,根组件与所有消费方同步刷新。@Provide 默认向全部后代可见,若只想对直接子组件可见,可配合 @Provide 的别名机制控制作用域。
五、@ObjectLink 与 @Watch:对象级观察与监听
- @ObjectLink 装饰 class 类型(需配合 @Observed 类装饰器),对对象内部属性变化做深度观察,常用于数组项或嵌套对象传入子组件的场景。
- @Watch 为状态变化添加回调,在数据变化后触发业务逻辑(如保存、上报),但 @Watch 不提供变化前后的值。
以评论数据features/multishortvideocomment/src/main/ets/model/CommentDataModel.ets为例,若使用 V1 体系,评论对象可声明为 @Observed class,列表项子组件用 @ObjectLink 接收,点赞数等属性就地修改即可精准刷新单条评论,而不是整表重绘:
@ObservedexportclassCommentDataModel{publiclikes:number=0;publiccontent:string='';...}// 子组件内@Componentexportstruct CommentItem{@ObjectLinkitem:CommentDataModel;// 深度观察对象属性@Watch('onLikesChange')@Proplikes:number=0;onLikesChange(){// 点赞数变化后的业务逻辑}}V1 装饰器能力汇总如下表:
| 装饰器 | 同步方向 | 支持类型 | 典型场景 |
|---|---|---|---|
| @State | 组件内 | 基本类型、对象、数组 | 页面局部状态 |
| @Prop | 父 → 子(单向) | 基本类型、对象一层 | 只读入参 |
| @Link | 父子双向 | 与父类型一致 | 需要回写的共享值 |
| @Provide/@Consume | 祖先 → 后代 | 任意 | 跨多层共享 |
| @ObjectLink | 父 → 子(深度) | @Observed 对象 | 数组项、嵌套对象 |
| @Watch | 监听回调 | 任意 @State 等 | 变化后触发逻辑 |
六、V1 常见状态不同步问题排查
实践中状态不同步多源于以下几类:
- 对象内部属性不刷新:普通 class 对象属性变化不会触发 UI 更新,必须配合 @Observed + @ObjectLink,或整体替换对象引用。
- @Prop 修改不回传:误以为 @Prop 是双向的,在子组件修改后父组件无感知。需要双向时改用 @Link。
- @Provide/@Consume 名称不匹配:两侧 key 必须完全一致(含别名写法),否则编译期不报错但运行时不生效。
- ForEach/Repeat 渲染键不稳定:用 JSON.stringify(item) 生成键(项目在
.key((item: AvDataSourceModel) => JSON.stringify(item))中采用),避免索引键导致的状态错位。 - 在非 UI 线程/回调中修改状态:某些异步回调(如 AVPlayer 的 stateChange)中直接赋值可能错过刷新时机,应回到 UI 上下文再赋值。
七、总结与最佳实践
- 状态就近声明:只被单个组件使用的状态用 @State,避免无谓提升。
- 参数只读优先 @Prop,需要回写才用 @Link;跨层共享优先 @Provide/@Consume,减少逐层透传。
- 对象级数据用 @Observed + @ObjectLink,保证细粒度刷新。
- 需要感知状态变化执行副作用(保存、埋点、联动播放器)时使用 @Watch。
- V1 的"就近管理"思想是 V2 的基石;新项目建议直接采用 V2(下一章详解),存量工程按需迁移。
注:文中 V1 示例均为项目既有场景的等价对照写法,multi-short-video 实际代码已采用 V2 装饰器,迁移思路见文章 22。