鸿蒙开发避坑日记:30 个让我加班的真实 bug
记录 HarmonyOS 真实项目中踩过的 30 个典型 bug,涵盖状态管理(@ObservedV2/@Trace)、UI 渲染(ForEach/List)、生命周期(定时器/异步)、网络请求(超时/Worker)、数据持久化(Preferences/RDB)、路由导航六大模块,每个 bug 附错误写法、正确写法与原因分析,帮你少加班少掉发。
鸿蒙开发避坑日记:30 个让我加班的真实 bug
鸿蒙开发避坑日记:30 个让我加班的真实 bug
前言
作为一名深耕 HarmonyOS 开发的工程师,这一年来踩过的坑比走过的路还多。每一个 bug 背后都是至少一个深夜,有的甚至让我排查了整整三天。
本文记录了我在真实项目中遭遇的 30 个典型 bug,涵盖状态管理、UI 渲染、生命周期、网络请求、数据持久化、路由导航六大模块。每个 bug 均附有错误代码、正确写法和原因分析。
希望这篇避坑日记能让你少掉几根头发,少加几次班。
一、状态管理篇
Bug 1:@State 嵌套对象修改不触发 UI 刷新
现象:修改 @State 对象的嵌套属性后,界面毫无反应。
错误写法:
@Entry @ComponentV2
struct WrongPage {
@Local user: { name: string; age: number } = { name: 'Tom', age: 18 }
build() {
Column() {
Text(this.user.name)
Button('改名').onClick(() => {
this.user.name = 'Jerry' // UI 不刷新!
})
}
}
}
正确写法:
@ObservedV2
class User {
@Trace name: string = 'Tom'
@Trace age: number = 18
}
@Entry @ComponentV2
struct CorrectPage {
@Local user: User = new User()
build() {
Column() {
Text(this.user.name)
Button('改名').onClick(() => {
this.user.name = 'Jerry' // 正常刷新
})
}
}
}
原因:@State 只能追踪引用变化,嵌套属性修改不触发响应。必须用 @ObservedV2 + @Trace 装饰需要追踪的属性。
Bug 2:@ObservedV2 忘记 @Trace,数据更新静悄悄
现象:明明用了 @ObservedV2,UI 还是不更新。
错误写法:
@ObservedV2
class Config {
theme: string = 'light' // 忘记加 @Trace!
}
正确写法:
@ObservedV2
class Config {
@Trace theme: string = 'light' // 必须标注
}
原因:@ObservedV2 只是标记类可被观察,真正触发响应的是 @Trace。两者缺一不可,少一个都会让响应式系统失效。
Bug 3:@Param 传对象引用导致父子状态污染
现象:子组件修改数据影响了父组件,父组件莫名其妙刷新。
错误写法:
@ComponentV2
struct ChildComp {
@Param item: { title: string } = { title: '' }
build() {
Button(this.item.title).onClick(() => {
this.item.title = '子组件修改' // 直接改父传来的对象!
})
}
}
正确写法:
@ComponentV2
struct ChildComp {
@Param item: { title: string } = { title: '' }
@Event onTitleChange: (title: string) => void = () => {}
build() {
Button(this.item.title).onClick(() => {
this.onTitleChange('子组件修改') // 通过事件通知父组件
})
}
}
原因:@Param 是单向数据流,子组件不应直接修改父传来的数据。应使用 @Event 回调通知父组件变更。
Bug 4:@Monitor 死循环导致应用卡死
现象:页面打开后立即卡死,CPU 飙升 100%。
错误写法:
@Entry @ComponentV2
struct BadPage {
@Local count: number = 0
@Monitor('count')
onCountChange() {
this.count++ // 修改 count 又触发监听,死循环!
}
build() {
Text(`${this.count}`)
}
}
正确写法:
@Entry @ComponentV2
struct GoodPage {
@Local count: number = 0
@Local displayCount: number = 0
@Monitor('count')
onCountChange() {
this.displayCount = this.count * 2 // 修改不同的变量
}
build() {
Text(`${this.displayCount}`)
}
}
原因:@Monitor 回调中修改被监听的变量会形成无限循环。监听变量 A,就不能在回调里修改 A。
Bug 5:AppStorage 存储复杂对象方法丢失
现象:从 AppStorage 取出的对象丢失了所有方法,调用时报 is not a function。
错误写法:
class UserModel {
name: string = ''
getName(): string { return this.name }
}
const user = new UserModel()
user.name = 'Tom'
AppStorage.setOrCreate('user', user)
const stored = AppStorage.get<UserModel>('user')
stored?.getName() // 报错:getName is not a function
正确写法:
interface UserData {
name: string
}
AppStorage.setOrCreate<UserData>('user', { name: 'Tom' })
const stored = AppStorage.get<UserData>('user')
const model = new UserModel()
model.name = stored?.name ?? ''
model.getName() // 正常调用
原因:AppStorage 内部做了序列化,class 实例的原型链丢失,方法全没了。存储时只保存纯数据,取出后重建实例。
Bug 6:LocalStorage 跨页面数据不同步
现象:页面 A 修改了数据,页面 B 读取到的还是旧值。
错误写法:
// 页面 A 和页面 B 各自 new 了一个实例,彼此独立!
const storageA = new LocalStorage({ count: 0 })
const storageB = new LocalStorage({ count: 0 })
正确写法:
// EntryAbility.ts
const storage = new LocalStorage({ count: 0 })
windowStage.loadContent('pages/Index', storage)
// 各页面通过装饰器共享同一实例
@Entry(storage)
@ComponentV2
struct PageA {
@StorageLink('count') count: number = 0
}
原因:LocalStorage 不是全局单例,需通过 windowStage.loadContent 传入,各页面才共享同一份数据。
二、UI 渲染篇
Bug 7:ForEach 缺少唯一 key 导致渲染错乱
现象:列表增删后,UI 展示顺序混乱,数据对不上号。
错误写法:
ForEach(this.list, (item: string) => {
Text(item)
}) // 缺少 keyGenerator
正确写法:
ForEach(this.list, (item: string) => {
Text(item)
}, (item: string) => item) // 提供唯一 key
原因:没有 key 时框架用 index 做标识,增删操作会导致 key 错位,组件被错误复用,渲染结果混乱。
Bug 8:直接 push 数组后 List 不刷新
现象:push 数据到数组后,List 组件没有任何变化。
错误写法:
@Local items: string[] = []
addItem() {
this.items.push('new item') // 引用未变,不触发刷新
}
正确写法:
@Local items: string[] = []
addItem() {
this.items = [...this.items, 'new item'] // 产生新引用
}
原因:push 修改数组内容但引用地址不变,框架检测不到变化。需替换为新数组才能触发重渲染。
Bug 9:Image 加载网络图片偶发白屏
现象:图片时而显示,时而空白,没有规律,尤其弱网下频繁出现。
错误写法:
Image(this.imageUrl)
.width(100)
.height(100)
// 没有错误处理
正确写法:
Image(this.imageUrl)
.width(100)
.height(100)
.onError(() => {
this.imageUrl = $r('app.media.placeholder')
})
.onComplete((event) => {
console.info(`图片加载成功: ${event?.width}x${event?.height}`)
})
原因:网络图片加载是异步的,网络异常时没有降级处理会直接白屏。必须监听 onError 并切换占位图。
Bug 10:TextInput 软键盘弹出导致布局挤压
现象:点击输入框后,软键盘弹出,页面内容被挤到上方看不见。
错误写法:
@Entry @ComponentV2
struct LoginPage {
build() {
Column() {
Image($r('app.media.logo')).height(200)
TextInput({ placeholder: '请输入账号' })
TextInput({ placeholder: '请输入密码' })
Button('登录')
}
.height('100%') // 键盘弹起时内容被压缩
}
}
正确写法:
@Entry @ComponentV2
struct LoginPage {
build() {
Scroll() {
Column() {
Image($r('app.media.logo')).height(200)
TextInput({ placeholder: '请输入账号' })
TextInput({ placeholder: '请输入密码' })
Button('登录')
}
.padding(16)
}
.expandSafeArea([SafeAreaType.KEYBOARD])
}
}
原因:需用 Scroll 包裹内容区并设置 expandSafeArea([SafeAreaType.KEYBOARD]),让键盘弹起时内容可滚动而非被压缩。
Bug 11:Scroll 嵌套 List 导致双向滑动冲突
现象:外层 Scroll 和内层 List 同时抢占滑动事件,两个都滑不动。
错误写法:
Scroll() {
Column() {
Text('头部内容')
List() {
// 列表项
}
.height('100%') // 内外滑动冲突
}
}
正确写法:
// 把头部内容也放进 List,统一管理滚动
List() {
ListItem() {
Text('头部内容')
}
ForEach(this.items, (item: string) => {
ListItem() {
Text(item)
}
}, (item: string) => item)
}
.scrollBar(BarState.Off)
原因:将头部内容作为 ListItem 放入 List 中,统一由一个滚动容器管理,彻底避免嵌套滑动冲突。
Bug 12:Stack 子组件遮挡顺序错乱
现象:浮层被底部内容遮挡,设置了 zIndex 也没用。
错误写法:
Stack() {
FloatButton() // 希望在最上层,却被内容区遮挡
.zIndex(999)
ContentArea()
}
正确写法:
Stack({ alignContent: Alignment.BottomEnd }) {
ContentArea() // 先声明,在下层
FloatButton() // 后声明,默认在上层
.zIndex(999)
}
原因:Stack 中后声明的子组件默认在上层。zIndex 需在同一父容器内比较才有效,调整声明顺序是最简洁的解法。
三、生命周期篇
Bug 13:定时器未清除导致内存泄漏
现象:页面关闭后,控制台还在持续打印,内存不断增长。
错误写法:
@Entry @ComponentV2
struct TimerPage {
aboutToAppear() {
setInterval(() => {
console.info('tick')
}, 1000) // 返回值未保存,永远无法清除!
}
build() {
Text('计时中')
}
}
正确写法:
@Entry @ComponentV2
struct TimerPage {
private timerId: number = -1
aboutToAppear() {
this.timerId = setInterval(() => {
console.info('tick')
}, 1000)
}
aboutToDisappear() {
if (this.timerId !== -1) {
clearInterval(this.timerId)
this.timerId = -1
}
}
build() {
Text('计时中')
}
}
原因:定时器持有组件引用,不清除会导致组件无法被 GC,造成内存泄漏。aboutToDisappear 是释放资源的最后防线。
Bug 14:异步任务完成时组件已销毁仍更新状态崩溃
现象:网络请求返回后应用闪退,日志显示组件已销毁。
错误写法:
@Entry @ComponentV2
struct DataPage {
@Local data: string = ''
aboutToAppear() {
fetchData().then((res) => {
this.data = res // 可能组件已经销毁了!
})
}
build() {
Text(this.data)
}
}
正确写法:
@Entry @ComponentV2
struct DataPage {
@Local data: string = ''
private isAlive: boolean = true
aboutToAppear() {
fetchData().then((res) => {
if (this.isAlive) {
this.data = res // 守卫:确认组件存活
}
})
}
aboutToDisappear() {
this.isAlive = false
}
build() {
Text(this.data)
}
}
原因:异步操作游离于组件生命周期之外,需用标志位守卫,确保组件存活时才更新状态。
Bug 15:onPageShow 在 Navigation 模式下不触发
现象:从子页面返回后,期望的刷新逻辑没有执行。
原因分析:onPageShow / onPageHide 属于 Router 的生命周期钩子。使用 Navigation 组件时,页面栈由 Navigation 自管,这两个钩子不会触发。
正确写法:
@ComponentV2
struct DetailPage {
build() {
NavDestination() {
Text('详情页')
}
.onShown(() => {
// 替代 onPageShow,页面显示时触发
this.loadData()
})
.onHidden(() => {
// 替代 onPageHide,页面隐藏时触发
this.pauseWork()
})
}
loadData() {}
pauseWork() {}
}
原因:Navigation 模式下用 NavDestination 的 onShown / onHidden 替代 onPageShow / onPageHide。
Bug 16:build() 中调用异步函数导致渲染异常
现象:页面渲染不完整,内容闪烁,控制台出现大量警告。
错误写法:
@Entry @ComponentV2
struct BadPage {
build() {
Column() {
// 严禁在 build 中做 I/O 或异步操作!
Text(this.loadTitleSync())
}
}
loadTitleSync(): string {
// 同步阻塞调用 = 卡主线程
return heavyBlockingOp()
}
}
正确写法:
@Entry @ComponentV2
struct GoodPage {
@Local title: string = '加载中...'
aboutToAppear() {
this.loadTitle()
}
async loadTitle() {
try {
this.title = await fetchTitle()
} catch (err) {
this.title = '加载失败'
}
}
build() {
Column() {
Text(this.title)
}
}
}
原因:build() 是同步渲染函数,数据获取应在 aboutToAppear 中完成,通过状态变量驱动 UI 更新。
Bug 17:pushUrl 后旧页面 aboutToDisappear 未立即执行
现象:切换页面后,旧页面资源没有释放,内存持续增长。
原因分析:Router.replaceUrl 销毁旧页面并触发 aboutToDisappear;但 Router.pushUrl 只是将旧页面压入栈,它仍存活,aboutToDisappear 不会立即执行。
正确写法:
// 需要销毁旧页面时用 replaceUrl
router.replaceUrl({ url: 'pages/NewPage' })
// 需保留返回能力时用 pushUrl,在 onPageHide 暂停重资源
onPageHide() {
this.videoController.pause()
this.cameraInput?.close()
}
onPageShow() {
this.videoController.start()
}
原因:明确 pushUrl 和 replaceUrl 的区别,针对性地在 onPageHide 中暂停重资源消耗操作。
四、网络请求篇
Bug 18:http 请求未设置超时永久等待
现象:弱网环境下,请求界面转圈圈永远不停,无法取消。
错误写法:
const httpRequest = http.createHttp()
httpRequest.request(url, {
method: http.RequestMethod.GET
// 没有任何超时设置,可能永远等待
})
正确写法:
async function fetchWithTimeout(url: string): Promise<string> {
const httpRequest = http.createHttp()
try {
const response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
connectTimeout: 10000, // 连接超时 10s
readTimeout: 30000 // 读取超时 30s
})
return response.result as string
} catch (err) {
throw new Error(`请求失败: ${err.message}`)
} finally {
httpRequest.destroy() // 必须释放资源
}
}
原因:必须设置连接超时和读取超时,并在 finally 中调用 destroy() 释放连接资源,避免资源泄漏。
Bug 19:HTTPS 自签名证书导致请求失败
现象:内网测试服务无法访问,报 SSL 证书验证错误。
正确写法:
const response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
caPath: context.filesDir + '/certs/internal-ca.pem',
connectTimeout: 10000,
readTimeout: 30000
})
原因:对于自签名证书,通过 caPath 指定证书路径,而非跳过验证(跳过验证有安全风险,禁止在生产环境使用)。
Bug 20:并发请求无数量限制导致 OOM
现象:列表页快速滚动时,同时发起大量图片请求,应用内存溢出崩溃。
错误写法:
// 100 个 item 同时发起请求,直接 OOM
items.forEach(async (item) => {
item.imageUrl = await loadImage(item.url)
})
正确写法:
async function loadWithConcurrency(
urls: string[],
limit: number
): Promise<string[]> {
const results: string[] = []
for (let i = 0; i < urls.length; i += limit) {
const batch = urls.slice(i, i + limit)
const batchResults = await Promise.all(
batch.map(url => loadImage(url))
)
results.push(...batchResults)
}
return results
}
// 每批最多 5 个并发
const images = await loadWithConcurrency(urls, 5)
原因:无限并发会耗尽内存和网络资源。分批控制,每批 5-10 个并发是合理上限。
Bug 21:Worker 线程直接更新 UI 崩溃
现象:Worker 完成数据处理后尝试更新 UI,应用立即崩溃。
错误写法:
// worker.ets
workerPort.onmessage = (e: MessageEvents) => {
const result = heavyCompute(e.data.value)
// 错误:Worker 线程中不能直接访问 UI 状态!
globalThis.pageComponent.data = result
}
正确写法:
// worker.ets:计算完成后 postMessage 回主线程
workerPort.onmessage = (e: MessageEvents) => {
const result = heavyCompute(e.data.value)
workerPort.postMessage({ result })
}
// 主线程:接收结果后更新 UI
const myWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets')
myWorker.onmessage = (e: MessageEvents) => {
this.result = e.data.result // 主线程中安全更新 UI
}
myWorker.postMessage({ value: inputData })
原因:UI 操作只能在主线程进行。Worker 完成计算后必须通过 postMessage 将结果发回主线程,再由主线程更新 UI。
Bug 22:Request Header 大小写导致服务端解析失败
现象:明明设置了 Authorization Header,服务端一直说未授权。
错误写法:
httpRequest.request(url, {
header: {
'authorization': 'Bearer token123', // 全小写,部分服务端不识别
'content-type': 'application/json'
}
})
正确写法:
httpRequest.request(url, {
header: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
原因:HTTP Header 标准上大小写不敏感,但部分服务端实现对大小写敏感。统一使用标准大驼峰格式最安全。
五、数据持久化篇
Bug 23:Preferences 同步 API 阻塞主线程触发 ANR
现象:应用启动时卡顿 1-2 秒,频繁出现 ANR 报告。
错误写法:
// 同步操作,直接阻塞主线程
const prefs = dataPreferences.getPreferencesSync(context, { name: 'config' })
const theme = prefs.getSync('theme', 'light') as string
正确写法:
async function loadConfig(context: Context): Promise<string> {
try {
const prefs = await dataPreferences.getPreferences(
context,
{ name: 'config' }
)
return await prefs.get('theme', 'light') as string
} catch (err) {
return 'light'
}
}
原因:所有 I/O 操作必须异步执行。同步 API 会阻塞主线程导致 UI 卡顿,严重时触发 ANR。
Bug 24:RelationalStore ResultSet 未关闭导致连接池耗尽
现象:长时间使用后应用崩溃,日志显示数据库连接数超限。
错误写法:
async function queryUsers(): Promise<User[]> {
const store = await relationalStore.getRdbStore(context, config)
const cursor = await store.query(predicates, columns)
return parseUsers(cursor) // 忘记关闭 cursor,连接泄漏!
}
正确写法:
async function queryUsers(): Promise<User[]> {
const store = await relationalStore.getRdbStore(context, config)
let cursor: relationalStore.ResultSet | null = null
try {
cursor = await store.query(predicates, columns)
return parseUsers(cursor)
} finally {
cursor?.close() // 无论成功失败,必须关闭
}
}
原因:ResultSet 是需要手动关闭的资源,不关闭会导致内存泄漏和连接池耗尽。finally 是保证关闭的最可靠方式。
Bug 25:JSON 序列化 class 实例丢失原型方法
现象:序列化后再反序列化,对象的所有方法调用报 is not a function。
错误写法:
class Task {
id: number = 0
title: string = ''
isValid(): boolean { return this.title.length > 0 }
}
const task = new Task()
task.title = 'Hello'
const json = JSON.stringify(task)
const restored = JSON.parse(json) as Task
restored.isValid() // 崩溃:isValid is not a function
正确写法:
interface TaskData { id: number; title: string }
const json = JSON.stringify({ id: task.id, title: task.title })
const data = JSON.parse(json) as TaskData
// 重新构建实例,恢复方法
const restored = new Task()
restored.id = data.id
restored.title = data.title
restored.isValid() // 正常
原因:JSON.parse 返回普通对象,不携带原型链。序列化只保存纯数据,反序列化后手动重建实例。
Bug 26:高频写入 Preferences 导致 ANR
现象:用户拖动滑块调节音量时,应用出现无响应弹窗。
错误写法:
// 滑块每次变化都立即写入,每秒可能触发数十次 I/O
onSliderChange(value: number) {
this.preferences.putSync('volume', value)
this.preferences.flushSync()
}
正确写法:
private saveTimer: number = -1
onSliderChange(value: number) {
this.currentVolume = value // 立即更新 UI
if (this.saveTimer !== -1) {
clearTimeout(this.saveTimer)
}
// 防抖:停止操作 500ms 后才写入
this.saveTimer = setTimeout(async () => {
await this.preferences.put('volume', value)
await this.preferences.flush()
}, 500)
}
原因:高频 I/O 是导致 ANR 的常见原因。防抖策略合并多次操作为一次写入,大幅降低 I/O 频率。
六、路由导航篇
Bug 27:Router.pushUrl 传参 class 实例方法丢失
现象:目标页面接收到的 params 中,对象方法全部不见了。
错误写法:
class Product {
id: number = 0
name: string = ''
getDisplayName(): string { return `[${this.id}] ${this.name}` }
}
const product = new Product()
product.name = '手机'
router.pushUrl({
url: 'pages/Detail',
params: product // class 实例被序列化,方法丢失
})
正确写法:
interface ProductParams { id: number; name: string }
router.pushUrl({
url: 'pages/Detail',
params: { id: product.id, name: product.name } as ProductParams
})
// 目标页面重新构建实例
const params = router.getParams() as ProductParams
const restored = new Product()
restored.id = params.id
restored.name = params.name
restored.getDisplayName() // 正常调用
原因:路由传参经过序列化,class 方法丢失。只传纯数据,目标页面自行重建对象实例。
Bug 28:Navigation 重复 push 同一页面导致返回栈臃肿
现象:连续点击按钮多次,返回时需要按很多次才能回到首页。
错误写法:
Button('进入详情').onClick(() => {
// 每次点击都 push,多点几下返回栈就有 N 个详情页
this.pathStack.pushPathByName('DetailPage', params)
})
正确写法:
Button('进入详情').onClick(() => {
const existing = this.pathStack.getIndexByName('DetailPage')
if (existing.length > 0) {
this.pathStack.moveToTop('DetailPage') // 已存在则移到顶部
} else {
this.pathStack.pushPathByName('DetailPage', params)
}
})
原因:重复 push 同一页面会在返回栈中堆叠多个实例。应先检查是否已存在,存在则移到顶部复用。
Bug 29:router.back() 无法向上一页传递数据
现象:从详情页返回列表页,列表页用 router.getParams() 始终拿到 undefined。
错误写法:
// 详情页:直接 back,无法携带参数
router.back()
// 列表页:永远拿不到返回数据
onPageShow() {
const params = router.getParams() // 始终是 undefined
}
正确写法:
// 方案一:用 AppStorage 中转返回数据
// 详情页
AppStorage.setOrCreate('needRefresh', true)
AppStorage.setOrCreate('updatedId', this.productId)
router.back()
// 列表页
onPageShow() {
const needRefresh = AppStorage.get<boolean>('needRefresh')
if (needRefresh) {
AppStorage.setOrCreate('needRefresh', false)
this.refreshList()
}
}
// 方案二:replaceUrl 传参(不保留详情页到栈中)
router.replaceUrl({
url: 'pages/List',
params: { refreshId: this.updatedId }
})
原因:router.back() 不支持传参。可用 AppStorage、EventHub 或 replaceUrl 实现返回数据传递。
Bug 30:动态 import 懒加载时机错误导致白屏
现象:进入某页面时一片白屏,等待数秒后内容才出现,体验极差。
错误写法:
@Entry @ComponentV2
struct LazyPage {
@Local module: ESObject = null
aboutToAppear() {
import('./HeavyModule').then(mod => {
this.module = mod
})
}
build() {
if (this.module) {
DynamicContent({ mod: this.module })
}
// module 为 null 时什么都不渲染 = 白屏
}
}
正确写法:
@Entry @ComponentV2
struct LazyPage {
@Local module: ESObject = null
@Local loading: boolean = true
aboutToAppear() {
import('./HeavyModule').then(mod => {
this.module = mod
this.loading = false
})
}
build() {
Stack() {
if (this.module) {
DynamicContent({ mod: this.module })
}
if (this.loading) {
Column() {
LoadingProgress().width(60).height(60)
Text('加载中...').fontSize(14).fontColor('#999')
}
}
}
}
}
原因:懒加载期间必须展示加载状态(骨架屏或 Loading 动画),而非让用户面对白屏。这是基本的用户体验要求。
常见问题 Q&A
Q1:ArkTS 中能用 any 类型吗?
不能。HarmonyOS ArkTS 严格禁止 any 类型,编译时会报错。使用 unknown 配合类型守卫,或明确声明具体类型。
Q2:@ComponentV2 中能混用 @State 吗?
不能。V2 组件体系(@ComponentV2)和 V1(@Component)不能混用装饰器。V2 用 @Local、@Param、@Event;V1 用 @State、@Prop、@Link。
Q3:ForEach 和 LazyForEach 如何选择?
数据量少于 100 条用 ForEach;超过 100 条或数据动态增长用 LazyForEach。LazyForEach 只渲染可视区域内的组件,显著节省内存。
Q4:为什么 @Trace 必须和 @ObservedV2 一起用?
@ObservedV2 标记类可观察,@Trace 标记具体属性可追踪。缺少任意一个,响应式都不生效。两者是配套关系,必须同时使用。
Q5:多页面共享状态怎么选方案?
| 场景 | 推荐方案 |
|---|---|
| 全局状态(主题、用户信息) | AppStorage |
| 同一 Ability 内多页面 | LocalStorage |
| 跨组件事件通知 | EventHub |
| 父子组件数据传递 | @Param + @Event |
总结
| 模块 | 高频坑点 | 核心原则 |
|---|---|---|
| 状态管理 | 忘加 @Trace、直接改嵌套属性 | @ObservedV2 + @Trace 缺一不可 |
| UI 渲染 | ForEach 无 key、数组直接 push | 产生新引用,提供唯一 key |
| 生命周期 | 定时器未清除、异步后组件已销毁 | 守卫标志位,aboutToDisappear 清理 |
| 网络请求 | 无超时、子线程更新 UI | 设置超时,结果回主线程 |
| 数据持久化 | 同步 I/O、不关闭 ResultSet | 全程异步,finally 关闭资源 |
| 路由导航 | 传参丢方法、back 无法传参 | 只传纯数据,共享层传返回值 |
踩坑是成长最快的方式,但不必每个坑都亲自踩一遍。希望这 30 个真实 bug 能帮你提前绕过这些深夜时刻,少加几次班。
如果你也有自己的踩坑经历,欢迎在评论区留言分享!觉得有帮助的话,请点赞收藏,你的支持是我继续写下去的动力。
参考资料
更多推荐



所有评论(0)