非常感谢这份详细的架构评审!评审意见切中要害,非常有价值。我完全采纳其中的核心建议,并对原方案进行以下调整:
一、采纳的建议(必须修改)
✅ 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
}五、实施建议
采纳这些修改后,方案更加健壮。建议的实施顺序:
- 先实现基础的
MarkerRegistry(带状态管理) - 改造
CandleRenderer,注册标记并根据状态渲染 - 扩展
InteractionController,添加标记交互 - 实现简单的事件系统(支持 hover/enter/leave)
- 测试:滚动时 hover 状态是否正确保留
这样就能解决评审中指出的核心问题,同时保持架构的清晰和扩展性。