先上一段 AI 给我生成的代码,你就能懂我为什么说它"红了一整页":
typescript
// ❌ DevEco Code 的 AI 自动补全出来的 CaseList 组件开头
@Component
export struct CaseList {
@State cases: CaseItem[] = CaseService.getHotList() // 编译报错
@State keyword: string = ''
@State filtered: CaseItem[] = this.filterCases() // 编译报错,还顺带逻辑错
filterCases(): CaseItem[] {
return this.cases.filter(c => c.title.includes(this.keyword))
}
build() {
// ... 省略
}
}
我盯着那两行红色波浪线愣了得有十秒。CaseService.getHotList() 明明是个正常的异步方法,为什么声明个变量都不让?
说白了,ArkTS 的严格模式(默认开)有个挺反直觉的规矩:成员变量的初始化表达式必须是"编译期就能确定的纯值",你不能在那儿调函数、不能写带副作用的逻辑、连 this.xxx 都不行。TS 里随手写 private list = load() 没问题,搬到 ArkTS 这儿编译器直接掀桌子,报的就是 arkts-no-side-effects-in-initialization。
为什么鸿蒙要这么轴?因为它想在编译期就锁死"对象创建过程没有副作用",这样运行时行为可预测,也方便做并发和激进优化。代价就是我们写 TS 那套"声明即计算"的偷懒习惯得改。
你猜怎么着,我当时第一反应是"这 AI 是不是抽风了",还手动给 getHotList() 加了层括号想蒙混过关------没用,报错换了个位置照样红。
说起来,雷达鸭鸿蒙版那个案例瀑布流,最早就是让 DevEco Code 的 AI 生成的,第一版就在声明处调了接口,编译直接给我红了一页。
坑一:声明处调函数,编译直接掀桌子
根因上面说了。AI 按 TypeScript 肌肉记忆写初始化,但 ArkTS 不吃这套。修复其实就一句话:声明处只给字面量或空值,真正的初始化逻辑挪到 aboutToAppear 里。
typescript
// ✅ 初始化挪进 aboutToAppear,声明处干干净净
@Component
export struct CaseList {
@State cases: CaseItem[] = [] // 只给空数组
@State keyword: string = ''
@State loading: boolean = true
aboutToAppear(): void {
CaseService.getHotList()
.then((list: CaseItem[]) => {
this.cases = list
this.loading = false
})
.catch(() => {
this.loading = false
})
}
build() {
if (this.loading) {
Loading()
return
}
List() {
ForEach(this.cases, (item: CaseItem) => {
CaseCard({ item })
}, (item: CaseItem) => item.id)
}
}
}
这个小改动能救你一晚上。我承认我一开始特别不习惯------TS 写久了,谁没事把初始化都塞生命周期里------但 ArkTS 就这么定死的,你拧不过它。
提一句,如果你非要在声明处给个"算出来的值",那只能是编译期常量或者字面量,比如 private pageSize: number = 20 这种,调方法一律不行。
还有个变体坑顺带说一下:AI 有时候不死心,把初始化塞进 constructor()。ArkTS 的 @Component 结构体压根没给你随便写构造函数去干这种活儿的口子,正经路子就是 aboutToAppear。我早期还真被它这么坑过一回,编译倒是过了,跑起来状态全空,查了半天才发现赋值写进了根本不会被调用的地方。
坑二:派生状态用 @State 存,源变了它装死
第一个坑好歹编译器会红脸提醒你。第二个坑才叫恶心:它不报错、不白屏,就是"行为不对",review 肉眼根本看不出来。
还是那个列表页,加了搜索框之后 AI 这么改的:
typescript
// ❌ 派生列表用 @State 存,源一变它不跟着变
@Component
export struct CaseList {
@State cases: CaseItem[] = []
@State keyword: string = ''
@State filtered: CaseItem[] = [] // 看着像状态,其实是一次性快照
aboutToAppear(): void {
CaseService.getHotList().then((list: CaseItem[]) => {
this.cases = list
this.filtered = list // 只在初始化时算一次
})
}
onSearch(value: string): void {
this.keyword = value
// AI 忘了同步更新 filtered,列表纹丝不动
}
build() {
List() {
ForEach(this.filtered, (item: CaseItem) => {
CaseCard({ item })
}, (item: CaseItem) => item.id)
}
}
}
你搜"咖啡",keyword 变了,cases 没变,filtered 更没变------因为它只在 aboutToAppear 里被赋值过一次,从此变成一张静态快照。用户输入半天,页面像死了一样。
根子在于 @State 只认自己那块内存的引用变化。你第一次 this.filtered = list 换了引用,它通知渲染;之后你只动 keyword 和 cases,filtered 的引用纹丝没动,框架自然觉得"这哥们没改过我,别重画"。AI 的逻辑是"初始化时算好就完事",它压根没意识到派生数据是要跟着源活的。
这种 bug 我搜了俩小时 Stack Overflow 都没找到对症的答案,末了是自己打了日志才发现 filtered 压根没被重新算。
正解有两种。最稳的、哪儿都能跑的是把派生数据写成只读 getter,每次 build 自动重算,根本不占 @State:
typescript
// ✅ 派生数据用 getter,源 @State 一变它自动重算
@Component
export struct CaseList {
@State cases: CaseItem[] = []
@State keyword: string = ''
get filtered(): CaseItem[] {
return this.cases.filter((c: CaseItem) => c.title.includes(this.keyword))
}
onSearch(value: string): void {
this.keyword = value // 只改源,filtered 在 build 时自己重算
}
build() {
List() {
ForEach(this.filtered, (item: CaseItem) => {
CaseCard({ item })
}, (item: CaseItem) => item.id)
}
}
}
要是你在 API 12 以上、想用状态管理 V2 那套更正式的机制,可以把派生状态标成 @Computed,它专门干这个活儿:
typescript
// ✅ API 12 状态管理 V2:@Computed 才是给派生状态准备的
import { Computed } from '@ohos.arkui.StateManagement'
@ComponentV2
struct CaseList {
@Local cases: CaseItem[] = []
@Local keyword: string = ''
@Computed
get filtered(): CaseItem[] {
return this.cases.filter((c: CaseItem) => c.title.includes(this.keyword))
}
build() {
ForEach(this.filtered, (item: CaseItem) => {
CaseCard({ item })
}, (item: CaseItem) => item.id)
}
}
@Computed 的好处是它只在依赖的 @Local 变了才重算,比每次 build 都跑一遍 getter 省点开销。但我个人更常用上面那个 getter 写法------不是所有项目都升到了 V2,老项目里 @ComponentV2 和 V1 装饰器混用容易出更难查的坑。
等一下,这里我漏说一个前提:@Computed 必须配合 @ComponentV2 和 @Local 用,跟 V1 的 @State 不是一套东西,别在老组件里硬塞。
收个尾
我现在让 DevEco Code 的 AI 写 ArkTS 组件,第一件事就是扫一眼变量声明处有没有偷偷调函数------只要看到 = xxx() 我就直接打回去重生成。这个习惯是拿两晚上的编译报错换来的。
说实话,我现在更信自己手写初始化,AI 补全的组件我一律先过一遍声明处。你要是也常被这类"能编译但行为诡异"的坑咬,建议把这条直接写进团队的 ArkTS 编码规范------别等上线了才发现有个列表死活搜不出来。
你写 ArkTS 的时候,AI 还给你埋过什么看起来能跑、实则埋雷的写法?评论区聊聊,我看看还有多少坑没踩完。
我是老三,10+ 年软件开发经验,软件设计师、人工智能应用工程师。现在主要做鸿蒙应用开发(ArkTS 北向)和 Web 前端,也在折腾 AI 自动化。偶尔在 CSDN 写点鸿蒙 / AI 方向的实战笔记。
本文遵循 MIT 协议,转载请注明出处。