鸿蒙开发避坑日记: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 模式下用 NavDestinationonShown / 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()
}

原因:明确 pushUrlreplaceUrl 的区别,针对性地在 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 条或数据动态增长用 LazyForEachLazyForEach 只渲染可视区域内的组件,显著节省内存。

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 能帮你提前绕过这些深夜时刻,少加几次班。

如果你也有自己的踩坑经历,欢迎在评论区留言分享!觉得有帮助的话,请点赞收藏,你的支持是我继续写下去的动力。


参考资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