前言

@Local 是 HarmonyOS 状态管理 V2 中替代 @State 的私有状态装饰器。与 @State 相比,@Local 有两项关键改进:严格私有性(父组件无任何途径读写它)和更清晰的更新语义(明确要求不可变更新模式,让数据流方向一目了然)。本文用一个完整的 TodoList 演示 @Local 操作数组、对象和基本类型的全套模式,以及 ArkTS 严格模式下的常见编译陷阱。

核心 API

@Local 声明语法

@Entry
@ComponentV2
struct MyPage {
  @Local count: number = 0             // 基本类型
  @Local title: string = '待办清单'    // 字符串
  @Local todos: Todo[] = []            // 数组
  @Local filter: string = 'all'        // 枚举字符串
}

触发 UI 更新的规则

数据类型 触发更新的写法 不触发更新的写法
基本类型 this.count++
字符串 this.title = '新标题'
数组 this.todos = [...this.todos, item] this.todos.push(item)
对象 this.profile = { ...fields } this.profile.name = '新'

核心规则@Local 检测的是引用变化,不是内容变化。数组和对象必须生成新引用才能触发重渲染;基本类型赋值本身就是新值,天然触发。

@Local vs @State 关键差异

// V1:@State 可被父组件通过 @ObjectLink 访问
@State items: Todo[] = []   // 父组件可通过 @ObjectLink 读写

// V2:@Local 严格私有,外部无法读写
@Local items: Todo[] = []   // 只有本组件内部能修改

实现思路

用一个 TodoList 覆盖 @Local 的三种数据类型:@Local todos: Todo[] 展示数组的不可变增删改;@Local filter: string 展示字符串赋值;@Local nextId: number 展示基本类型自增。getStats() 返回派生统计数据(后续 article68 中 @Computed 会自动缓存此类计算)。ArkTS 不允许对象展开 { ...obj },所有对象更新须逐字段构造,在第 3、4 步中重点展示。

逐步实现

第 1 步:定义接口和 @Local 状态

interface Todo {
  id: number
  text: string
  done: boolean
}

@Entry
@ComponentV2
struct Index {
  @Local todos: Todo[] = []       // 任务列表
  @Local nextId: number = 1       // 自增 ID(基本类型)
  @Local filter: string = 'all'   // 过滤器(字符串)
  @Local inputText: string = ''   // 输入框绑定

三种类型一次覆盖:数组、基本类型、字符串,触发方式各不相同。

第 2 步:添加任务——数组不可变更新

private addTodo(): void {
  const text = this.inputText.trim()
  if (!text) return
  // [...旧数组, 新元素] 产生新数组引用,触发 @Local 更新
  this.todos = [...this.todos, { id: this.nextId, text, done: false }]
  this.nextId++       // 基本类型直接赋值
  this.inputText = '' // 清空输入框
}

[...this.todos, newItem] 是数组的标准不可变添加模式,等价于 V1 的 this.todos = this.todos.concat([newItem])

第 3 步:切换完成状态——map 返回新数组

private toggleDone(id: number): void {
  // ArkTS 禁止对象展开 { ...t },须逐字段构造新对象
  this.todos = this.todos.map(t =>
    t.id === id
      ? { id: t.id, text: t.text, done: !t.done }  // 新对象
      : t
  )
}

map() 天然返回新数组,配合逐字段构造对象,一行完成不可变更新。注意 ArkTS 严格模式禁止 { ...t, done: !t.done }arkts-no-spread 错误),必须显式列出每个字段。

第 4 步:删除任务——filter 返回新数组

private removeTodo(id: number): void {
  this.todos = this.todos.filter(t => t.id !== id)
}

filter() 同样返回新数组,是最简洁的不可变删除写法。

第 5 步:过滤显示与统计

private filteredTodos(): Todo[] {
  if (this.filter === 'active') return this.todos.filter(t => !t.done)
  if (this.filter === 'done') return this.todos.filter(t => t.done)
  return this.todos
}

private getStats(): Stats {
  const done = this.todos.filter(t => t.done).length
  return { total: this.todos.length, done, active: this.todos.length - done }
}

切换 this.filter 字符串值即触发重渲染,filteredTodos() 返回对应子集。

完整代码

// Article 62: @Local 私有状态深度解析

interface Todo {
  id: number
  text: string
  done: boolean
}

interface Stats {
  total: number
  done: number
  active: number
}

@Entry
@ComponentV2
struct Index {
  @Local todos: Todo[] = []
  @Local nextId: number = 1
  @Local filter: string = 'all'
  @Local inputText: string = ''

  private addTodo(): void {
    const text = this.inputText.trim()
    if (!text) return
    this.todos = [...this.todos, { id: this.nextId, text, done: false }]
    this.nextId++
    this.inputText = ''
  }

  private toggleDone(id: number): void {
    this.todos = this.todos.map(t =>
      t.id === id ? { id: t.id, text: t.text, done: !t.done } : t
    )
  }

  private removeTodo(id: number): void {
    this.todos = this.todos.filter(t => t.id !== id)
  }

  private getStats(): Stats {
    const done = this.todos.filter(t => t.done).length
    return { total: this.todos.length, done, active: this.todos.length - done }
  }

  private filteredTodos(): Todo[] {
    if (this.filter === 'active') return this.todos.filter(t => !t.done)
    if (this.filter === 'done') return this.todos.filter(t => t.done)
    return this.todos
  }

