上周帮同事排查一个 HarmonyOS6 PC 项目的 bug他在一个表单页面里改了用户对象的name属性结果界面上死活不刷新。他对着屏幕嘀咕了半天我明明改了啊为什么不变我凑过去一看——典型的 State 对象更新踩坑。这个坑几乎每个从前端转 HarmonyOS6 PC 开发的人都会踩一次。今天就把这个问题彻底讲透顺便聊聊解决方案。问题根源State 到底在监听什么先回到 State 的工作原理。当你写State user: UserProfile new UserProfile()的时候框架监听的不是user这个对象内部的每一个属性而是user这个引用本身。换句话说框架关心的问题是user 指向的对象还是原来那个吗“而不是user 里面的 name 变了吗”这就导致了以下行为// 这样操作UI 会更新 —— 引用变了this.usernewUserProfile()// 这样操作UI 不会更新 —— 引用没变this.user.name新名字坦白讲这个设计一开始让我挺不适应的。在 Vue 里用reactive包一个对象内部属性变化自动追踪在 React 里虽然也需要 immutable 更新但至少有useState配合展开运算符还算方便。ArkUI 的 State 对对象的处理方式更原始一些——它只认引用变化。踩坑现场一个用户信息卡片来看具体的案例。假设我们在做一个 PC 端的用户资料编辑页面classUserProfile{name:string张三age:number25level:number1}EntryComponentstruct StateObjectDemo{Stateuser:UserProfilenewUserProfile()build(){Column(){Text(用户信息).fontSize(18).fontWeight(FontWeight.Bold)Column({space:8}){Row(){Text(姓名: ).fontSize(13);Text(this.user.name).fontSize(13)}Row(){Text(年龄: ).fontSize(13);Text(this.user.age.toString()).fontSize(13)}Row(){Text(等级: ).fontSize(13);Text(this.user.level.toString()).fontSize(13)}}.width(100%).padding(16).backgroundColor(#F8F9FA).borderRadius(8)Button(改名).onClick((){// 踩坑写法直接修改属性UI 不更新// this.user.name this.user.name 张三 ? 李四 : 张三// 正确写法创建新对象替换整个引用constnewUsernewUserProfile()newUser.namethis.user.name张三?李四:张三newUser.agethis.user.age newUser.levelthis.user.levelthis.usernewUser})}}}看到那个踩坑写法的注释了吗那就是我同事写出来的代码。this.user.name 新名字这行代码执行了对象的属性确实变了但框架压根不知道——因为你没动引用框架觉得对象还是原来那个对象。正确姿势整体替换对象解决 State 对象不更新的问题最朴素的方法就是每次修改都创建一个全新的对象然后整体赋值。Button(增长年龄).onClick((){constnewUsernewUserProfile()newUser.namethis.user.name newUser.agethis.user.age1newUser.levelthis.user.levelthis.usernewUser})Button(升级).onClick((){constnewUsernewUserProfile()newUser.namethis.user.name newUser.agethis.user.age newUser.levelthis.user.level1this.usernewUser})说实话这代码写起来挺烦的。每改一个字段都得把整个对象重新构造一遍属性多了简直就是噩梦。一个用户信息可能有二十几个字段每次改个昵称都要复制粘贴一堆代码。但这个方法有个好处——一定能触发更新。因为引用变了框架能感知到。用字面量对象简化代码如果你不想定义 class用字面量对象也行写起来稍微简洁一点EntryComponentstruct LiteralObjectDemo{Stateuser:Recordstring,string|number{name:张三,age:25,level:1}build(){Column(){Text(姓名:${this.user[name]}).fontSize(14)Text(年龄:${this.user[age]}).fontSize(14)Text(等级:${this.user[level]}).fontSize(14)Button(改名).onClick((){// 展开运算符创建新对象this.user{age:this.user[age],level:this.user[level],name:李四}})Button(增长年龄).onClick((){this.user{level:this.user[level],name:李四,age:(this.user[age]asnumber)1}})}}}用展开运算符...来创建新对象比手动 new 一个 class 再逐个赋值优雅多了。不过这种方式牺牲了类型安全——Recordstring, string | number远不如一个明确定义的 class 来得清晰。真正优雅的方案Observed 类ArkUI 其实提供了一个专门解决这个问题的装饰器——Observed。给你的 class 加上 Observed框架就会深度追踪这个类实例的每一个属性变化。ObservedclassUserProfile{name:string张三age:number25level:number1}EntryComponentstruct ObservedDemo{Stateuser:UserProfilenewUserProfile()build(){Column(){Text(用户信息 (使用 Observed)).fontSize(18).fontWeight(FontWeight.Bold)Column({space:8}){Row(){Text(姓名: ).fontSize(13);Text(this.user.name).fontSize(13)}Row(){Text(年龄: ).fontSize(13);Text(this.user.age.toString()).fontSize(13)}Row(){Text(等级: ).fontSize(13);Text(this.user.level.toString()).fontSize(13)}}.width(100%).padding(16).backgroundColor(#F8F9FA).borderRadius(8)Row({space:8}){Button(改名).onClick((){// 现在直接修改属性也能触发更新了this.user.namethis.user.name张三?李四:张三})Button(增长年龄).onClick((){this.user.age1})Button(升级).onClick((){this.user.level1})}.width(100%).justifyContent(FlexAlign.SpaceEvenly).margin({top:12})}.width(100%).padding(16)}}看到了吗加了 Observed 之后this.user.name 新名字就能直接触发 UI 更新了再也不用每次创建新对象了。这才是真正符合直觉的写法。Observed 的使用条件不过 Observed 也不是万能的它有几个前提条件只能用在 class 上不能用在字面量对象或Record类型上。必须和 State 配合使用。Observed 标记 class 是可观察的State 持有这个 class 的实例两者缺一不可。嵌套对象需要层层标记。如果你的 class 里面还嵌套了另一个 class内层的 class 也得加 Observed否则内层对象的变化还是追踪不到。嵌套的例子ObservedclassAddress{city:string北京street:string长安街1号}ObservedclassUserProfile{name:string张三address:AddressnewAddress()}EntryComponentstruct NestedObservedDemo{Stateuser:UserProfilenewUserProfile()build(){Column(){Text(this.user.name).fontSize(16)Text(this.user.address.city).fontSize(14)Button(修改城市).onClick((){// Address 也标记了 Observed所以这里能触发更新this.user.address.city上海})}}}如果Address没有加 Observed修改this.user.address.city就不会触发更新。这个嵌套追踪的逻辑要牢记。在 PC 端的实际应用场景PC 端的表单场景比移动端复杂得多。一个设置页面可能有十几个字段还有各种联动关系。来看看 Observed 在实际 PC 项目中的用法。场景一多字段表单PC 端的个人资料编辑页通常有很多输入框。用 Observed 包装一个表单模型类每个字段都能直接修改ObservedclassProfileForm{nickname:stringemail:stringbio:stringphone:stringgender:string男birthday:string2000-01-01}EntryComponentstruct ProfileEditDemo{Stateform:ProfileFormnewProfileForm()StateisSaved:booleanfalsebuild(){Column(){Text(编辑个人资料).fontSize(18).fontWeight(FontWeight.Bold)Column({space:12}){TextInput({text:this.form.nickname,placeholder:昵称}).onChange((v:string){this.form.nicknamev})TextInput({text:this.form.email,placeholder:邮箱}).onChange((v:string){this.form.emailv})TextInput({text:this.form.bio,placeholder:个人简介}).onChange((v:string){this.form.biov})}.width(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(12)// 实时预览Column(){Text(预览).fontSize(14).fontWeight(FontWeight.Medium)Text(昵称:${this.form.nickname||未填写}).fontSize(13)Text(邮箱:${this.form.email||未填写}).fontSize(13)Text(简介:${this.form.bio||未填写}).fontSize(13)}.width(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(12).margin({top:10})Button(this.isSaved?已保存:保存).width(100%).margin({top:12}).backgroundColor(this.isSaved?#6BCB77:#007DFF).onClick((){this.isSavedtruesetTimeout((){this.isSavedfalse},2000)})}.width(100%).height(100%).backgroundColor(#F5F6FA).padding(16)}}每个 TextInput 的 onChange 里直接修改this.form.xxx下面的预览区同步更新。代码清爽逻辑清晰。场景二复杂数据表格PC 端最常见的就是各种数据表格。一行数据往往就是一个对象编辑某一行的某个字段时需要精准更新ObservedclassTableRow{id:numbername:stringstatus:stringscore:numberconstructor(id:number,name:string,status:string,score:number){this.ididthis.namenamethis.statusstatusthis.scorescore}}EntryComponentstruct TableDemo{Staterows:TableRow[][newTableRow(1,项目A,进行中,85),newTableRow(2,项目B,已完成,92),newTableRow(3,项目C,待审核,78)]build(){Column(){Text(项目数据表).fontSize(18).fontWeight(FontWeight.Bold)ForEach(this.rows,(row:TableRow){Row(){Text(row.name).fontSize(13).width(80)Text(row.status).fontSize(13).width(80).fontColor(row.status已完成?#6BCB77:#FF8E53)Text(row.score.toString()).fontSize(13).width(50)Button(row.status已完成?重新打开:标记完成).fontSize(12).onClick((){// Observed 让直接修改属性成为可能row.statusrow.status已完成?进行中:已完成})}.width(100%).padding(10).backgroundColor(#F8F9FA).borderRadius(6).margin({top:6})})}.width(100%).padding(16).backgroundColor(#F5F6FA)}}在表格里直接点击按钮修改某一行的状态UI 即时更新。Observed 让数组内的对象属性修改也能被追踪到这在 PC 端的数据管理场景中非常实用。性能考量Observed 的代价Observed 虽然好用但它也不是免费的午餐。框架需要对 Observed 类的每一个属性做代理拦截本质上是用 Proxy 或类似机制这比普通的 State 引用追踪开销更大。几个建议只有确实需要深度追踪的类才加 Observed。如果一个对象创建后就不会修改内部属性直接用 State 就够了。避免深层嵌套的 Observed。三四层嵌套的对象链每一层都 Observed性能开销会叠加。大列表场景慎用。如果你的表格有上千行每行都是 Observed 对象可能需要在虚拟滚动和按需加载上做文章。在 PC 端硬件性能通常比手机强所以 Observed 的性能开销一般不是问题。但养成良好的编码习惯总是好的。总结一下踩坑经验回头看看这次的踩坑经历核心教训就三条State 对对象类型只追踪引用不追踪内部属性变化。直接this.user.name xxx不会触发 UI 更新。朴素的解决方案是每次创建新对象整体替换但代码写起来很啰嗦。最优雅的方案是给 class 加上 Observed 装饰器让框架深度追踪属性变化。嵌套对象需要每一层都标记。搞懂了这些你在 HarmonyOS6 PC 开发中处理对象状态就不会再踩坑了。
@State踩坑实录:对象类型状态更新的正确姿势
上周帮同事排查一个 HarmonyOS6 PC 项目的 bug他在一个表单页面里改了用户对象的name属性结果界面上死活不刷新。他对着屏幕嘀咕了半天我明明改了啊为什么不变我凑过去一看——典型的 State 对象更新踩坑。这个坑几乎每个从前端转 HarmonyOS6 PC 开发的人都会踩一次。今天就把这个问题彻底讲透顺便聊聊解决方案。问题根源State 到底在监听什么先回到 State 的工作原理。当你写State user: UserProfile new UserProfile()的时候框架监听的不是user这个对象内部的每一个属性而是user这个引用本身。换句话说框架关心的问题是user 指向的对象还是原来那个吗“而不是user 里面的 name 变了吗”这就导致了以下行为// 这样操作UI 会更新 —— 引用变了this.usernewUserProfile()// 这样操作UI 不会更新 —— 引用没变this.user.name新名字坦白讲这个设计一开始让我挺不适应的。在 Vue 里用reactive包一个对象内部属性变化自动追踪在 React 里虽然也需要 immutable 更新但至少有useState配合展开运算符还算方便。ArkUI 的 State 对对象的处理方式更原始一些——它只认引用变化。踩坑现场一个用户信息卡片来看具体的案例。假设我们在做一个 PC 端的用户资料编辑页面classUserProfile{name:string张三age:number25level:number1}EntryComponentstruct StateObjectDemo{Stateuser:UserProfilenewUserProfile()build(){Column(){Text(用户信息).fontSize(18).fontWeight(FontWeight.Bold)Column({space:8}){Row(){Text(姓名: ).fontSize(13);Text(this.user.name).fontSize(13)}Row(){Text(年龄: ).fontSize(13);Text(this.user.age.toString()).fontSize(13)}Row(){Text(等级: ).fontSize(13);Text(this.user.level.toString()).fontSize(13)}}.width(100%).padding(16).backgroundColor(#F8F9FA).borderRadius(8)Button(改名).onClick((){// 踩坑写法直接修改属性UI 不更新// this.user.name this.user.name 张三 ? 李四 : 张三// 正确写法创建新对象替换整个引用constnewUsernewUserProfile()newUser.namethis.user.name张三?李四:张三newUser.agethis.user.age newUser.levelthis.user.levelthis.usernewUser})}}}看到那个踩坑写法的注释了吗那就是我同事写出来的代码。this.user.name 新名字这行代码执行了对象的属性确实变了但框架压根不知道——因为你没动引用框架觉得对象还是原来那个对象。正确姿势整体替换对象解决 State 对象不更新的问题最朴素的方法就是每次修改都创建一个全新的对象然后整体赋值。Button(增长年龄).onClick((){constnewUsernewUserProfile()newUser.namethis.user.name newUser.agethis.user.age1newUser.levelthis.user.levelthis.usernewUser})Button(升级).onClick((){constnewUsernewUserProfile()newUser.namethis.user.name newUser.agethis.user.age newUser.levelthis.user.level1this.usernewUser})说实话这代码写起来挺烦的。每改一个字段都得把整个对象重新构造一遍属性多了简直就是噩梦。一个用户信息可能有二十几个字段每次改个昵称都要复制粘贴一堆代码。但这个方法有个好处——一定能触发更新。因为引用变了框架能感知到。用字面量对象简化代码如果你不想定义 class用字面量对象也行写起来稍微简洁一点EntryComponentstruct LiteralObjectDemo{Stateuser:Recordstring,string|number{name:张三,age:25,level:1}build(){Column(){Text(姓名:${this.user[name]}).fontSize(14)Text(年龄:${this.user[age]}).fontSize(14)Text(等级:${this.user[level]}).fontSize(14)Button(改名).onClick((){// 展开运算符创建新对象this.user{age:this.user[age],level:this.user[level],name:李四}})Button(增长年龄).onClick((){this.user{level:this.user[level],name:李四,age:(this.user[age]asnumber)1}})}}}用展开运算符...来创建新对象比手动 new 一个 class 再逐个赋值优雅多了。不过这种方式牺牲了类型安全——Recordstring, string | number远不如一个明确定义的 class 来得清晰。真正优雅的方案Observed 类ArkUI 其实提供了一个专门解决这个问题的装饰器——Observed。给你的 class 加上 Observed框架就会深度追踪这个类实例的每一个属性变化。ObservedclassUserProfile{name:string张三age:number25level:number1}EntryComponentstruct ObservedDemo{Stateuser:UserProfilenewUserProfile()build(){Column(){Text(用户信息 (使用 Observed)).fontSize(18).fontWeight(FontWeight.Bold)Column({space:8}){Row(){Text(姓名: ).fontSize(13);Text(this.user.name).fontSize(13)}Row(){Text(年龄: ).fontSize(13);Text(this.user.age.toString()).fontSize(13)}Row(){Text(等级: ).fontSize(13);Text(this.user.level.toString()).fontSize(13)}}.width(100%).padding(16).backgroundColor(#F8F9FA).borderRadius(8)Row({space:8}){Button(改名).onClick((){// 现在直接修改属性也能触发更新了this.user.namethis.user.name张三?李四:张三})Button(增长年龄).onClick((){this.user.age1})Button(升级).onClick((){this.user.level1})}.width(100%).justifyContent(FlexAlign.SpaceEvenly).margin({top:12})}.width(100%).padding(16)}}看到了吗加了 Observed 之后this.user.name 新名字就能直接触发 UI 更新了再也不用每次创建新对象了。这才是真正符合直觉的写法。Observed 的使用条件不过 Observed 也不是万能的它有几个前提条件只能用在 class 上不能用在字面量对象或Record类型上。必须和 State 配合使用。Observed 标记 class 是可观察的State 持有这个 class 的实例两者缺一不可。嵌套对象需要层层标记。如果你的 class 里面还嵌套了另一个 class内层的 class 也得加 Observed否则内层对象的变化还是追踪不到。嵌套的例子ObservedclassAddress{city:string北京street:string长安街1号}ObservedclassUserProfile{name:string张三address:AddressnewAddress()}EntryComponentstruct NestedObservedDemo{Stateuser:UserProfilenewUserProfile()build(){Column(){Text(this.user.name).fontSize(16)Text(this.user.address.city).fontSize(14)Button(修改城市).onClick((){// Address 也标记了 Observed所以这里能触发更新this.user.address.city上海})}}}如果Address没有加 Observed修改this.user.address.city就不会触发更新。这个嵌套追踪的逻辑要牢记。在 PC 端的实际应用场景PC 端的表单场景比移动端复杂得多。一个设置页面可能有十几个字段还有各种联动关系。来看看 Observed 在实际 PC 项目中的用法。场景一多字段表单PC 端的个人资料编辑页通常有很多输入框。用 Observed 包装一个表单模型类每个字段都能直接修改ObservedclassProfileForm{nickname:stringemail:stringbio:stringphone:stringgender:string男birthday:string2000-01-01}EntryComponentstruct ProfileEditDemo{Stateform:ProfileFormnewProfileForm()StateisSaved:booleanfalsebuild(){Column(){Text(编辑个人资料).fontSize(18).fontWeight(FontWeight.Bold)Column({space:12}){TextInput({text:this.form.nickname,placeholder:昵称}).onChange((v:string){this.form.nicknamev})TextInput({text:this.form.email,placeholder:邮箱}).onChange((v:string){this.form.emailv})TextInput({text:this.form.bio,placeholder:个人简介}).onChange((v:string){this.form.biov})}.width(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(12)// 实时预览Column(){Text(预览).fontSize(14).fontWeight(FontWeight.Medium)Text(昵称:${this.form.nickname||未填写}).fontSize(13)Text(邮箱:${this.form.email||未填写}).fontSize(13)Text(简介:${this.form.bio||未填写}).fontSize(13)}.width(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(12).margin({top:10})Button(this.isSaved?已保存:保存).width(100%).margin({top:12}).backgroundColor(this.isSaved?#6BCB77:#007DFF).onClick((){this.isSavedtruesetTimeout((){this.isSavedfalse},2000)})}.width(100%).height(100%).backgroundColor(#F5F6FA).padding(16)}}每个 TextInput 的 onChange 里直接修改this.form.xxx下面的预览区同步更新。代码清爽逻辑清晰。场景二复杂数据表格PC 端最常见的就是各种数据表格。一行数据往往就是一个对象编辑某一行的某个字段时需要精准更新ObservedclassTableRow{id:numbername:stringstatus:stringscore:numberconstructor(id:number,name:string,status:string,score:number){this.ididthis.namenamethis.statusstatusthis.scorescore}}EntryComponentstruct TableDemo{Staterows:TableRow[][newTableRow(1,项目A,进行中,85),newTableRow(2,项目B,已完成,92),newTableRow(3,项目C,待审核,78)]build(){Column(){Text(项目数据表).fontSize(18).fontWeight(FontWeight.Bold)ForEach(this.rows,(row:TableRow){Row(){Text(row.name).fontSize(13).width(80)Text(row.status).fontSize(13).width(80).fontColor(row.status已完成?#6BCB77:#FF8E53)Text(row.score.toString()).fontSize(13).width(50)Button(row.status已完成?重新打开:标记完成).fontSize(12).onClick((){// Observed 让直接修改属性成为可能row.statusrow.status已完成?进行中:已完成})}.width(100%).padding(10).backgroundColor(#F8F9FA).borderRadius(6).margin({top:6})})}.width(100%).padding(16).backgroundColor(#F5F6FA)}}在表格里直接点击按钮修改某一行的状态UI 即时更新。Observed 让数组内的对象属性修改也能被追踪到这在 PC 端的数据管理场景中非常实用。性能考量Observed 的代价Observed 虽然好用但它也不是免费的午餐。框架需要对 Observed 类的每一个属性做代理拦截本质上是用 Proxy 或类似机制这比普通的 State 引用追踪开销更大。几个建议只有确实需要深度追踪的类才加 Observed。如果一个对象创建后就不会修改内部属性直接用 State 就够了。避免深层嵌套的 Observed。三四层嵌套的对象链每一层都 Observed性能开销会叠加。大列表场景慎用。如果你的表格有上千行每行都是 Observed 对象可能需要在虚拟滚动和按需加载上做文章。在 PC 端硬件性能通常比手机强所以 Observed 的性能开销一般不是问题。但养成良好的编码习惯总是好的。总结一下踩坑经验回头看看这次的踩坑经历核心教训就三条State 对对象类型只追踪引用不追踪内部属性变化。直接this.user.name xxx不会触发 UI 更新。朴素的解决方案是每次创建新对象整体替换但代码写起来很啰嗦。最优雅的方案是给 class 加上 Observed 装饰器让框架深度追踪属性变化。嵌套对象需要每一层都标记。搞懂了这些你在 HarmonyOS6 PC 开发中处理对象状态就不会再踩坑了。