当前位置: 首页 > news >正文

HarmonyOS @Local 完全指南:V2 组件私有状态的不可变更新模式

前言

@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

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

注意事项

  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),避免不必要的重复遍历。

http://www.jsqmd.com/news/1267850/

相关文章:

  • 【私密内参】头部AI实验室绝少公开的逻辑题压力测试框架:融合形式验证+反事实扰动+认知负荷建模
  • JSBSim完整指南:如何用开源飞行动力学库构建专业级飞行仿真
  • 【小白系列】GaussianBeV + REVFormer:3D高斯表示与可逆Transformer的自动驾驶BEV感知实战|全流程拆解 + 疑难解答
  • Python+Pygame贪吃蛇实战:从零掌握游戏开发核心架构
  • HR面试后整理记录怎么选?2026年3款灵听录音转文字工具 快速输出完整面试纪要
  • 2026毓典奢品汇|北京轻奢名包回收 新款闲置包包高效保值变现指南 - 名表行情观察
  • 2026年AI产品经理转型指南:大模型技术全解析
  • 基于YOLOv8的阿丁克拉符号识别系统设计与优化
  • CC110x/CC2500串行同步模式:实现无线传感器网络连续流传输
  • 销售自动开票获取用户填写抬头信息—东方仙盟
  • Wayback Machine:你的智能网页时光机,拯救消失的网络记忆
  • 嵌入式硬件设计:电源时序与时钟系统在汽车级SoC中的关键作用
  • 为什么tkinter-helper是你Python GUI开发的终极解决方案?3个强力理由揭秘
  • 10分钟上手BetterDiscordPanel:从安装到发送第一条消息的快速入门
  • PianoPlayer终极指南:免费智能钢琴指法生成工具快速上手
  • 3大技术突破:MobileNet-Yolo如何实现移动端毫秒级目标检测
  • 芜湖市闲置黄金变现,璟安黄金回收上门回收黄金,验货即刻打款 - 新芸鼎珠宝首饰
  • 程序员必知的大模型核心技术与实战指南
  • AI视频工业化生产已落地:从脚本生成→语音克隆→智能分镜→动态渲染的5层技术栈全拆解(附2024私有化部署清单)
  • Seedance 2.0:动态种子扩散技术解析与AI视频生成实战
  • 基于TI C2000的BLDC无感梯形控制:从硬件配置到闭环调试全流程
  • 深入解析DM355 IPIPE:嵌入式ISP硬件流水线设计与工程实践
  • GetQzonehistory:三步实现QQ空间历史说说的完整数字化备份
  • 3步实现AI转PSD:矢量图层完整保留的终极解决方案
  • 基于嵌入向量的智能对话话题聚类:从原理到工程实践
  • 计算机视觉文献综述与方法论构建指南
  • 别踩2026做视频号内容总结的常见误区 我实测整理的可落地实操经验
  • LSSVM优化与VMD分解在电力负荷预测中的应用
  • 从聊天机器人到自主智能体的开发实践
  • AI Agent架构演进:从ReAct到龙虾架构的深度解析