鸿蒙 PC Markdown 编辑器滚动稳定性避免双向同步递归抖动双向同步滚动最大的风险不是目标位置稍有误差而是左右两侧互相触发。源码滚动设置预览scrollTop预览产生 scroll事件又设置源码两边高度和像素取整不同反馈可能持续表现为抖动、回弹和触控板失控。本文基于 OhMarkdown聚焦同步滚动的重入锁、动画帧释放、程序滚动与用户滚动竞争、模式和开关边界。代码位于 https://gitcode.com/VON-/codex_md_oh反馈环如何产生两个监听器editor.scrollDOM.addEventListener(scroll,()synchronizeScroll(editor.scrollDOM,preview),{passive:true});preview.addEventListener(scroll,()synchronizeScroll(preview,editor.scrollDOM),{passive:true});用户滚动 A回调写 B浏览器为 B派发事件回调再写 A。若转换完全可逆且浏览器不派发相同值事件可能很快停止但不能依赖。高度比例乘除、浮点和整数像素都会产生差值。一个布尔重入锁letsynchronizingScrollfalse;functionsynchronizeScroll(source:HTMLElement,target:HTMLElement):void{if(!syncScrollEnabled||currentMode!split||synchronizingScroll){return;}// 计算范围synchronizingScrolltrue;target.scrollToptargetRange*(source.scrollTop/sourceRange);window.requestAnimationFrame((){synchronizingScrollfalse;});}源事件进入时锁为 false写目标前设 true。目标产生的回调看到 true立即返回。锁不区分方向因为同一帧只允许一侧作为源。锁是 Web页面级状态不通过 Bridge。scroll可能每帧多次发生跨 ArkTS会增加延迟和乱序。为什么不能立即释放错误写法synchronizingScrolltrue;target.scrollTopnext;synchronizingScrollfalse;scroll事件可能在赋值后的任务或渲染阶段派发立即释放时目标回调已经看不到锁。requestAnimationFrame让锁保持到下一次绘制覆盖同一帧的程序滚动反馈。固定 setTimeout 100ms又太长会吞掉用户快速转到另一侧的真实操作。动画帧约束与视觉更新节奏一致是更小干预。为什么监听器是 passive{ passive: true }声明不会preventDefault。浏览器处理滚轮和触控板时无需等待 JavaScript决定是否阻止滚动。同步只观察结果并设置另一容器不需要取消源输入。若未来要建立主控侧并拦截某些手势不能随意改为非 passive应在 wheel和 scroll职责之间分层避免高频路径阻塞。比例计算的边界constsourceRangesource.scrollHeight-source.clientHeight;consttargetRangetarget.scrollHeight-target.clientHeight;if(sourceRange0||targetRange0){return;}target.scrollToptargetRange*(source.scrollTop/sourceRange);使用可滚动范围而非总高度底部才能对应底部。任一范围为零说明短内容无可滚动位置避免除零和 NaN。比例结果可能为小数浏览器内部取整或保存亚像素反向计算不一定回原值正是需要锁的原因。可额外夹紧0到1但正常 scrollTop已被浏览器约束。模式边界防止隐藏视图参与只有currentMode split同步。源码模式预览隐藏预览模式编辑器隐藏隐藏容器尺寸可能为零参与同步没有意义。模式切换后旧 scroll事件若到达也会被条件拒绝。大文档保护强制 source因此自动退出同步。同步乘法本身便宜昂贵的是大文档预览 DOM不能因为有同步功能而绕过保护。用户开关优先syncScrollEnabledfalse立即返回。ArkUI Toggle通过setSyncScroll设置setSyncScroll:(enabled){syncScrollEnabledenabled;}关闭后两侧独立程序不做“智能恢复”。用户可能要对照文档不同章节开关是持续偏好而非一次命令。重新开启时当前实现不立即对齐要等下一次滚动。这样不会突然跳动若产品希望即时对齐应明确以哪侧为源。用户快速换边的竞争帧锁可能忽略同一帧内用户在目标侧的第一个事件。通常小于一帧不可察觉但说明布尔锁不是完整输入仲裁。触控板惯性滚动还可能在用户开始另一侧时继续产生旧侧事件。更强方案记录activeSource与最近 wheel时间。wheel事件代表用户意图程序只同步另一侧一段静默后释放主控。也可根据鼠标所在容器选择源。引入前必须用真机证明当前锁不足否则复杂状态机会制造新卡顿。渲染重排与滚动Markdown输入会重建预览 DOMscrollHeight改变。若重排恰好发生在滚动帧比例基于新旧高度组合可能跳动。当前renderPreview同步执行后续 scroll读取完成后的布局值图片异步加载仍可能再改变高度。在线图片被禁用减少不确定重排。本地图片、字体和表格仍可能变化。可用 ResizeObserver在预览尺寸变化时记录并恢复比例但恢复写入同样要使用重入锁。程序跳转与同步的关系点击大纲调用 CodeMirrorscrollIntoView会产生编辑器 scroll事件。在分栏且同步开启时预览会跟随到相同比例这是合理默认。但标题跳转若未来直接滚动两侧就会与通用监听重复需要标记程序来源。搜索定位也会滚动源码并带动预览。用户可能希望预览跟随当前匹配。所有程序滚动复用相同抑制机制不能各自写一个局部布尔导致互相不知道。自动化验证关闭语义测试构造120个章节让两边都有滚动范围。把源码滚到底并派发 scroll等待一帧后断言预览大于零。随后关闭同步把预览设为0断言源码位置仍大于零。awaitpage.evaluate(()host.OhMarkdownEditor.setSyncScroll(false));awaitpage.locator(#preview).evaluate((element){element.scrollTop0;element.dispatchEvent(newEvent(scroll));});expect(awaitpage.locator(.cm-scroller).evaluate((element)element.scrollTop)).toBeGreaterThan(0);还应统计事件次数确保一次源滚动不会产生长反馈链用 Playwright暴露计数器或开发构建诊断钩子比肉眼判断抖动更稳定。鸿蒙 PC 触控板验证下图来自 MateBook Pro 2in1模拟器分栏和 Sync开关处于开启状态。真实验证需要分别滚动源码、预览快速换边并关闭开关。模拟器鼠标滚轮不能完全代表物理触控板惯性。最终应在鸿蒙 PC真机采集长文档滚动帧、事件次数和主观跟手感尤其测试高刷新率屏幕。语义同步不会消除递归将比例算法升级为标题锚点或源码行映射可以提高局部准确度但目标赋值仍会触发 scroll。无论算法多智能都需要来源抑制。语义映射还可能在锚点切换时产生不连续跳变更需要把用户源和程序目标区分。因此重入控制应与位置算法解耦。synchronizeScroll未来可替换目标计算锁和开关边界继续复用。性能诊断高频回调读取scrollHeight/clientHeight可能触发布局。若性能数据出现问题可在 ResizeObserver缓存范围在 requestAnimationFrame中合并多次源事件只应用最后位置。不能用长防抖因为滚动会明显落后。指标包括每秒 scroll回调、每帧目标写次数、长任务、掉帧、源目标位置误差。日志不涉及正文可在开发构建采集。当前边界布尔锁没有区分用户与程序源没有主动侧仲裁比例同步存在局部语义偏差图片重排未校正真机触控板压力尚未完成。当前自动化与模拟器已覆盖双向、关闭和基本稳定性。建议的压力语料同步滚动不能只测纯文本重复行。技术文档应混合短段落、长段落、一级到六级标题、表格、引用、任务列表、超宽代码块和不同高度本地图片。源码中的一行图片语法会在预览占据数百像素最容易暴露比例偏差长代码块则能观察独立内部横向滚动是否误触发外层同步。还要准备快速变化语料持续输入让预览高度增加在惯性滚动未停止时切换标签在窗口缩放过程中滚动从分栏切到源码再切回开启和关闭 Sync后立刻换边。这些组合比一次从顶部拖到底部更接近真实桌面使用。测试记录应包含源 scrollTop、源范围、目标 scrollTop、目标范围、锁状态和时间戳但不记录正文。通过事件序列可以判断是比例误差、布局变化还是反馈重入不必靠录屏逐帧猜测。无障碍与减少动态效果同步滚动是自动视图移动对部分用户可能造成不适。应用提供显式 Toggle是必要基础未来还可读取系统减少动态效果偏好默认关闭平滑滚动或同步动画。当前实现直接设置位置没有额外缓动响应明确且减少持续动画。键盘 PageUp/PageDown、Home/End和大纲跳转同样会触发同步。焦点始终留在用户操作侧目标侧只是视觉跟随不能因为程序滚动抢走焦点。读屏用户关闭同步后两侧应完全独立不产生隐藏区域位置变化导致意外朗读。与窗口自由缩放的组合鸿蒙 PC窗口缩放会改变两侧 clientHeight和布局断点。宽度低于760像素时 Web分栏变成上下布局两个可滚动范围会重新计算。锁只保护事件递归不缓存旧范围所以缩放后的下一次滚动使用新尺寸。如果缩放本身触发 scroll事件当前条件仍会同步。后续可在 ResizeObserver回调中短暂标记布局调整来源避免用户没有滚动时位置来回变化但不能长期锁住否则缩放结束后的第一个真实事件会被忽略。窗口测试至少覆盖断点两侧反复跨越而不是只在固定720宽启动一次。结语同步滚动稳定性的核心是一条反馈控制规则程序写目标时锁住反向事件并在下一动画帧释放。再配合 passive监听、可滚动范围、短内容检查、split模式和用户开关简单比例算法才能稳定运行。鸿蒙 PC编辑器可以以后改进语义对齐但不能牺牲停止响应和用户控制。先消除递归抖动再谈更聪明的同步是高频桌面交互的正确工程顺序。