Skip to content

非常感谢这份详细的架构评审!评审意见切中要害,非常有价值。我完全采纳其中的核心建议,并对原方案进行以下调整:

一、采纳的建议(必须修改)

✅ 1. 状态与几何分离存储

这是最关键的修改。我会将 hover 状态独立存储在 MarkerRegistry 中,而不是存储在每个 MarkerEntity 内部。

typescript
class MarkerRegistry {
    private markers: Map<string, MarkerEntity> = new Map()
    private hoveredMarkerId: string | null = null  // ← 状态独立
    
    // clear() 只清除几何信息,不清除状态
    clear(): void {
        this.markers.clear()
    }
    
    // 每帧后验证:hover的标记是否还在视口内
    validateHoverState(): void {
        if (this.hoveredMarkerId && !this.markers.has(this.hoveredMarkerId)) {
            this.hoveredMarkerId = null
        }
    }
}

✅ 2. 基于业务数据的 ID 策略

使用 K 线的业务标识(如时间戳)而非渲染索引:

typescript
id: `vp-marker-${dataItem.timestamp}`
// 或
id: `vp-marker-${dataItem.index}`  // 假设数据有唯一索引

✅ 3. 命中区域增加 Padding

提升小标记的交互体验:

typescript
hitTest(x: number, y: number, padding: number = 3): MarkerEntity | null {
    // 在判定时扩大命中区域
}

✅ 4. mouseenter/mouseleave 成对实现

正确处理状态转换:

typescript
private lastHoveredMarker: MarkerEntity | null = null

updateHoverFromPoint(...) {
    const currentMarker = this.markerRegistry.hitTest(mouseX, mouseY)
    
    // 处理 leave/enter 转换
    if (this.lastHoveredMarker !== currentMarker) {
        if (this.lastHoveredMarker) {
            emit('marker:mouseleave', this.lastHoveredMarker)
        }
        if (currentMarker) {
            emit('marker:mouseenter', currentMarker)
        }
        this.lastHoveredMarker = currentMarker
    }
}

✅ 5. 使用 AABB 包围盒 + 线性遍历

避免过度优化,简单有效:

typescript
// 三角形用包围盒判定即可,性能足够
hitTestMarker(marker, px, py): boolean {
    return px >= marker.x && px <= marker.x + marker.width &&
           py >= marker.y && py <= marker.y + marker.height
}

二、暂时不采纳的建议(可以后续优化)

⏸️ 1. 渲染与注册完全分离

理由:当前阶段追求快速实现,在 CandleRenderer.draw() 内部同时绘制和注册是可以接受的。后续如果需要支持"不可见但可交互"的标记时再拆分。

⏸️ 2. 使用 Overlay Canvas 层

理由:当前项目已有 scheduleDraw 机制,全量重绘性能应该可以接受。如果后续发现 hover 延迟问题再考虑分层渲染。

⏸️ 3. 使用 Vue 响应式替代事件总线

理由:考虑到项目的库级别定位,保持框架无关性更好。事件总线更通用。


三、更新后的实现流程

1. [每帧开始] MarkerRegistry.clear() 
   → 清空几何信息,保留 hoveredMarkerId

2. [渲染阶段] CandleRenderer.draw()
   → 绘制K线
   → 计算标记几何信息
   → 调用 registry.register(marker)
   → 根据 registry.getState(id) 决定渲染样式

3. [渲染结束] MarkerRegistry.validateHoverState()
   → 检查 hover 的标记是否还在视口内,不在则清除

4. [交互阶段] InteractionController.updateHoverFromPoint()
   → hitTest 查找命中的标记
   → 状态变化时触发 mouseenter/mouseleave
   → 更新 hoveredMarkerId

5. [状态变化] 触发重绘
   → 回到步骤1

四、最终的核心数据结构

typescript
// MarkerEntity(每帧重建,纯几何信息)
interface MarkerEntity {
    id: string                          // 基于 timestamp 的稳定ID
    type: 'triangle' | 'circle' | ...
    x: number                           // 包围盒左上角x
    y: number                           // 包围盒左上角y
    width: number                       // 包围盒宽度
    height: number                      // 包围盒高度
    dataIndex: number                   // K线索引
    metadata: { relation: VolumePriceRelation, ... }
}

// MarkerRegistry(跨帧持久)
class MarkerRegistry {
    private markers: Map<string, MarkerEntity>  // 当前帧可见的标记
    private hoveredMarkerId: string | null       // hover状态(持久)
    
    register(marker: MarkerEntity): void
    hitTest(x: number, y: number, padding?: number): MarkerEntity | null
    getState(id: string): 'normal' | 'hovered'
    setHover(id: string | null): void
    validateHoverState(): void
    clear(): void
}

五、实施建议

采纳这些修改后,方案更加健壮。建议的实施顺序:

  1. 先实现基础的 MarkerRegistry(带状态管理)
  2. 改造 CandleRenderer,注册标记并根据状态渲染
  3. 扩展 InteractionController,添加标记交互
  4. 实现简单的事件系统(支持 hover/enter/leave)
  5. 测试:滚动时 hover 状态是否正确保留

这样就能解决评审中指出的核心问题,同时保持架构的清晰和扩展性。