
让 AI 把全项目 State 改成 Observed内存直接翻倍——我们用了一周才定位到原因先说结论别相信 AI 给出的等价重构承诺特别是 ArkTS 这种状态语义有隐藏规则的框架。我们组上周让 Cursor 把雷达鸭鸿蒙版的老代码全过一遍把 28 个State全部改成ObservedObjectLink看着像规范升级了结果灰度发布当天 OOM 报警——线上 PSS 峰值从 180MB 涨到 360MB差点没把工单系统打爆。复盘完发现是 AI 没搞懂 ArkTS 三件事Observed装饰的是类不是字段ObjectLink只能接受被Observed装饰的实例这俩装饰器都依赖引用追踪来通知更新一旦传值错位就泄漏监听器。下面把整个过程拆开讲重点不是骂 AI而是把AI 自动重构 ArkTS 老代码这条路到底哪里有坑说清楚。一、为什么要做这个重构这个 App 最早是 2025 年写的那会儿我刚接触 ArkTS所有列表数据都是State items: CaseItem[] []这种写法。单个页面没啥问题但当瀑布流、收藏夹、案例详情三个页面共用同一份CaseItem数据时问题就来了——任一页面修改 item 字段其它页面不会刷新。按 ArkTS 官方文档的说法要让嵌套对象的变化能通知到所有引用方就得用ObservedObjectLink。这事儿我之前一直拖因为改动量大、风险高。后来组里新来了个同学Cursor 用得飞起我寻思让他试试 AI 自动重构省事。给 Cursor 的 prompt 很朴素“把这个页面里所有State items: CaseItem[]改成Observed装饰 CaseItem 类子组件用ObjectLink接收。保持原有功能不变。”Cursor 干得很快5 分钟改完一个页面看着编译过、单测过diff 也挺干净。我们一激动直接把 12 个页面全让他改了。当晚跑回归列表渲染、数据绑定、收藏切换看起来都正常。第二天发灰度运维群里开始爆 OOM 报警。二、第一个坑Observed 必须装饰类不是字段这是 AI 最容易犯的低级错。Cursor 把CaseItem类的每个字段都加了Observed像这样// AI 重构后的错误版本ObservedclassCaseItem{Observedid:string// ❌ 字段不能加 ObservedObservedtitle:string// ❌Observedcover:string// ❌Observedlikes:number0// ❌}ArkTS 编译器居然不报错——Observed放在字段上是个合法的无副作用装饰器类型检查能过运行时也没崩。但装饰器没生效对象属性变化时不会触发通知子组件的ObjectLink完全收不到更新信号。更糟的是因为Observed没真的装饰类ObjectLink拿到的就是个普通对象引用引用追踪表里压根没注册。表面看一切正常实际每次属性变化都在偷偷泄漏旧的监听器。正确的版本是只装饰类// 正确版本ObservedclassCaseItem{id:stringtitle:stringcover:stringlikes:number0}三、第二个坑ObjectLink 不能接整个数组Cursor 在子组件里写了这种代码Componentstruct CaseListView{ObjectLinkitems:CaseItem// ❌ 只能接单个实例build(){List(){ForEach(this.items,(item:CaseItem){ListItem(){Text(item.title)}})}}}ObjectLink设计上只能装饰单个被Observed装饰的实例不能装饰数组或者基本类型。AI 这么写编译器会报ObjectLinkcannot decorate a non-observed type。但 Cursor 给出的修复是把它降级回State——这是它最鸡贼的地方遇到编译错误就退回到能编译的版本不告诉你语义已经丢了。正确写法是父组件保留数组引用子组件接单个元素ObservedclassCaseItem{id:stringtitle:string}Componentstruct CaseRow{ObjectLinkitem:CaseItem// ✅ 接单个实例build(){Row(){Text(this.item.title)}}}Componentstruct CaseListView{Stateitems:CaseItem[][]// 父组件继续用 State 管数组build(){List(){ForEach(this.items,(item:CaseItem){ListItem(){CaseRow({item:item})// 传单个实例}})}}}ObjectLink拿到的是引用子组件修改item.title会反向写回父组件的数组对象——这才是 ArkTS 设计ObservedObjectLink的本意。四、第三个坑传值错位导致监听器泄漏这是导致 OOM 的元凶。Cursor 在跨页面传值时犯了 ArkTS 状态语义的典型错误。老代码里瀑布流页和详情页用的是同一份CaseItem引用// 老代码this.router.pushUrl({url:pages/CaseDetail,params:{id:item.id}})详情页接收的时候Cursor 写成这样ObservedclassCaseDetailState{item:CaseItemnewCaseItem()// ❌ 创建了新实例}EntryComponentstruct CaseDetail{Statestate:CaseDetailStatenewCaseDetailState()aboutToAppear(){constparamsthis.router.getParams()asRecordstring,stringconstidparams[id]// ❌ 从全局 store 重新查一份新对象this.state.itemCaseStore.getById(id)}}问题来了CaseStore.getById()内部如果用了深拷贝我们的老代码就是这么写的怕外部修改污染 store那详情页拿到的item和列表页的item根本不是同一个对象。父组件修改时详情页的ObjectLink监听器指向的引用和实际数据没关系——但监听器已经注册了不会自动释放。每进一次详情页出来就泄漏一对监听器。瀑布流页和详情页的来回跳转是用户的高频操作5 分钟就能把监听器表撑爆。修复方法要么 store 不做深拷贝要么传参时带引用。考虑到老代码改不动我们用了前者——给CaseStore加了浅拷贝选项classCaseStore{staticgetById(id:string,deep:booleanfalse):CaseItem{constitemthis.db.find(xx.idid)if(!item)thrownewError(case not found:${id})returndeep?structuredCopy(item):item// 默认浅拷贝}}详情页aboutToAppear改成CaseStore.getById(id, false)和列表页共用同一份引用。监听器指向的对象地址一致问题消失。五、复盘结论这次踩坑的根本原因不是 Cursor 不行是我们给 AI 的 prompt没覆盖 ArkTS 的状态语义约束。在 React/Vue 项目里AI 重构 Hooks/响应式变量一般不会出大问题因为底层都是引用追踪。ArkTS 这套Observed/ObjectLink有三个隐藏规则装饰类、接实例、引用一致AI 不知道就会按看起来能编译的方式去填。如果让我重来一次我会先把 prompt 改成这样重构这个页面的状态管理。要求Observed只装饰类不要加在字段上ObjectLink只接单个Observed实例不能接数组跨页面传值时保持引用一致禁止深拷贝改完后必须用 grep 验证装饰器只在类声明行不出现在字段声明行。光给 AI 说等价重构是没用的。ArkTS 的状态语义不像 JavaScript 那么宽容编译过 ≠ 跑得对。给 AI 的指令必须把禁止项写明而不是把希望写明。至于 AI 能不能完全替代人工重构 ArkTS 老代码我的态度是单个组件可以整个项目不行。单个组件的错误你能用眼睛扫出来整个项目的话监听器泄漏这种问题连编译器都不报你得跑 Profiler 才知道。等 Profiler 报警的时候线上的内存已经涨了 200% 了。反正这次之后我把 Cursor 的自动重构权限从整个项目降回了单文件 我审模式。多花点时间看 diff比半夜起来接运维电话划算得多。10 年搬砖经验的老程序员最近几年专注鸿蒙 ArkTS 北向开发也搞搞 Web 前端。日常在折腾 AI 自动化和工具链偶尔在 CSDN 分享一些鸿蒙和 AI 方向的踩坑实录不定期更新。本文遵循 MIT 协议转载请注明出处。