HarmonyOS @Local 完全指南:V2 组件私有状态的不可变更新模式
前言Local是 HarmonyOS 状态管理 V2 中替代State的私有状态装饰器。与State相比Local有两项关键改进严格私有性父组件无任何途径读写它和更清晰的更新语义明确要求不可变更新模式让数据流方向一目了然。本文用一个完整的 TodoList 演示Local操作数组、对象和基本类型的全套模式以及 ArkTS 严格模式下的常见编译陷阱。核心 APILocal 声明语法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 关键差异// V1State 可被父组件通过 ObjectLink 访问 State items: Todo[] [] // 父组件可通过 ObjectLink 读写 // V2Local 严格私有外部无法读写 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前两条显示删除线注意事项ArkTS 禁止对象展开{ ...obj, key: val }会触发arkts-no-spread编译错误必须逐字段构造新对象{ id: t.id, text: t.text, done: !t.done }。这是 ArkTS 与标准 TypeScript 的重要区别迁移代码时需全面替换。数组方法的可变/不可变区别push/pop/splice/sort原地变更→ 不触发Local更新map/filter/concat/[...arr]返回新数组→ 触发更新 始终使用返回新数组的方法。build() 内不能声明变量ForEach回调或其他 UI 构建函数内不允许const/let声明Only UI component syntax can be written here需提取为组件方法如filterLabel()。Local 严格私有父组件不能通过任何方式读取或修改子组件的Local状态。如果需要父写子用Paramarticle63子写父用Eventarticle64。派生计算的性能getStats()和filteredTodos()每次渲染都重新计算。当列表较大时1000 项应改用Computed缓存article68避免不必要的重复遍历。

相关新闻

B站评论数据采集系统:基于Selenium的完整评论层级爬取方案

B站评论数据采集系统:基于Selenium的完整评论层级爬取方案

B站评论数据采集系统:基于Selenium的完整评论层级爬取方案 【免费下载链接】BilibiliCommentScraper B站视频评论爬虫 Bilibili完整爬取评论数据,包括一级评论、二级评论、昵称、用户ID、发布时间、点赞数 项目地址: https://gitcode.com/gh_mirrors/b…

2026/10/24 11:28:44 阅读更多 →
阿里云百炼大模型API调用实战指南

阿里云百炼大模型API调用实战指南

1. 从零开始调用大模型API:完整指南 作为一名长期从事AI应用开发的工程师,我深知初学者在接触大模型API时的困惑。第一次调用API时,我也曾对着文档发愣,不知从何下手。本文将带你完整走通阿里云百炼大模型API的调用全流程&#x…

2026/10/23 20:57:32 阅读更多 →
ComfyUI-WD14-Tagger:AI绘画必备的智能图像标签识别工具

ComfyUI-WD14-Tagger:AI绘画必备的智能图像标签识别工具

ComfyUI-WD14-Tagger:AI绘画必备的智能图像标签识别工具 【免费下载链接】ComfyUI-WD14-Tagger A ComfyUI extension allowing for the interrogation of booru tags from images. 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI-WD14-Tagger 你是否曾…

2026/10/25 10:03:58 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/25 11:19:54 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/24 17:47:00 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/24 18:34:36 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/25 10:02:42 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/25 3:12:30 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/24 21:09:59 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 10:02:43 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/24 11:09:35 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 7:47:32 阅读更多 →