ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 7 ArkUI:平行视界跨栏回写来源标签与环路抑制

HarmonyOS 7 ArkUI:平行视界跨栏回写来源标签与环路抑制 一、三次相同的选择变更暴露的不是 List 性能问题TwinSelect 是一个素材挑选 Demo左栏是 240 条商品素材右栏显示已选集合和大图预览。需求看起来很普通——宽屏以 ArkUINavigationMode.Split呈现窄屏退化为单栏左边勾选右边立即更新右边移除左边同步取消。真正把问题暴露出来的是批量选择我在左栏一次勾了 18 条HiLog 却连续收到三次同内容事件右栏又把“应用外部状态”当成用户操作回写。最终没有死循环但 revision 从 39 快速涨到 47埋点重复、动画重启偶尔还会把刚取消的第 18 条重新选中。这不是 List 刷新慢也不是State不可靠。根因是两个栏位都同时扮演“状态拥有者”和“事件生产者”而同步消息里只有selectedIds没有来源、基线版本和事件身份。只要视图重建、订阅重连或右栏做一次排序内容相同的数组也能被当成新意图。我把这轮修复编号定为PV-0712。验收场景固定为宽度1136vp、Split 模式、240 条素材、最终选中 18 条。目标不是让 revision 不增长而是让一次用户意图只产生一个可追踪提交最终revision42、事件evt_0042、来源catalog-pane三条回声全部丢弃回滚次数为 0快照摘要为d13e7a9c。二、先收回两边各自维护真相的权力项目没有引入复杂状态框架目录也很克制pages/BatchSelectionPage.ets负责布局components/CatalogPane.ets和SelectionPane.ets只产生用户意图model/SelectionCoordinator.ets保存唯一快照model/SelectionEnvelope.ets定义同步协议。页面可以有两套渲染但只能有一份已提交选择集合。第一段代码解决“相同内容为什么被重复提交”。同步载荷增加origin、单调递增 revision、事件 ID 和稳定 fingerprintCoordinator 先判事件身份再判基线版本最后才提交新快照。// model/SelectionCoordinator.etsexportinterfaceSelectionEnvelope{eventId:stringorigin:catalog-pane|selection-panebaseRevision:numberselectedIds:string[]fingerprint:string}ObservedV2exportclassSelectionCoordinator{TraceselectedIds:string[][]Tracerevision:number39TraceduplicateDrops:number0privateseen:SetstringnewSet()apply(envelope:SelectionEnvelope):boolean{if(this.seen.has(envelope.eventId)||envelope.fingerprintthis.fingerprint(this.selectedIds)){this.duplicateDropsreturnfalse}if(envelope.baseRevision!this.revision){thrownewError(STALE_BASE:${envelope.baseRevision}-${this.revision})}this.seen.add(envelope.eventId)this.selectedIds[...envelope.selectedIds].sort()this.revisionhilog.info(0x1200,TwinSelect,commit${envelope.eventId}rev${this.revision}origin${envelope.origin})returntrue}privatefingerprint(ids:string[]):string{returnids.slice().sort().join(|)}}这里故意没有用数组引用判断。跨栏传输、持久化恢复和组件重建都会创建新数组引用不同不代表业务内容不同。baseRevision也不能省它把“右栏基于 39 号快照删除一条但左栏已经提交到 41”变成明确的冲突而不是静默覆盖。Demo 中fingerprint用排序字符串便于读懂产品工程应换成稳定哈希并给seen设置容量或时间窗否则长会话会把去重集合变成内存泄漏点。apply()只在 Coordinator 内改变 revision。异常分支不修改任何可观察字段因此 UI 不会先闪成错误状态再回退。重复调用是允许的但幂等真正的陈旧基线则抛出错误让上层刷新快照后重新生成意图不能拿旧 envelope 原样重试。三、外部状态落地时组件必须短暂失去“发言权”第二段代码处理最隐蔽的回写环路。右栏收到快照后要更新 Checkbox 与计数但这次变化不是用户点击不应该再次发布。简单的布尔锁容易在异步动画中提前释放所以我用一次性的applyEpoch标记本轮外部落地并把用户事件与渲染赋值拆开。// components/SelectionPane.etsComponentV2exportstruct SelectionPane{Paramcoordinator:SelectionCoordinatornewSelectionCoordinator()LocalprivaterenderedIds:string[][]privateapplyEpoch:number0Monitor(coordinator.revision)onSnapshotChanged():void{constepochthis.coordinator.revisionif(epochthis.applyEpoch)returnthis.applyEpochepochthis.renderedIds[...this.coordinator.selectedIds]}privateremoveByUser(id:string):void{constnextthis.renderedIds.filter(itemitem!id)constenvelopeSelectionEnvelopeFactory.create(selection-pane,this.coordinator.revision,next)this.coordinator.apply(envelope)}build(){List(){ForEach(this.renderedIds,(id:string){ListItem(){Row(){Text(id).layoutWeight(1)Button(移除).onClick(()this.removeByUser(id))}}},(id:string)id)}}}关键点不是Monitor本身而是它只负责“投影快照”绝不调用apply()。所有写入路径必须带明确的用户动作入口。ForEach的 key 直接使用素材 ID避免数组整体替换后把 18 个 ListItem 全部当成新节点。页面退出时组件随树销毁没有额外监听器如果 Coordinator 接到 EventHub 或分布式订阅则必须在aboutToDisappear成对注销不能仅依赖 JS 对象回收。另一个边界是快速连点。removeByUser()读取当前 revision 创建 envelope连续点击可能让第二个 envelope 基线过期。项目策略不是吞掉第二次操作而是捕获STALE_BASE从最新快照重新计算next只重建一次。最多重试一次仍冲突就提示“选择已在另一栏更新”避免无限重试制造新的环路。四、Split 只决定摆放方式不能决定状态寿命第三段代码把布局变化和状态提交分开。宽度切到396vp时页面从 Split 回落到 Stack但 Coordinator 保持 42 号快照再次展开也不能重放 0042 事件。布局 epoch 只用于调试不参与业务 revision。// pages/BatchSelectionPage.etsEntryComponentV2struct BatchSelectionPage{Localcoordinator:SelectionCoordinatornewSelectionCoordinator()Localmode:NavigationModeNavigationMode.SplitLocalwidthVp:number1136LocallayoutEpoch:number7privateonAreaChanged(width:number):void{constnextwidth840?NavigationMode.Split:NavigationMode.Stackif(nextthis.mode)returnthis.widthVpwidththis.modenextthis.layoutEpochhilog.info(0x1200,TwinSelect,layout epoch${this.layoutEpoch}mode${this.mode}width${width}vp)}build(){Navigation(){CatalogPane({coordinator:this.coordinator})}.mode(this.mode).navDestination({builder:(){SelectionPane({coordinator:this.coordinator})}}).onAreaChange((_,area)this.onAreaChanged(Number(area.width)))}}我把阈值判断做成单一函数并在模式未变化时立即返回。真实项目还应加入 24vp 左右的迟滞带防止自由窗口停在阈值附近时 Split/Stack 抖动。layoutEpoch与 revision 分离尤其重要布局可以反复变化业务选择不应随之“伪更新”。调试时我只盯四类日志用户意图、Coordinator 提交、重复丢弃、布局变化。修复前一次批量选择出现 1 次 commit 加 3 次 echo修复后仍能观察到三条到达但被 fingerprint/eventId 门禁丢弃。这样能证明问题被控制而不是日志被删掉。图中的工程、代码和运行态都对应同一验收BatchSelectionPage.ets在中间编辑右侧模拟器为 1136vp Split 布局底部日志显示evt_0042从catalog-pane提交到 revision 42随后echo drop3摘要d13e7a9c。这组数据也被写进自动化测试避免截图好看而协议已经变了。五、窄屏回落不是另一个 Demo而是同一状态机的另一投影把窗口收窄到396×824vp后页面变成单栏。选择结果仍是 18/240revision 仍为 42任务仍是PV-0712变化的只有modeSTACK和layoutEpoch8。这一步抓到了旧实现的第二个问题页面曾在 Stack 模式初始化时用本地缓存覆盖 Coordinator导致 revision 倒退。现在初始化只读快照不再写回。手机态页面特意保留“同步诊断”卡片而不是只展示商品列表。它让测试人员能直接确认来源标签、事件 ID、重复丢弃数和摘要。真正上线时这些字段可以受 debug 开关控制但日志与状态机必须继续存在否则跨栏偶发问题会重新变成“用户说丢了一个勾选”。六、最终保留的工程边界这次改动没有追求一个万能 Store而是明确了四条边界。第一栏位只产生意图Coordinator 才能提交真相第二外部快照落地与用户动作是两条不同路径第三业务 revision 与布局 epoch 永不混用第四去重是有界资源订阅与缓存都要在生命周期结束时释放。验收结果是宽屏1136vp / SPLIT与窄屏396×824vp / STACK之间切换 20 次18 条选择无丢失evt_0042只提交一次3 条回声全部丢弃回滚 0最终摘要始终为d13e7a9c。更重要的是日志能解释每一次状态变化而不是仅仅“现在不复现了”。参考资料华为 ArkUI Navigation 分栏开发文档https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-navigation-split-mode。
返回列表