项目架构改进建议
一、核心差异对比
✅ 当前项目已实现的优秀设计
- 分层架构 - Chart → Pane → Renderer 职责清晰
- RAF 调度 -
scheduleDraw()已合并多次重绘请求 - 插件化渲染 - PaneRenderer 接口支持灵活扩展
- 独立 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、离屏渲染等。
三、优先级建议
高优先级(立即实施)
- 无效化级别系统 - 性能提升最明显
- 双层画布设计 - 交互流畅度显著提升
- TimeScale 类 - 代码质量提升,消除重复逻辑
中优先级(短期规划)
- 视口裁剪优化 - 大数据量时性能提升
- Model-View 分离 - 架构清晰度提升
低优先级(长期优化)
- 动画系统 - 用户体验优化
- CanvasRenderingTarget 2 D 抽象 - 扩展性提升
四、总结
你的项目已经具备了良好的基础架构(Chart-Pane-Renderer 分层、RAF 调度、插件化渲染),与 TradingView 的核心思路一致。
主要差距在于:
- 缺少精细的无效化控制 - 导致不必要的全量重绘
- 画布分层不够细致 - 静态/动态内容未分离
建议优先实现无效化级别系统和双层画布设计,这两个改动能带来最明显的性能提升,且改动成本相对较低。