From e3b1003f4d58bfc72a51162607b928c5622ad470 Mon Sep 17 00:00:00 2001 From: skycurtain Date: Tue, 8 Sep 2026 13:26:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(view):=20=E6=8B=96=E6=8B=BD=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E6=9D=A1=E7=99=BD=E5=B1=8F=E2=80=94=E2=80=94=E8=8A=82?= =?UTF-8?q?=E6=B5=81=E8=A1=A5=20trailing=EF=BC=8C=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E6=8C=81=E7=BB=AD=E8=B7=9F=E9=9A=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useThrottleFn 第三参 trailing 默认 false,拖拽滚动条的连续 scroll 事件(间隔恒 ≤33ms)在节流窗口内被全部丢弃不补执行,scrollTopPx 停在起拖位置导致白屏;开启 trailing 后窗口每 33ms 补执行一次跟随滚动。新增拖拽场景测试(连续 scroll + fake timers 断言窗口跟随最终位置),曾用 stash 对照验证旧行为下测试失败。 --- src/components/json-text-view.spec.ts | 36 +++++++++++++++++++++++++++ src/components/json-text-view.vue | 14 +++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/components/json-text-view.spec.ts b/src/components/json-text-view.spec.ts index 2595af0..a87ada7 100644 --- a/src/components/json-text-view.spec.ts +++ b/src/components/json-text-view.spec.ts @@ -78,4 +78,40 @@ describe('json-text-view 虚拟滚动', () => { ]); wrapper.unmount(); }); + + it('连续滚动事件(拖拽滚动条场景):节流 trailing 补执行,窗口跟随最终位置不白屏', async () => { + vi.useFakeTimers(); + try { + const text = makeJsonText(6000); // ~2.4 万行,scrollTop 任意值都有内容 + const wrapper = mount(JsonTextView, { + props: { json: JSON.parse(text), rawText: text }, + attachTo: document.body, + }); + const container = wrapper.find('.json-text-view'); + const scrollLines = () => wrapper.findAll('.line').map((line) => line.find('.gutter').text()); + + // 模拟拖拽滚动条:连续高频 scroll 事件,间隔均 ≤33ms(全部落在节流窗口内) + const dragSequence = [1000, 2000, 3000, 4000, 5000]; + for (const scrollTop of dragSequence) { + Object.defineProperty(container.element, 'scrollTop', { + value: scrollTop, + configurable: true, + }); + await container.trigger('scroll'); + await wrapper.vm.$nextTick(); + } + + // 若 trailing 未开启:窗口内容还停在首帧位置(白屏根因);开启后 33ms 内必有一次补执行 + vi.advanceTimersByTime(34); + await wrapper.vm.$nextTick(); + + // 行号 5000/20=250 起(5000px→第 250 行,含 overscan 从 245 起),证明窗口跟随到了最终位置 + const firstGutter = scrollLines()[0]!; + expect(Number(firstGutter)).toBeGreaterThanOrEqual(245); + expect(Number(firstGutter)).toBeLessThan(260); + wrapper.unmount(); + } finally { + vi.useRealTimers(); + } + }); }); diff --git a/src/components/json-text-view.vue b/src/components/json-text-view.vue index 62369c8..ec09637 100644 --- a/src/components/json-text-view.vue +++ b/src/components/json-text-view.vue @@ -42,11 +42,15 @@ const { height: containerHeightPx } = useElementSize(scrollContainerRef); const scrollTopPx = ref(0); -const handleScroll = useThrottleFn(() => { - if (scrollContainerRef.value) { - scrollTopPx.value = scrollContainerRef.value.scrollTop; - } -}, 33); // ~30fps 的窗口更新足够视觉流畅,重计算频率减半 +const handleScroll = useThrottleFn( + () => { + if (scrollContainerRef.value) { + scrollTopPx.value = scrollContainerRef.value.scrollTop; + } + }, + 33, + true, // trailing 必开:拖拽滚动条是连续事件(间隔恒 ≤33ms),默认 false 会把窗口内事件全部丢弃不补执行 → 白屏 +); // ~30fps 的窗口更新足够视觉流畅,重计算频率减半 /* ---------- 可视窗口(纯函数,阶段二已单测锁死) ---------- */