Skip to content

项目架构改进建议

一、核心差异对比

✅ 当前项目已实现的优秀设计

  1. 分层架构 - Chart → Pane → Renderer 职责清晰
  2. RAF 调度 - scheduleDraw() 已合并多次重绘请求
  3. 插件化渲染 - PaneRenderer 接口支持灵活扩展
  4. 独立 Pane 管理 - 每个 Pane 有独立的 yAxis 和布局

❌ 与 TradingView 相比的主要缺失

二、关键改进建议

1. 无效化级别系统(最重要)

当前问题:

typescript
// 现在每次都全量重绘
scheduleDraw() → draw() → 清空所有 canvas → 重新绘制所有内容

改进方案: 引入 InvalidationLevel 枚举

typescript
enum InvalidationLevel {
  None = 0,      // 无需渲染
  Cursor = 1,    // 仅十字光标
  Light = 2,     // 数据变化(K线、标签)
  Full = 3       // 完全重绘(布局、尺寸变化)
}

class Chart {
  private invalidationLevel = InvalidationLevel.None
  
  cursorUpdate() {
    this.invalidationLevel = Math.max(this.invalidationLevel, InvalidationLevel.Cursor)
    this.scheduleDraw()
  }
  
  lightUpdate() {
    this.invalidationLevel = Math.max(this.invalidationLevel, InvalidationLevel.Light)
    this.scheduleDraw()
  }
  
  fullUpdate() {
    this.invalidationLevel = InvalidationLevel.Full
    this.scheduleDraw()
  }
  
  draw() {
    if (invalidationLevel >= InvalidationLevel.Full) {
      // 清空画布、重绘所有内容
    } else if (invalidationLevel >= InvalidationLevel.Light) {
      // 只重绘 K线、标签
    } else if (invalidationLevel >= InvalidationLevel.Cursor) {
      // 只重绘十字光标
    }
    this.invalidationLevel = InvalidationLevel.None
  }
}

性能提升: 交互时(鼠标移动)只需重绘顶层,性能提升 5-10 倍。


2. 双层画布设计

当前问题:

typescript
// 3 层 canvas 都是单层,每次重绘要清空全部内容
plotCanvas: HTMLCanvasElement      // K线 + 十字线混在一起
yAxisCanvas: HTMLCanvasElement     // 价格轴 + 价格标签
xAxisCanvas: HTMLCanvasElement     // 时间轴 + 时间标签

改进方案: 为每个 Pane 添加顶层画布

typescript
class PaneRenderer {
  private dom: {
    plotCanvas: HTMLCanvasElement      // 主画布:K线、网格、MA线
    topCanvas: HTMLCanvasElement       // 顶层:十字线、悬浮标签
    yAxisCanvas: HTMLCanvasElement     // Y轴
  }
  
  draw(args: DrawArgs, level: InvalidationLevel) {
    // Cursor 级别:只重绘 topCanvas
    if (level === InvalidationLevel.Cursor) {
      this.drawTopLayer(args)
      return
    }
    
    // Light/Full 级别:重绘 plotCanvas
    this.drawPlotLayer(args)
  }
}

性能提升: 交互时(拖拽、缩放)顶层画布与主画布分离,减少 50%+ 绘制调用。


3. 时间轴缩放器

当前问题:

typescript
// 时间轴坐标计算散落在多处
const unit = opt.kWidth + opt.kGap
const idx = Math.floor(offset / unit)
const centerWorldX = opt.kGap + idx * unit + opt.kWidth / 2

改进方案: 抽取 TimeScale 类(类似 PriceScale)

typescript
class TimeScale {
  private kWidth: number
  private kGap: number
  
  constructor(kWidth: number, kGap: number) {
    this.kWidth = kWidth
    this.kGap = kGap
  }
  
  // 索引 → 屏幕坐标
  indexToX(index: number, scrollLeft: number): number {
    return this.kGap + index * (this.kWidth + this.kGap) - scrollLeft
  }
  
  // 屏幕坐标 → 索引
  xToIndex(x: number, scrollLeft: number): number {
    return Math.floor((x + scrollLeft - this.kGap) / (this.kWidth + this.kGap))
  }
  
  // 获取 K 线中心点
  indexToCenterX(index: number, scrollLeft: number): number {
    return this.indexToX(index, scrollLeft) + this.kWidth / 2
  }
}

好处: 坐标转换逻辑统一,便于后续优化(如支持不等宽时间轴)。


4. Model-View 分离

当前问题:

typescript
// Pane 既包含数据模型,又直接调用 renderer
class Pane {
  priceRange: PriceRange
  yAxis: PriceScale
  renderers: PaneRenderer[]  // 直接持有渲染器
}

