React Native 适配鸿蒙这阵子我其实折腾了不少,真要说哪个环节最让我挠头,不是桥接问题,也不是样式差异,反而是状态管理。尤其是涉及 MobX 的 Observable 数据监听,在鸿蒙这套运行环境里,踩的坑比在普通 Android 上多出一倍不止。这篇文章我不打算复述文档,就讲我实际跑过的一个模拟项目X里,怎么把 MobX 的数据监听在鸿蒙侧调稳,以及哪些问题是你大概率也会遇到的。
这套方案适合谁?如果你正准备把现有的 React Native 工程迁移到鸿蒙,或者刚拿到一个“鸿蒙优先”的跨端需求,又不想把状态管理整套重写,那 MobX 的 Observable 模型是性价比很高的选择。它能让你少写很多手动同步的胶水代码,但前提是你得真正吃透它的监听机制。下面我会从选型思路、核心概念、实操落地到问题排查,一条线讲清楚。
1. 为什么在鸿蒙适配中绕不开状态管理这道坎
1.1 RN跨端架构下的状态同步难点
先聊一个基础问题:在React Native工程里,JS逻辑和UI渲染其实是两套体系。普通 Android 上,JS 层的数据变化通过 Bridge 通知原生层,再由原生 View 刷新;鸿蒙这边走的也是类似思路,但它的自绘渲染机制和事件调度跟 Android 不完全一样,中间多了一层鸿蒙自己的UI框架参与。这意味着,如果你的业务状态散落在各个组件的 useState 里,跨页面、跨模块同步时会变得非常痛苦——你要么层层回调,要么全局挂一个事件总线,最后代码里全是手动通知和清理逻辑。
我当时在模拟项目X里做一个双栏联动的商品选择页,左侧分类、右侧商品列表、底部购物车角标,三个模块都要响应同一个数据源的变化。如果不用统一状态层,就得在每个模块里各自维护一份副本,再通过回调或者全局事件去对齐。听起来还行,但一旦加入接口加载、筛选条件、本地缓存这些因素,副本之间很容易出现“你说更新了、我没收到”的尴尬情况。鸿蒙这边的日志排查又不像 Android 那么顺手,出了问题定位成本很高。
所以结论很直接:跨端适配时,必须有一个统一、可预测、能集中管理的状态层。状态层负责把数据变化“广播”给所有关心它的组件,同时把业务逻辑从 UI 里抽出来,避免页面代码越来越臃肿。
1.2 为什么偏偏是MobX的Observable模型更适合这种场景
在状态管理选型上,我见过不少团队纠结 Redux、MobX、Zustand 三选一。Redux 的单一数据源和纯函数 reducer 确实很规整,但样板代码多,对于需要快速完成鸿蒙适配的团队来说,心智负担不低。Zustand 轻量,但它的订阅机制在跨端环境里偶尔会有更新时序的边界问题,调试起来需要额外小心。
MobX 的优势在于它的响应式模型天然贴近“数据变化驱动界面更新”这件事。你可以把 Store 里的字段声明成 Observable,当字段值变化时,所有依赖它的组件和计算属性自动收到通知。这不是通过组件树逐层派发,而是基于依赖收集的精准通知——谁依赖了这份数据,谁就被通知,没依赖的完全不受影响。
这种模型放到鸿蒙适配的场景里,最大的价值是减少了“手动关联”的出错点。你不需要像 Redux 那样写一堆 action、reducer 再 map 到组件 props,只需要在组件外层包一个 observer,MobX 会在渲染过程中自动收集当前组件用到了哪些可观察数据,之后数据一变,组件自动重渲染。简单说,开发者只需要关心“数据在哪里、怎么改”,不需要关心“改完之后怎么让它传到界面上”。
商业项目里最怕的不是逻辑复杂,而是逻辑分散。MobX 能把状态收敛到一个或多个 Store 里,同时通过 Observable 数据监听把状态和视图的同步变成半自动过程,这在鸿蒙适配的人力紧张阶段非常关键。
2. MobX可观察数据在鸿蒙环境里的特殊表现
2.1 需要吃透的几个核心概念
直接用 MobX 做业务,不需要把源码全部啃完,但有几个概念必须搞清楚,否则后面排查问题会无从下手。
第一个是 Observable。这是 MobX 监听的基础,相当于给数据装了一颗“信号发射器”。当你把一个对象、数组或者类字段标记为 observable 后,MobX 会通过代理或者属性劫持的方式跟踪它的读写。读的时候记录“谁在依赖它”,写的时候通知“所有依赖它的人”。
第二个是 Computed,也就是计算属性。它本身也是一个可观察值,但它的值是由其他可观察值推导出来的。比如购物车里“总价 = 商品A价格 + 商品B价格”,当商品数量变化时,总价会自动更新。在鸿蒙适配中,我习惯把需要二次加工的数据都放进 computed,避免在组件里做重复计算,也避免因为计算时机不对导致界面不一致。
第三个是 Action。所有修改可观察数据的操作都应该放在 action 里。这不是形式主义,而是因为 action 能保证批量修改在一次事务里完成,不会出现“改了第一个字段、界面刷新了一半、第二个字段还没改”的中间态。在鸿蒙的渲染帧调度下,这种中间态更容易引起视觉闪烁。
第四个是 Reaction,它是在数据变化时自动执行的副作用函数。组件层的 observer 本质上就是 Reaction 的一种封装,它会在可观察数据变化时安排重渲染。理解这一点,就能明白为什么“观察者必须真正读取可观察数据”这件事如此重要——如果组件没有读取某个 observable,MobX 就不会认为它依赖了这个数据,自然不会在数据变化时通知它。
2.2 鸿蒙桥接层对数据监听的影响
说实话,MobX 本身是纯 JS 库,它在鸿蒙和 Android 上的核心逻辑完全一致,真正不一样的是“界面更新怎么到达鸿蒙原生层”。
React Native 在鸿蒙上运行时,组件树最终会通过适配层映射到鸿蒙的原生组件。当 MobX 检测到数据变化并触发组件重渲染时,React 的 diff 过程会计算出哪些节点需要更新,再把这些更新指令通过桥接层发给鸿蒙侧。这个过程在 Android 上很成熟,但在鸿蒙适配初期,桥接层的通信效率、批量更新能力都在持续优化中,所以偶尔会出现“数据已经变了,界面晚了一拍才动”的现象。
我给模拟项目X做性能采样时发现,如果某个 Store 在一个事件里连续修改了十几个字段,MobX 会把所有变化都通知给观察者,React 也可能因此触发多次协调过程。在 Android 上这通常能合并成一次渲染提交,但在鸿蒙侧,适配层的批处理能力弱一些,实际表现出来的就是界面更新有轻微卡顿。
针对这个问题,我在项目中做了一件事:把高频且非关键的更新从 mobx-react-lite 的默认调度中拆出来。比如拖拽排序时的临时位置变化,不放在 observer 组件读取的 observable 里,而是用局部状态暂存,排序结束后一次性写入 Store。这样既不破坏 MobX 的监听机制,又能减少鸿蒙桥接层的压力。
2.3 可观察粒度的选择
在使用 MobX 时,很容易掉进“什么都做成 observable”的陷阱。如果可观察数据过多、依赖关系过密,MobX 的依赖收集过程本身会成为性能瓶颈。我见过有些同事把一个接口返回的完整业务对象全部展开到 observable 字段里,结果一个列表项的数据变化会触发几十个组件的重新评估。
合理做法是保持可观察数据的粒度“按需最小化”。接口数据里有些字段是纯展示用的,页面只读不写,这种不需要变成 observable;如果字段之间没有联动关系,也不需要塞进同一个 observable 对象。尤其是在鸿蒙侧渲染成本比 Android 更高的阶段,更小的可观察粒度意味着更少的界面更新点,性能表现会更稳定。
3. 实操:在鸿蒙工程里把MobX监听真正跑起来
3.1 环境准备和依赖安装
先说环境。模拟项目X是基于 React Native 的鸿蒙适配分支做的,开发时需要用鸿蒙开发工具链配置 RN 工程。这一步各个团队的初始化流程不太一样,但不管怎么初始化,最终你得到的应该是一个能在鸿蒙模拟器里跑起来的 RN 应用。
依赖安装很简单,只需要两个包:
npm install mobx mobx-react-lite我在项目里用的是 mobx 6.x 和 mobx-react-lite 4.x。注意不要安装旧版 mobx-react,那是针对 class 组件的,写起来更啰嗦,而且在新版本 React Native 和鸿蒙适配层下没有优势。
安装完以后,需要在工程入口处引入 config:
// 入口文件,通常是 index.tsx 或 App.tsx import { configure } from 'mobx'; configure({ enforceActions: 'observed', useProxies: 'always', });enforceActions: 'observed'的意思是:只有被观察的数据修改才强制要求放在 action 里。这样做能避免在鸿蒙调试阶段不小心在组件事件里直接改 Store 数据,导致更新时序混乱。useProxies: 'always'是让 MobX 优先使用 Proxy 实现,性能更好。
3.2 定义可观察Store:从设计到落地
在模拟项目X里,我维护了一个商品选择页的 Store,核心代码如下:
import { makeAutoObservable, runInAction } from 'mobx'; export interface Product { id: string; name: string; price: number; stock: number; selected: boolean; } export class ProductStore { categoryList: string[] = []; productList: Product[] = []; selectedCategory = ''; selectedProducts: Map<string, number> = new Map(); loading = false; constructor() { makeAutoObservable(this); } get selectedCount() { return Array.from(this.selectedProducts.values()).reduce( (sum, count) => sum + count, 0 ); } get selectedTotalPrice() { return this.productList.reduce((sum, product) => { const count = this.selectedProducts.get(product.id) ?? 0; return sum + product.price * count; }, 0); } async fetchCategoryList() { this.loading = true; try { const data = await requestCategoryList(); runInAction(() => { this.categoryList = data.list; this.selectedCategory = data.list[0] ?? ''; }); } finally { runInAction(() => { this.loading = false; }); } } selectCategory(category: string) { this.selectedCategory = category; this.productList = []; this.fetchProductList(category); } async fetchProductList(category: string) { this.loading = true; try { const data = await requestProductList(category); runInAction(() => { this.productList = data.list; }); } finally { runInAction(() => { this.loading = false; }); } } addProduct(id: string) { const count = this.selectedProducts.get(id) ?? 0; this.selectedProducts.set(id, count + 1); } removeProduct(id: string) { const count = this.selectedProducts.get(id) ?? 0; if (count <= 1) { this.selectedProducts.delete(id); } else { this.selectedProducts.set(id, count - 1); } } }有几个细节值得展开说。makeAutoObservable(this)放在构造函数里,它会把所有字段变成 observable,把 getter 变成 computed,把方法变成 action。字段里我用的是 Map 来保存选择数量,Map 本身也是可观察的,但 MobX 对 Map 的跟踪是“引用级”的——修改已有 key 的值不会触发更新,只有新增或删除 key 才会。所以我在addProduct和removeProduct里用了整体替换的方式,而不是直接修改 key 对应的 value,这是很多新手容易忽略的点。
异步方法的处理也很有讲究。fetch 方法里的第一行this.loading = true是同步执行的,直接改没问题。但 await 之后返回的数据赋值,需要包在runInAction里。MobX 6 在启用enforceActions的情况下,异步回调里修改 observable 会直接报错。runInAction就是用来明确告诉 MobX:这是一个合法的同步修改事务。
3.3 在组件层绑定数据监听的最佳姿势
Store 定义好了,接下来是组件消费。基础写法大家都见过:
import { observer } from 'mobx-react-lite'; const ProductList = observer(({ store }: { store: ProductStore }) => { return ( <View> {store.productList.map((product) => ( <ProductItem key={product.id} product={product} store={store} /> ))} </View> ); });但这里面有个容易被忽略的细节:当 Store 里的 productList 变化时,ProductList会重新渲染,可它下面的每个ProductItem不一定都会跟着重渲染。如果ProductItem不是 observer,那它只会在父组件重新渲染时连带重渲染,这时候列表项的局部数据更新可能不如预期。
更合适的做法是让每个ProductItem也变成 observer,并且只读取自己需要的字段。比如这一项是否选中、当前数量是多少。这样当 selectedProducts 变化时,MobX 只通知到对应的列表项组件,其他组件不受影响。在鸿蒙侧,这能大幅减少桥接层的更新任务量。
另外要注意,在 observer 组件里不能用解构的方式读取 Store 数据。举例:
// 错误示范:解构之后,MobX 无法收集依赖 const { productList, selectedCategory } = store;这是因为解构出来的值在组件渲染时是一次性读取的,MobX 的依赖收集发生在读取那一刻,解构操作本身会截断“这个组件依赖了 store.productList”的关系。如果你用解构后的变量去渲染,MobX 可能认为组件并没有依赖原始字段,数据变化时就不会触发更新。
3.4 可观察性陷阱排查清单
结合鸿蒙适配经验,我整理了一个自查清单,照着检查可以避开大部分“数据变了UI不动”的问题:
- Store 字段是否通过 makeAutoObservable 或 observable 标记过。
- 修改数据的操作是否全部在 action 或 runInAction 里完成。
- 组件是否被 observer 包裹。
- 组件内是否直接读取了对应的 observable 字段。
- 是否使用了解构读取Store数据。
- 是否直接修改了 observable 对象内部的深层属性。
- 是否在异步回调里直接赋值。
这些点我在模拟项目X里踩遍了,后面排查异常时基本就是按这个顺序逐项核对。
4. 常见问题与实战排查记录
4.1 数据变了页面不动,问题出在哪?
这是群里被问得最多的问题,也是我在鸿蒙环境里第一次排查 MobX 问题时遇到的。现象是接口返回数据后,Store 里的 productList 已经更新了,console 里能看到新增数据,但列表 UI 没有任何变化。
排查思路分三步走。第一步,确认组件是不是 observer。我见过有人明明引入了 observer,但忘记包裹组件函数,导致组件只是普通函数组件,完全没有依赖收集。第二步,检查组件是否正确读取了 store 字段。如果组件内部把 store 传给了子组件,自己并没有读取任何 observable 字段,那它就不应该被 MobX 通知,这时候应该把 observer 放在真正读取数据的子组件上。第三步,检查数据更新是否经过 action。启用 enforceActions 后,异步回调里直接赋值会报错,但如果项目里没有启用 enforceActions,这种错误是静默的——数据换了,监听没触发,界面自然不动。
在鸿蒙侧,还有一个需注意的点:检查是否有多个 Store 实例。如果某个模块自己 new 了一个 Store,页面用的又是另一个 Store,两边互相不知道,也会出现数据不一致的表现。我当时就是因为初始化入口里 store 是单例,但组件里又动态创建了一次,排查了好几个小时才定位到。
4.2 监听不释放导致的内存泄漏
使用 MobX 的一大潜在风险是 observer 组件的监听没有被正确释放。在 React Native 里,当组件从界面树上移除时,mobx-react-lite 会自动清理对应的 Reaction,一般情况下不需要手动写 dispose。但在鸿蒙适配过程中,某些页面使用了原生生命周期或者自定义的导航容器,组件卸载时机可能跟 React 的预期不完全一致。
我在模拟项目X里遇到过一个情况:从一个商品页跳转到订单详情页,再返回时,商品页里的列表性能明显下降,拖拽卡顿。用工具检测后发现,之前的页面实例虽然视觉上已经不存在了,但组件对象并没有被卸载,它订阅的 observable 一直在被通知,每次数据变化它都在后台执行重渲染逻辑。
解决办法有两个层面。第一层,检查导航和页面容器是否有内存持有问题,确保页面组件在不可见时真的被卸载。第二层,在 Store 层设计时,针对那些“页面私有”的可观察数据,使用局部 Store 而不是全局单例,页面卸载时跟随组件一起销毁,从根上避免监听残留。
如果一定要用全局 Store 保存页面数据,可以加手动清理方法,在组件卸载的生命周期里调用:
useEffect(() => { return () => { store.clearPageData(); }; }, [store]);4.3 跨线程更新界面时的“隐性崩溃”
鸿蒙的 UI 线程和 JS 引擎线程是分开的,MobX 的数据变化发生在 JS 线程,界面刷新要切换到鸿蒙的 UI 线程执行。正常情况下,React Native 适配层会处理这个切换,但如果你在 setTimeout、原生回调或者某些异步构造器里修改了 observable,并且没有通过 React Native 的标准事件机制触发,偶尔会遇到 UI 线程和 JS 线程不同步的问题。
表现不是崩溃,而是界面更新延迟,或者某一个字段更新了、另一个字段没跟上。我的处理方式是利用 runInAction 把所有修改收敛到同一个同步代码块里,让 MobX 在同一个事件循环里完成依赖通知。另一个经验是避免在 render 期间直接修改 observable,这会造成 MobX 的“循环依赖”警告,在鸿蒙环境里更容易导致界面卡死。
4.4 问题排查速查表
我把高频问题整理成了一个速查表,方便各位按图索骥:
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 数据变了 UI 不动 | 组件没包 observer | 检查是否使用 observer 包裹组件 |
| 数据变了 UI 不动 | 解构了 store 字段 | 改为直接点读 store 字段 |
| 异步请求返回后不刷新 | 没走 action/runInAction | 检查接口回调里的赋值方式 |
| 界面更新闪烁或卡顿 | observable 粒度过大 | 缩小组件读取的数据范围 |
| 旧页面数据影响新页面 | Store 未清理 | 使用局部 Store 或手动清理 |
| 列表局部更新导致整页重绘 | 父组件 observer 范围过大 | 把 observer 下沉到列表项组件 |
| 修改数组索引不生效 | 直接改索引 | 用数组整体替换或使用 set 方法 |
| 新增属性没反应 | 对象属性一开始没有初始化 | 在声明 Store 时预先定义所有属性 |
4.5 数组操作的隐藏坑点
最后单独说一下可观察数组。MobX 对数组的监听是基于整体替换的,也就是说this.list.push(item)这种方式不会触发依赖更新,你需要这样做:
this.list = [...this.list, item];或者:
this.list = this.list.concat(item);我一开始在鸿蒙适配时,把 Android 上惯用的push写法搬了过来,结果列表死活不刷新。后来在 MobX 官方文档里确认了这一点——可观察数组的判断逻辑是引用是否变化,而不是内容变化。这类细节如果没人提醒,排查起来非常耗时间,希望各位不要重蹈覆辙。
5. 实测后的性能观察和优化建议
5.1 减少鸿蒙侧渲染压力的分流方法
MobX 本身非常快,它的依赖收集算法在 JS 层是毫秒级的,真正的性能瓶颈在通知之后的 UI 更新。在鸿蒙适配阶段,我建议所有 observer 组件的粒度尽量小。一个商品列表页,可以让“商品项组件”和“数量加减组件”分别成为 observer,而不是让整个页面成为 observer。这样操作数量时,只有对应商品项和角标会更新,其他区域的 View 不会重绘。
我在模拟项目X里做了个对比:整页 observer 时,连续点五次加号,页面会整体刷新五次;改为列表项 observer 后,同样五次操作,只有价格、角标和当前项改变了,其余区域完全不动。这个差异在鸿蒙模拟器上非常明显,从肉眼可感知的掉帧变得非常顺畅。
5.2 避免长列表下重复渲染
长列表场景一直是跨端渲染的考验。鸿蒙适配层对 FlatList/ListView 的优化程度相比 Android 还有差距,所以在使用 MobX 时,尤其需要关注列表项是否被多余的 observable 通知触发重渲染。
我的经验是把列表页的 Store 拆成两层。第一层是页面级 Store,保存列表数据和加载状态;第二层是单个列表项的数据结构,只包含自己和自身选中状态。页面级 Store 的 observable 不包含列表项业务字段,或者干脆用非可观察的普通数组保存列表项引用。这样即使某些全局状态变化,列表项也不会被批量通知。
5.3 批次更新的实际配置
MobX 在通知观察者时支持批量处理,但默认不开启。针对鸿蒙环境,我在入口配置里加了一行:
import { configure } from 'mobx'; configure({ enforceActions: 'observed', useProxies: 'always', recomputationHeuristics: { updateImmediately: true } });不过要说明的是,不同版本的 MobX 配置项名称会有差异,不要照抄。重点是理解思路:让 MobX 在一次事件循环里合并所有数据变更,统一触发一次通知,而不是每次赋值都立刻派发。这样在鸿蒙桥接层收到的更新指令数量会明显减少。
我个人实际操作中的体会是,MobX 的 Observable 数据监听在鸿蒙侧的难点,不在于库本身,而在于你对“可观察性”的直觉是否建立起来。拿到任何一段业务数据,先下意识判断它是不是可观察对象、更新它的时候有没有经过 action、读取它的组件是不是 observer、读取时有没有绕开解构。这套直觉建立之后,无论鸿蒙适配层怎么变,你都能快速定位问题。
最后再分享一个小技巧:在开发阶段,把 MobX 的日志输出打开,观察每次可观察数据变更的触发路径,能在早期发现很多隐藏的性能坑。我就是靠这个方法,在模拟项目X上线前把十几个页面里的无效通知全部清洗干净了。