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/9/30 22:38:26 阅读更多 →
阿里云百炼大模型API调用实战指南

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

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

2026/9/24 8:42:42 阅读更多 →
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/9/29 10:04:18 阅读更多 →

最新新闻

从“全民养虾”到快速降温(OpenClaw退潮真相):小龙虾为什么突然不香了,是需求塌了还是供给崩了?

从“全民养虾”到快速降温(OpenClaw退潮真相):小龙虾为什么突然不香了,是需求塌了还是供给崩了?

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

2026/9/30 22:38:37 阅读更多 →
学术PPT的“骨架翻译学”:毕夏AI官网 www.bixiaai.com 如何让论文逻辑自己站起来

学术PPT的“骨架翻译学”:毕夏AI官网 www.bixiaai.com 如何让论文逻辑自己站起来

毕夏AI官网 www.bixiaai.com 毕夏AI写作官网 www.bixiaai.com 毕夏官网 www.bixiaai.com 毕夏智能写作官网 www.bixiaai.com 我是教论文写作的。过去几年,我被问得最多的问题不是“论文怎么写”,而是“PPT怎么做”。 这很讽刺。一篇三万字的硕士论…

2026/9/30 22:38:37 阅读更多 →
三菱PLC ST语言初值写法:声明赋值与首扫描,哪个防丢数据?

三菱PLC ST语言初值写法:声明赋值与首扫描,哪个防丢数据?

先交代一个现场:某天半夜我在调试一台完成品计数设备,触摸屏上显示产量从 0 开始,后台却明明断电前有三万多件。查了半天,问题出在 ST 程序里一行很不起眼的声明赋值——i_done : INT : 0;。产品停了电再上电,声明区的…

2026/9/30 22:38:37 阅读更多 →
开发利器 openCode + Oh My OpenCode:用 TaoToken 统一 Key 打通多智能体编排层

开发利器 openCode + Oh My OpenCode:用 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/9/30 22:38:37 阅读更多 →
零售系统上线后怎么评估效果?连带率、复购率与库存周转的指标设计

零售系统上线后怎么评估效果?连带率、复购率与库存周转的指标设计

零售系统上线以后,项目团队常常需要回答一个问题:除了收银和报表可以正常使用,它究竟改善了什么? 直接比较上线前后的销售额,容易把节假日、折扣活动和门店扩张的影响都算到系统头上。只看使用人数或功能调用次数&…

2026/9/30 22:38:37 阅读更多 →
一键开关机芯片选型的四大工程生死线

一键开关机芯片选型的四大工程生死线

1. 为什么“一键开关机”不是按个按钮那么简单?你拆过那些带“长按3秒开机、短按关机”的智能设备吗?比如某款国产便携式示波器、某品牌工业手持终端,或者你自己焊的STM32开发板——表面看就是个按键控制电源通断,但实际一上电就死…

2026/9/30 22:37:36 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/30 13:14:22 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 18:13:06 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/30 13:14:49 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/30 15:27:04 阅读更多 →