fix(chart): guard deferred pointer updates

This commit is contained in:
李启源
2026-07-21 09:18:46 +08:00
parent 671fde76f7
commit c63ea2855e
2 changed files with 73 additions and 30 deletions

View File

@@ -13,6 +13,7 @@ import {
} from 'd3'
import { resolveWaveformRenderingOptions } from '../core'
import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../utils'
import { useAnimationFrameThrottle } from './utils/useAnimationFrameThrottle'
import {
computed,
nextTick,
@@ -173,11 +174,10 @@ const annotationInteraction = useWaveformAnnotationInteraction()
const editorSeriesOptions = ref<AnnotationSeriesCandidate[]>([])
let generatedAnnotationId = 0
let synchronizingZoomTransform = false
let zoomAnimationFrame: number | null = null
let pendingSharedZoomTransform: ZoomTransform | null = null
const pendingIndependentZoomTransforms = new Map<number, ZoomTransform>()
let hoverAnimationFrame: number | null = null
let pendingHoverUpdate: (() => void) | null = null
const zoomThrottle = useAnimationFrameThrottle()
const hoverThrottle = useAnimationFrameThrottle()
const preparedSeries = usePreparedWaveformSeries(() => props.data, handleDataReferenceChange)
// 用于传递给 WaveformTooltip 的接口
@@ -608,27 +608,18 @@ function commitPendingZoom() {
}
function scheduleZoomCommit() {
if (zoomAnimationFrame !== null) return
zoomAnimationFrame = requestAnimationFrame(() => {
zoomAnimationFrame = null
commitPendingZoom()
})
zoomThrottle.schedule(() => commitPendingZoom())
}
function flushPendingZoom() {
if (zoomAnimationFrame !== null) {
cancelAnimationFrame(zoomAnimationFrame)
zoomAnimationFrame = null
}
zoomThrottle.flush()
commitPendingZoom()
}
function cancelPendingZoom() {
pendingSharedZoomTransform = null
pendingIndependentZoomTransforms.clear()
if (zoomAnimationFrame === null) return
cancelAnimationFrame(zoomAnimationFrame)
zoomAnimationFrame = null
zoomThrottle.cancel()
}
function clearZoomBindings() {
@@ -705,21 +696,11 @@ function configureZoom() {
}
function cancelPendingHover() {
pendingHoverUpdate = null
if (hoverAnimationFrame === null) return
cancelAnimationFrame(hoverAnimationFrame)
hoverAnimationFrame = null
hoverThrottle.cancel()
}
function scheduleHover(update: () => void) {
pendingHoverUpdate = update
if (hoverAnimationFrame !== null) return
hoverAnimationFrame = requestAnimationFrame(() => {
hoverAnimationFrame = null
const nextUpdate = pendingHoverUpdate
pendingHoverUpdate = null
nextUpdate?.()
})
hoverThrottle.schedule(update)
}
function hoveredPointsMatch(nextPoints: HoveredSeriesPoint[]): boolean {
@@ -1051,9 +1032,15 @@ function handleIndependentPointerMove(event: PointerEvent, trackIndex: number) {
const overlay = event.currentTarget as SVGRectElement | null
if (!overlay) return
const [pointerX, pointerY] = pointer(event, overlay)
scheduleHover(() => {
// 捕获轨道对象以避免竞态条件
const track = trackLayouts.value[trackIndex]
if (!track) return
if (!track || !track.hasVisibleSeries) return
scheduleHover(() => {
// 重新验证轨道仍然有效且有可见系列
const currentTrack = trackLayouts.value[trackIndex]
if (!currentTrack || !currentTrack.hasVisibleSeries || currentTrack !== track) return
const xValue = track.xScale.invert(Math.max(0, Math.min(innerWidth.value, pointerX)))
const nextPoints = track.seriesList.flatMap((series) => {
const point = nearestPoint(series, xValue)
@@ -1177,7 +1164,7 @@ watch(
)
if (
retainedIds.size !== internalHiddenSeriesIds.value.size ||
Array.from(retainedIds).some((seriesId) => !internalHiddenSeriesIds.value.has(seriesId))
Array.from(internalHiddenSeriesIds.value).some((seriesId) => !retainedIds.has(seriesId))
) {
internalHiddenSeriesIds.value = retainedIds
}

View File

@@ -0,0 +1,56 @@
/**
* 创建一个基于 requestAnimationFrame 的节流工具
* 用于合并多个快速连续的调用到单个动画帧中
*/
export function useAnimationFrameThrottle<T = void>() {
let frameHandle: number | null = null
let pendingCallback: (() => T) | null = null
/**
* 调度一个回调在下一个动画帧中执行
* 如果已经有待处理的帧,则替换待处理的回调
*/
function schedule(callback: () => T): void {
pendingCallback = callback
if (frameHandle !== null) return
frameHandle = requestAnimationFrame(() => {
frameHandle = null
const cb = pendingCallback
pendingCallback = null
cb?.()
})
}
/**
* 取消待处理的动画帧调度
*/
function cancel(): void {
pendingCallback = null
if (frameHandle === null) return
cancelAnimationFrame(frameHandle)
frameHandle = null
}
/**
* 立即执行待处理的回调(如果有)
*/
function flush(): void {
if (frameHandle !== null) {
cancelAnimationFrame(frameHandle)
frameHandle = null
}
const cb = pendingCallback
pendingCallback = null
cb?.()
}
/**
* 检查是否有待处理的调度
*/
function isPending(): boolean {
return frameHandle !== null
}
return { schedule, cancel, flush, isPending }
}