调用链中状态管理V2的定位
想聊 @Local 之前,得先把它的位置摆正。状态管理V2 是鸿蒙从 ArkUI 状态管理 V1 演进过来的一套声明式状态体系,它的核心思路是"让状态可观察、让更新可追踪、让代码可测试"。而 @Local 是 V2 体系里最基础的一批装饰器之一——它代表"组件自身持有的、局部的、可观察的普通状态"。通俗点讲,@Local 就是给组件内部变量贴上一个"可观察"的标签,让变量变化时,UI 能自动跟着刷新。
很多刚接触 V2 的开发者会问:既然 V1 里已经有 @State,为什么还要搞一个 @Local?答案在于 V2 对"状态来源"做了更细致的划分。在一个复杂的业务页面里,状态可以来自组件自己(本地)、可以来自父组件(注入)、可以来自全局(应用级共享)。V1 时代这些来源经常混在一起,一个 @State 既当本地变量又当属性透传,业务复杂后很难分清谁改了什么。V2 用 @Local、@Param、@Global 三个装饰器把来源切分清楚,其中 @Local 只负责"我自己这一亩三分地"。
这篇笔记适合正在学鸿蒙状态管理、想从 V1 平滑过渡到 V2、或者已经上手 V2 但对 @Local 细节还不熟的开发者。我会把 @Local 的定义、用法、适用场景、底层原理、常见坑一次性讲透,代码基于 API 12 及以上实测可用。
1. 内容整体设计与思路拆解
1.1 V1到V2:状态管理为什么需要一次"换代"
先回顾一下 V1 里最常见的问题。V1 的 @State 设计得很早,当时 ArkUI 组件化还没那么成熟,所以它承担了太多职责:既能标记本地状态,又能通过赋值触发子组件更新,还能借助 $ 双向绑定做一些隐式操作。这些能力叠加在一起的后果是,状态流的可预测性变差。我在实际项目里遇到过一个很典型的情况:一个深层的子组件不小心直接改了父组件传下来的 @State 数组里的某个对象属性,结果整条链路的状态都乱了,排查了整整一个下午,最后发现是"引用共享"导致的隐式修改。
V2 的诞生就是冲着这些问题来的。它把状态分成了三类来源,用三个装饰器表达:
| 装饰器 | 状态来源 | 典型场景 |
|---|---|---|
| @Local | 组件自身持有 | 页面内部的临时开关、输入框文本、下拉选中值 |
| @Param | 父组件传入 | 父组件配置项、子组件初始化参数 |
| @Global | 应用级共享 | 登录态、主题配置、跨页面数据 |
这个划分的好处一眼就能看出来:每个状态都有明确的"所有权"。"谁拥有、谁修改、谁负责"变成了可执行的代码规范。你在 code review 时只要看到 @Local 就知道这变量只属于当前组件,绝不会漂移到别处;看到 @Param 就知道它只读,想改就得通过回调。
1.2 @Local 在 V2 体系中的生态位
@Local 对应英文 "Local State",直译就是"局部状态"。它装饰的变量由组件自己管理和初始化,不依赖外部传入。V2 框架会为这些变量建立观察能力,变量值发生变化时,依赖它的 UI 部分会自动重新渲染。
用生活化类比来理解:@Local 就像你办公桌上的一本便利贴,写什么、改什么都是你自己的事,不需要老板审批,也不需要同事签字。而 @Param 是公司下发的任务文档,内容是别人拟好的,你只能照着执行;@Global 是公司公告栏,所有人都能看到,改一条全员知晓。
从框架实现角度看,@Local 底层通过属性代理和依赖收集机制实现观察。简单说,当你访问一个 @Local 变量的值时,框架会记录"当前 UI 依赖于这个变量";当你给变量赋新值时,框架会通知所有依赖方重新求值。这个"依赖记录-变更通知"的模型和 Vue 的响应式原理非常像,只是鸿蒙做了自己的 ArkUI 运行时实现。
1.3 为什么状态来源划分这么重要
很多初学者觉得,多一个装饰器就是多一个语法糖,其实不然。状态来源划分带来三个直接收益:
第一个收益是代码可读性提升。读代码时不再需要追着变量满文件跑,@Local 一眼就能确认变量的作用域和生命周期。第二个收益是变更范围可控。状态归谁管,谁就能改;从机制上杜绝了跨层隐式修改。第三个收益是测试性增强。V2 的状态类可以被独立实例化出来做单测,这在 V1 时代几乎不可能。
我自己的体会是,V2 这套设计更像是在给前端的"状态所有权"立规矩。规矩立好了,协作开发和后期维护都会轻松很多。特别是团队超过三个人同时做一个页面时,这个优势会被放大得非常明显。以前 V1 时代 code review 需要反复确认"这个 @State 是不是被哪个子组件联动修改了",现在看到 @Local 就心里有数了。
2. 核心细节解析与实操要点
2.1 @Local 的基础用法:五步上手
先看一个最基础的例子,一个带计数器的小组件:
@Entry @Component struct CounterPage { @Local count: number = 0; build() { Column({ space: 12 }) { Text(`当前计数:${this.count}`) .fontSize(24) Button('加一') .onClick(() => { this.count++; }) } .padding(16) } }这个例子麻雀虽小,五脏俱全。关键的五个步骤是:
- 导入组件依赖(API 12 起 V2 装饰器无需显式 import,DevEco Studio 会自动处理)。
- 用 @Local 修饰变量,并赋初始值。
- 在 build 里通过 this.xxx 读取变量。
- 在事件回调里通过 this.xxx = 新值 修改变量。
- 界面自动刷新,无需手动调用任何更新方法。
你可能会问,为什么 V2 的 @Local 可以不用描述类型?因为在 V2 体系中,@Local 支持类型推断,基础类型、对象、数组、联合类型都能直接推断。当然,显式标注类型依然是更严谨的写法,尤其在大型项目里,显式类型本身就是一种文档。
实操心得:我建议所有 @Local 变量都显式初始化。V2 在未初始化时虽然不会报错,但可能出现不确定的初始渲染,尤其是对象类型。显式初始化还能顺便当作"默认值文档",后面维护的人一眼能看出这个组件的初始样子。遇到过不少线上问题,最后查下来就是某个变量忘了初始化,导致首帧渲染拿到的值是 undefined。
提示:@Local 修饰的变量必须是组件实例的成员变量,不能是局部变量。局部变量用不上装饰器,也不具备观察能力。
2.2 对象与数组:@Local 的深度观察能力
V2 的 @Local 不只是观察基础类型,它对对象和数组也做深层次观察。看这个例子:
@Entry @Component struct UserCard { @Local user: { name: string; age: number } = { name: '张三', age: 25 }; @Local tags: string[] = ['开发者', '鸿蒙']; build() { Column({ space: 12 }) { Text(`姓名:${this.user.name}`) Text(`年龄:${this.user.age}`) ForEach(this.tags, (tag: string) => { Text(tag) }) Button('改名').onClick(() => { this.user.name = '李四'; // 深层对象的属性修改也能触发UI更新 }) Button('加标签').onClick(() => { this.tags.push('开源爱好者'); // 数组方法调用同样触发UI更新 }) } .padding(16) } }这段代码里,this.user.name = '李四' 直接修改了嵌套属性的值,this.tags.push() 调用了数组的变异方法,两处都会触发界面刷新。这在 V1 的某些场景下是需要额外处理才能做到的,V2 把它们变成了默认行为。
底层原理:@Local 在初始化时会对对象类型进行代理包装,对数组则拦截变异方法(push、pop、splice 等)和索引赋值。访问属性时收集依赖,修改属性或调用变异方法时触发更新。
实操注意事项:
- 对象的整体替换(this.user = { name: '王五', age: 30 })同样生效,且性能更优,因为一次变更通知就能完成整个子树更新。
- 数组的索引直接赋值 this.tags[0] = 'x' 在 V2 中也可以被观察到,但建议优先用 splice 或整体替换,语义更清晰。索引赋值的可读性比较差,后面的人看代码时不知道你为什么只改了一个位置。
- 不要使用解构赋值去"接住"对象的某一层,然后修改解构出来的临时对象,那样修改的是副本,UI 不会更新。这个坑我在后面排查案例里会详细说。
2.3 @Local 与 @State 的关键差异
| 对比维度 | @State(V1) | @Local(V2) |
|---|---|---|
| 状态来源 | 混合语义,既本地又透传 | 仅本地持有 |
| 对象深观察 | 部分场景依赖额外处理 | 默认深度代理 |
| 数组方法 | 个别版本需 @Observed 配合 | 默认拦截变异方法 |
| 类实例 | 需配合 @Observed 和 @ObjectLink | 可直接观察普通类 |
| 跨层传递 | 可双向绑定,存在隐式修改风险 | 组件私有,杜绝外泄 |
选择建议:新项目一律用 V2,老项目迁移时把"纯本地状态"的 @State 直接替换成 @Local,风险很低;把"父传子"的 @Prop/@Link 替换成 @Param + 回调,这个迁移工作量稍大但值得做。我自己在迁移一个中型项目时,光是替换 @Prop/@Link 就花了两天,但迁移完之后的双周迭代明显顺畅了。
3. 实操过程与核心环节实现
3.1 实操前的环境准备与版本确认
动手之前先把环境对齐,我实测的配置是:
- DevEco Studio 5.0.0 及以上版本
- API 12(HarmonyOS NEXT 及以上)或更高
- 项目编译目标设置为 API 12+,并且启用了 V2 状态管理(新版 DevEco Studio 默认支持)
检查是否处于 V2 模式,最直接的方法是看工程里有没有开启状态管理 V2 的编译开关。新版工程模板里,build-profile.json5 或 module.json5 中会有相关配置项,如果找不到开关,那么默认就是 V2 模式。
有几个环境细节要注意。第一,模拟器和真机的行为在某些版本上会有细微差异,数组的变异方法在模拟器上如果出现不刷新的情况,优先在真机上复现确认,不要急着怀疑是代码问题。第二,DevEco Studio 的实时预览对 V2 装饰器的支持在个别版本有延迟,代码改动后如果预览不刷新,重启一下预览进程就好,不代表代码本身有错。第三,如果项目是从旧版本升级上来的,注意清理构建缓存,我在升级后遇到过装饰器没生效的假象,清理重编后就正常了。
3.2 从 V1 迁移的最小改造实例
假设有一个老项目里这样写:
@Entry @Component struct OldPage { @State count: number = 0; @State list: string[] = []; build() { // ...省略UI } }迁移到 V2,只需要做两处改动:
@Entry @Component struct NewPage { @Local count: number = 0; @Local list: string[] = []; build() { // ...UI不变 } }注意,这个"最小改造"只适用于组件自身持有、不依赖父组件的状态。如果 @State 状态来自父组件透传,那就要一并对父子的通信方式做改造,不能简单替换。
我的建议是:迁移时按"由叶到根"的顺序推进。先迁移不依赖任何外部状态的叶子组件,再逐步迁移中层,最后处理根组件状态。每迁移一层,跑一遍构建和关键路径冒烟测试,不要一次性大范围替换,否则出问题不好定位。我曾经见过一个团队试图在一个大版本迭代里同时迁移十几个页面,结果线上出了三个关联性 bug,花了双倍时间才排完。
3.3 构建一个带搜索过滤的列表页(完整示例)
来一个更接近真实业务的完整示例,一个带搜索过滤的商品列表页:
@Entry @Component struct ProductListPage { @Local keyword: string = ''; @Local products: Product[] = []; @Local filtered: Product[] = []; aboutToAppear(): void { // 模拟从服务端拉取数据 this.products = [ { id: 1, name: '鸿蒙开发板', price: 299 }, { id: 2, name: '智能门锁', price: 599 }, { id: 3, name: '车载中控屏', price: 1299 }, ]; this.filtered = [...this.products]; } build() { Column({ space: 12 }) { TextInput({ placeholder: '输入商品名过滤' }) .onChange((value: string) => { this.keyword = value; this.filtered = this.products.filter( item => item.name.includes(this.keyword) ); }) List({ space: 8 }) { ForEach(this.filtered, (item: Product) => { ListItem() { Row({ space: 8 }) { Text(item.name) Text(`¥${item.price}`) } } }, (item: Product) => item.id.toString()) } .layoutWeight(1) } .padding(16) } } interface Product { id: number; name: string; price: number; }这里有两个容易忽略的细节。
第一个细节是 filtered 数组必须整体替换,不能直接修改原数组再期望自动过滤。因为过滤逻辑本身是"生成新数组"的过程,不是"原地修改"。整体替换 this.filtered = newArray 这一行,V2 会认为依赖它的整个列表都变了,从而触发完整重渲染。这里性能上可能有人担心全量重渲染会比精准更新慢,但在列表规模不大(百级以内)的场景下,整体替换的简单性和可靠性远大于那点性能差异。
第二个细节是 ForEach 的 key 生成器。我用了 item.id.toString() 作为唯一键,这样当数据更新时,框架能精准对比出哪些列表项需要重建、哪些可以复用。如果不用 key 或者用不稳定的 key(比如直接用 index),列表复用时会产生渲染错乱。这个错乱很隐蔽,表现是"滚动到某个位置时突然出现重复项"或者"删除项后 UI 错位",排查起来非常痛苦。
3.4 在 @Local 中使用自定义类
V2 可以直接观察自定义类的实例属性,这一点比 V1 舒服太多。看下面的示例:
@Entry @Component struct TaskCard { @Local task: Task = new Task('写周报', false); build() { Column({ space: 12 }) { Text(this.task.title) Checkbox() .select(this.task.done) .onChange((checked: boolean) => { this.task.done = checked; }) } .padding(16) } } class Task { title: string; done: boolean; constructor(title: string, done: boolean) { this.title = title; this.done = done; } }在 V1 时代,想要观察一个自定义类实例的属性变化,你得给类加 @Observed 装饰器,还要在父组件用 @ObjectLink 接住。V2 直接取消了这套繁琐的配套,@Local 对任意类实例的属性都有观察能力。实测下来,类实例字段的修改、嵌套对象属性的修改、数组内对象的属性修改,都能稳定触发更新。
一个小提醒:虽然 @Local 能观察普通类实例,但在设计数据模型时,我还是建议把"可观察数据"和"业务逻辑方法"分开。不要在数据类里塞一堆方法,保持类的纯粹性,这样观察性能更好,测试也更容易。比如上面这个 Task 类,如果你给它加一个 saveToServer() 方法,测试的时候就要 mock 网络层,很麻烦;不如把保存逻辑抽到 Page 层或者独立的 Service 里。
4. 状态更新机制与框架源码视角
4.1 依赖收集与更新触发链路
把 V2 底层的更新链路拆开看,主要有四个环节:
- 读取阶段:UI 渲染时访问 @Local 变量,框架记录"哪个 UI 节点依赖了这个属性"。
- 修改阶段:代码给 @Local 变量或它的深层属性赋值,框架捕获这次写入。
- 通知阶段:框架根据依赖记录,找到所有受影响的 UI 节点。
- 渲染阶段:框架只重新执行受影响节点的构建函数,生成新的 UI。
一个容易误解的点是:不是整个页面都会重新渲染,而是只有依赖了被修改属性的 UI 部分会重新渲染。ArkUI 的运行时会做精确到属性的依赖追踪,这一点在复杂页面上的性能收益很明显。比如一个页面顶部有个用户头像,底部有个数据列表,你只修改了列表数据,头像部分根本不会重新执行构建函数。
4.2 框架如何包装对象类型
对于对象类型的 @Local 变量,运行时在初始化时会生成一个代理对象。这个代理对象拦截属性访问和属性写入:
- 访问属性时,检查当前是否有正在渲染的 UI 上下文,如果有就建立依赖。
- 写入属性时,标记属性为 dirty,并触发异步刷新。
这里的设计目的很明确:把"数据操作"和"UI 更新"解耦。开发者只管改数据,框架负责在合适的时机批量刷新 UI,避免每次赋值都同步重绘,造成性能浪费。实际写代码时你不用关心这些,但理解了这个机制,遇到"为什么我改了数据但是没立刻看到 UI 变化"这类问题时,就不会慌,因为它可能只是框架在等下一个刷新周期。
4.3 与 Vue 响应式原理的对照
理解 V2 的响应式原理,借 Vue 来对照会很快。Vue 3 的 reactive 用 Proxy 代理对象,收集 effect 依赖,触发 update。ArkUI V2 也是类似的思路:代理对象做依赖收集,赋值时触发通知。不同点是 ArkUI 的"依赖"不再是 effect 函数,而是 UI 组件节点和它们的构建函数。
这个对照不是为了掉书袋,而是帮你迁移已有的前端知识。如果你懂 Vue 3 的响应式原理,学习 V2 状态管理会快很多;反过来,如果你先学了 V2,再去看 Vue 3 源码,很多概念也能对上号。我在团队内部做技术分享时就常用这个类比,前端同事理解起来特别快,原生开发同事则需要多花一点时间,但只要把"构建函数"当成"自动重新执行的渲染函数"来想,也很快能通。
5. 常见问题与排查技巧实录
5.1 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 修改对象属性后 UI 不刷新 | 对象不是 @Local 修饰,或在解构副本上修改 | 用 @Local 声明,直接在原对象上改属性 |
| 数组 push 后界面不更新 | 使用了不支持观察的旧版 API 或拿局部变量 push | 确认在 this 上调用,必要时整体替换数组 |
| 列表项复用后状态错乱 | ForEach 没用唯一 key 或 key 不稳定 | 用业务 id 做 key,避免用 index |
| 自定义类属性不更新 | 类实例不是通过 @Local 直接持有 | 把实例字段挂到 @Local 变量上 |
| 页面销毁后还有异步任务改状态 | 定时器/网络回调未清理 | 在 aboutToDisappear 里清理异步任务 |
5.2 一个典型的排查案例:为什么改名没反应
有位朋友遇到一个问题:页面上有个 Text 显示用户名,点击按钮修改 this.user.name,但界面纹丝不动。代码长这样:
@Entry @Component struct BugPage { @Local user: any = { name: '张三' }; build() { Column() { Text(this.user.name) Button('改名').onClick(() => { const tmp = { ...this.user }; tmp.name = '李四'; this.user = tmp; }) } } }初看好像没问题,整体替换对象嘛,应该触发更新的。但问题恰恰出在解构赋值上:{ ...this.user } 是浅拷贝,如果对象只有一层,确实能复制到 name 属性;但如果 user 对象里有嵌套对象,浅拷贝会把嵌套对象的引用一起拷过来,导致对嵌套对象的修改仍然作用在原对象上。另一个可能的坑是:tmp 是一个全新的普通对象,V2 的代理没有覆盖它,所以 this.user = tmp 虽然触发了整体替换,但 tmp 内部不再是代理对象,后续对 tmp 的深层修改就观察不到了。
我的建议是:不要手动复制再赋值。直接 this.user.name = '李四',让框架的代理机制去处理。如果业务上确实需要不可变更新,就在替换前对原对象做结构化克隆,保证替换后的整个对象是全新的原始对象,再由 @Local 重新完成代理包装。
5.3 状态管理的几个实战禁忌
第一个禁忌是"跨组件偷改状态"。V2 虽然把状态来源分清了,但如果你在子组件里仍然通过某种方式拿到了父组件的 @Local 对象引用并修改它,那就能绕过机制。所以 code review 时要重点检查有没有把 @Local 对象作为参数传给子组件然后子组件直接改属性的写法,如果有,一定要改成回调模式。
第二个禁忌是"在 build 中做副作用"。build 函数应该是纯渲染函数,不能在里面发起网络请求、修改状态、生成随机数。V2 的依赖追踪在 build 执行时建立,如果在 build 里改状态,会重新触发 build,造成死循环。我在实际项目里见过因为这个卡死的页面,表现是点击一次之后整个页面假死,日志里全是重复构建的警告。排查方法很简单,把 build 里所有赋值语句和网络调用全部移到事件回调里就正常了。
第三个禁忌是"滥用 @Local 存所有数据"。@Local 适合组件内部临时状态。如果是多组件共享数据,优先考虑 @Param 注入和回调;如果是全局数据,用 @Global 或应用级存储。把全局登录态塞到某个页面的 @Local 里,一旦页面销毁状态就没了,而且其他页面还访问不到。规范的做法是:先判断状态的"生命周期"和"作用域",再决定用哪个装饰器,而不是哪个顺手用哪个。
5.4 与其他装饰器搭配的常见模式
@Local 很少单打独斗,实践中常用的搭配有:
- @Local + @Param:组件内部有自己状态,又需要接受父组件的初始配置。
- @Local + @Global:本地临时状态 + 全局共享数据。
- @Local + @Monitor:监听本地状态变化,执行副作用逻辑。
@Monitor 是 V2 里的监听装饰器,用于观察 @Local/@Param/@Global 等状态的变化,然后执行自定义逻辑。比如搜索框场景里,可以用 @Monitor 监听 keyword 变化,然后调用接口做防抖搜索,而不是在 onChange 里写一堆逻辑。这个组合在 V2 项目里非常实用,因为它把"数据变更"和"响应动作"之间的关系显式声明出来了,后续加逻辑不用去翻 UI 回调。
举个例子,@Monitor 监听 keyword 变化后自动过滤列表:
@Entry @Component struct MonitorSearchPage { @Local keyword: string = ''; @Local allItems: string[] = ['鸿蒙开发', '状态管理', '装饰器']; @Local filteredItems: string[] = []; @Monitor('keyword') onKeywordChange() { this.filteredItems = this.allItems.filter( item => item.includes(this.keyword) ); } aboutToAppear(): void { this.filteredItems = [...this.allItems]; } build() { Column({ space: 12 }) { TextInput({ placeholder: '输入关键字' }) .onChange((value: string) => { this.keyword = value; }) ForEach(this.filteredItems, (item: string) => { Text(item) }) } .padding(16) } }这个写法把过滤逻辑从 onChange 里解放出来了。TextInput 负责改 keyword,@Monitor 负责响应变化并更新列表。代码读起来非常清爽,后面如果要加埋点、加统计,直接在 onKeywordChange 里追加就行,不会污染 UI 层。
6. 实操总结与进阶路线参考
6.1 把 @Local 用到熟练的标志
什么时候算真正掌握了 @Local?我的判断标准是三条:
第一,清楚它的状态来源定位,拿到需求能立刻说出"这个状态应该放 @Local 还是 @Param"。第二,知道深观察的边界,知道哪些修改能触发更新、哪些不能。第三,能写出可维护的状态代码,不把业务逻辑混进 build,不用 @Local 过度承载跨组件数据。
做到这三条,你在团队里基本就是状态管理的"靠谱担当"了。别人遇到"界面不刷新"的怪问题,大概率第一时间会来找你。
6.2 接下来建议学习的方向
掌握了 @Local,可以从两个方向继续深入:
一个是同体系内的扩展:学习 @Param 的传参和初始化、@Global 的全局共享、@Monitor 的监听编排、@Computed 的计算属性。这些组合起来基本就是 V2 状态管理的全部骨架。特别是 @Computed,它解决的是"派生状态"的问题,比如列表的统计数字、过滤后的数量,这些都不该用普通变量手动维护,而应该用计算属性自动推导。
另一个是结合场景的实战:用 V2 状态管理重构一个已有的 V1 页面,从简单列表到复杂表单,再到带刷新的详情页。每重构一个页面,对装饰器边界的理解就深一层。我自己就是在重构一个含表单校验、多级联动、草稿保存的复杂页面时,才真正理解了为什么 V2 要把状态来源分得这么清。
6.3 最后分享一个我在实际使用中的小技巧
写 @Local 时,我习惯把"初始值"写成具名常量,而不是直接写在声明里。比如:
const DEFAULT_KEYWORD = ''; @Entry @Component struct SearchPage { @Local keyword: string = DEFAULT_KEYWORD; }这样做的原因是:当后续要做重置功能时,直接引用常量,不用再复制一遍初始值,也不会因为多次手写值不一致而出错。在状态多的页面里,这个习惯能省下不少心智负担。比如一个筛选页有七八个筛选条件,初始化时要赋值一遍,重置时要再赋值一遍,如果初始值是散落在各处的字面量,重置功能很容易出现"原样是 A,重置后是 B"的诡异问题。
另外再补充一个小的实战经验:如果你发现一个页面的 @Local 变量超过 10 个,就要警惕是不是状态拆分的粒度太粗了。可以考虑把一组强相关的状态封装成一个类,用单个 @Local 持有,既减少装饰器数量,又能天然形成一组“内聚状态”。比如筛选条件可以封装成 FilterState 类,包含 keyword、category、priceRange 等字段,整个组件只需要一个 @Local filter 变量。这个习惯能显著提升代码的可读性。
状态管理是一条可以走很深的路,但掌握 @Local 就是一个非常扎实的起点——它帮你理解 V2 的观察模型、依赖收集和更新链路。把这个基础打牢后,后面的 @Param、@Global、@Monitor 学起来都会轻松不少。希望这篇笔记能帮你在鸿蒙状态管理 V2 的路上少踩几个坑。