  private filterLabel(f: string): string {
    if (f === 'active') return '待完成'
    if (f === 'done') return '已完成'
    return '全部'
  }

  build() {
    Column({ space: 0 }) {
      Row() {
        Text('@Local 私有状态').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')
      }
      .width('100%').height(56).backgroundColor('#ffffff').padding({ left: 16 })
      .border({ width: { bottom: 1 }, color: '#f0f0f0' })

      Scroll() {
        Column({ space: 12 }) {

          Row({ space: 0 }) {
            Column({ space: 4 }) {
              Text(`${this.getStats().total}`).fontSize(28).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')
              Text('全部').fontSize(11).fontColor('#aaa')
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column({ space: 4 }) {
              Text(`${this.getStats().active}`).fontSize(28).fontWeight(FontWeight.Bold).fontColor('#0066ff')
              Text('待完成').fontSize(11).fontColor('#aaa')
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column({ space: 4 }) {
              Text(`${this.getStats().done}`).fontSize(28).fontWeight(FontWeight.Bold).fontColor('#07c160')
              Text('已完成').fontSize(11).fontColor('#aaa')
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
          }
          .width('100%').backgroundColor('#ffffff').borderRadius(12).padding(16)
          .margin({ left: 12, right: 12 })

          Column({ space: 10 }) {
            Text('添加任务').fontSize(13).fontColor('#888').fontWeight(FontWeight.Medium).width('100%')
            Row({ space: 10 }) {
              TextInput({ placeholder: '输入任务内容…', text: this.inputText })
                .layoutWeight(1).height(44).borderRadius(22).fontSize(14)
                .backgroundColor('#f5f5f5').padding({ left: 16, right: 16 })
                .onChange((v: string) => { this.inputText = v })
              Button('添加')
                .height(44).borderRadius(22).fontSize(14).padding({ left: 20, right: 20 })
                .backgroundColor(this.inputText.trim() ? '#0066ff' : '#e0e0e0').fontColor('#fff')
                .onClick(() => this.addTodo())
            }
          }
          .width('100%').backgroundColor('#ffffff').borderRadius(12).padding(16)
          .margin({ left: 12, right: 12 })

          Row({ space: 8 }) {
            ForEach(['all', 'active', 'done'] as string[], (f: string) => {
              Text(this.filterLabel(f))
                .fontSize(13).padding({ left: 16, right: 16, top: 6, bottom: 6 }).borderRadius(16)
                .fontColor(this.filter === f ? '#fff' : '#666')
                .backgroundColor(this.filter === f ? '#0066ff' : '#f0f0f0')
                .onClick(() => { this.filter = f })
            })
          }
          .width('100%').padding({ left: 16, right: 16 })

          Column({ space: 8 }) {
            if (this.filteredTodos().length === 0) {
              Text('暂无任务').fontSize(14).fontColor('#ccc').margin({ top: 20 })
            } else {
              ForEach(this.filteredTodos(), (todo: Todo) => {
                Row({ space: 12 }) {
                  Text(todo.done ? '✅' : '⬜').fontSize(20)
                    .onClick(() => this.toggleDone(todo.id))
                  Text(todo.text)
                    .fontSize(14).layoutWeight(1).fontColor(todo.done ? '#bbb' : '#1a1a1a')
                    .decoration({ type: todo.done ? TextDecorationType.LineThrough : TextDecorationType.None })
                  Text('✕').fontSize(16).fontColor('#ddd')
                    .onClick(() => this.removeTodo(todo.id))
                }
                .width('100%').backgroundColor('#ffffff').borderRadius(12)
                .padding({ left: 16, right: 16, top: 14, bottom: 14 })
              })
            }
          }
          .width('100%').padding({ left: 12, right: 12, bottom: 24 })

        }
        .width('100%').padding({ top: 12 })
      }
      .layoutWeight(1).backgroundColor('#f8f8f8')
    }
    .width('100%').height('100%').backgroundColor('#f8f8f8')
  }
}

运行效果

初始态:列表为空,三项统计均为 0

idle

添加 4 条任务后:全部 4 / 待完成 2 / 已完成 2,前两条显示删除线

done

注意事项

  1. ArkTS 禁止对象展开{ ...obj, key: val } 会触发 arkts-no-spread 编译错误,必须逐字段构造新对象:{ id: t.id, text: t.text, done: !t.done }。这是 ArkTS 与标准 TypeScript 的重要区别,迁移代码时需全面替换。

  2. 数组方法的可变/不可变区别

    • push/pop/splice/sort(原地变更)→ 不触发 @Local 更新
    • map/filter/concat/[...arr](返回新数组)→ 触发更新 始终使用返回新数组的方法。
  3. build() 内不能声明变量ForEach 回调或其他 UI 构建函数内不允许 const/let 声明(Only UI component syntax can be written here),需提取为组件方法(如 filterLabel())。

  4. @Local 严格私有:父组件不能通过任何方式读取或修改子组件的 @Local 状态。如果需要父写子,用 @Param(article63);子写父,用 @Event(article64)。

  5. 派生计算的性能getStats()filteredTodos() 每次渲染都重新计算。当列表较大时(>1000 项),应改用 @Computed 缓存(article68),避免不必要的重复遍历。

Logo

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

更多推荐