// Renderer 需要知道 Pane、data、range 等过多参数
renderer.draw({ ctx, pane, data, range, scrollLeft, kWidth, kGap, dpr })

改进方案: 引入 PaneView 抽象层

typescript
interface IPaneView {
  renderer(): IPaneRenderer
}

class Pane {
  // 只保留数据和布局
  priceRange: PriceRange
  yAxis: PriceScale
  
  // 提供视图对象
  paneViews(): IPaneView[] { ... }
}

// Renderer 参数简化
interface IPaneRenderer {
  draw(target: CanvasRenderingContext2D, isHovered: boolean): void
}

// Chart 中绘制
paneViews.forEach(view => {
  view.renderer().draw(ctx, isHovered)
})

好处: 渲染器无需知道具体数据结构,职责更清晰。


5. 视口裁剪优化

当前问题:

typescript
// 绘制 K 线时遍历所有可见数据
for (let i = range.start; i <= range.end; i++) {
  const k = data[i]
  // 绘制每根 K 线
}

改进方案: 提前计算屏幕边界

typescript
// TimeScale 新增方法
class TimeScale {
  getVisibleIndices(scrollLeft: number, viewWidth: number): { start: number; end: number } {
    const start = Math.floor(scrollLeft / this.unit)
    const end = Math.ceil((scrollLeft + viewWidth) / this.unit)
    return { start, end }
  }
}

// Renderer 中使用
const visible = timeScale.getVisibleIndices(scrollLeft, plotWidth)
for (let i = visible.start; i <= visible.end; i++) {
  // 确保不绘制屏幕外的 K 线
  const x = timeScale.indexToX(i, scrollLeft)
  if (x < -this.kWidth || x > plotWidth) continue
  // 绘制 K 线
}

6. 动画系统

当前问题:

typescript
// 滚动和缩放是即时完成的,缺少平滑过渡
zoomAt(mouseX, scrollLeft, deltaY) {
  // 直接设置新的 kWidth
  this.opt.kWidth = newKWidth
  this.scheduleDraw()
}

改进方案: 引入 KineticAnimation

typescript
class KineticAnimation {
  private velocity: number = 0
  private lastTime: number = 0
  
  start(initialVelocity: number) {
    this.velocity = initialVelocity
    this.lastTime = performance.now()
    this.animate()
  }
  
  private animate() {
    const now = performance.now()
    const dt = now - this.lastTime
    this.velocity *= 0.95  // 摩擦力
    if (Math.abs(this.velocity) < 0.1) return
    
    chart.setScrollLeft(chart.getScrollLeft() + this.velocity)
    requestAnimationFrame(() => this.animate())
  }
}

// InteractionController 中使用
onPointerUp(e: PointerEvent) {
  if (this.isDragging) {
    const velocity = this.dragVelocity
    this.animation.start(velocity)
  }
}

7. CanvasRenderingTarget 2D 抽象

当前问题:

typescript
// 渲染器直接操作 CanvasContext2D,难以测试和扩展
renderer.draw({ ctx, pane, data, range, ... })

改进方案: 引入渲染目标抽象

typescript
interface ICoordinateSpace {
  bitmapSize: { width: number; height: number }
  context: CanvasRenderingContext2D
}

interface IRenderingTarget {
  useBitmapCoordinateSpace(callback: (space: ICoordinateSpace) => void): void
  useMediaCoordinateSpace(callback: (space: ICoordinateSpace) => void): void
}

// Renderer 使用
renderer.draw(target: IRenderingTarget, isHovered: boolean) {
  target.useBitmapCoordinateSpace(({ context, bitmapSize }) => {
    context.clearRect(0, 0, bitmapSize.width, bitmapSize.height)
    // 绘制内容
  })
}

好处: 便于未来支持 WebGL、离屏渲染等。


三、优先级建议

高优先级(立即实施)

  1. 无效化级别系统 - 性能提升最明显
  2. 双层画布设计 - 交互流畅度显著提升
  3. TimeScale 类 - 代码质量提升,消除重复逻辑

中优先级(短期规划)

  1. 视口裁剪优化 - 大数据量时性能提升
  2. Model-View 分离 - 架构清晰度提升

低优先级(长期优化)

  1. 动画系统 - 用户体验优化
  2. CanvasRenderingTarget 2 D 抽象 - 扩展性提升

四、总结

你的项目已经具备了良好的基础架构(Chart-Pane-Renderer 分层、RAF 调度、插件化渲染),与 TradingView 的核心思路一致。

主要差距在于:

  1. 缺少精细的无效化控制 - 导致不必要的全量重绘
  2. 画布分层不够细致 - 静态/动态内容未分离

建议优先实现无效化级别系统和双层画布设计,这两个改动能带来最明显的性能提升,且改动成本相对较低。