Skip to content

看到你的简历了,技术栈非常扎实!QuantKLineChart 作为前端可视化项目,很适合展示你的前端深度。我帮你系统梳理复习要点和可能的面试深挖点。

📚 项目核心知识点复习清单

一、Canvas 基础(必问)

1. Canvas vs SVG

- Canvas:位图,适合大量元素、频繁更新(图表、游戏)
- SVG:矢量图,适合少量元素、需要交互编辑(图标、流程图)

我选择 Canvas 的原因:
- K 线图可能渲染成百上千根蜡烛线,SVG DOM 节点过多会影响性能
- 需要频繁重绘(拖拽、缩放),Canvas 更适合
- 需要精确的像素级控制(DPR 适配)

2. Canvas 核心 API

javascript
// 关键 API(面试可能让你手写)
ctx.beginPath()          // 开始新路径
ctx.moveTo(x, y)         // 移动到起点
ctx.lineTo(x, y)         // 画线到终点
ctx.stroke()             // 描边
ctx.fillRect(x, y, w, h) // 填充矩形
ctx.fillText(text, x, y) // 绘制文字
ctx.save() / ctx.restore() // 保存/恢复状态(非常重要)

二、核心架构设计(高分点)

1. 三层 Canvas 分层渲染

javascript
// 为什么分层?
plotCanvas    // 主内容层:蜡烛线、MA线、十字线(变化最频繁)
yAxisCanvas   // 右侧价格轴(变化中等)
xAxisCanvas   // 底部时间轴(变化最少)

// 优势:
// - 避免全屏重绘,提升性能
// - 比如:拖拽时只需要重绘 plotCanvas

2. 坐标系统转换(必考点)

你需要掌握三种坐标系统:

typescript
// 1. 逻辑像素:CSS 像素,开发时使用
kWidth = 10  // 逻辑像素

// 2. 物理像素:设备实际像素
dpr = window.devicePixelRatio  // 通常是 1、2、3
kWidthPx = 10 * dpr

// 3. 屏幕坐标:鼠标事件的 clientX/Y
// 需要转换为 Canvas 内部坐标

// 转换公式:
canvasX = mouseX - rect.left
canvasY = mouseY - rect.top

面试可能问:

"为什么需要适配 DPR?"

答:如果不适配,在高分屏(如 iPhone Retina)上,Canvas 会模糊。需要:

  1. canvas. width 设置为宽度 * dpr
  2. canvas. height 设置为高度 * dpr
  3. ctx.scale (dpr, dpr) 缩放上下文
  4. 之后所有绘制都使用逻辑像素

三、性能优化核心(深入)

1. RAF(RequestAnimationFrame)节流

typescript
// 问题:为什么不用防抖/节流函数?
// 答:RAF 是浏览器原生 API,与屏幕刷新率同步(通常 60fps)

// 实现(你在 code 里的实际实现)
private raf: number | null = null

scheduleDraw() {
    if (this.raf != null) cancelAnimationFrame(this.raf)
    this.raf = requestAnimationFrame(() => {
        this.raf = null
        this.draw()
    })
}

// 面试可能问:
// - RAF vs setTimeout/setInterval 的区别?
// 答:RAF 与渲染周期同步,更省电、不丢帧

2. 视口裁剪(只渲染可见区域)

typescript
// 计算可见的 K 线索引范围
const { start, end } = getVisibleRange(
    scrollLeft,
    plotWidth,
    kWidth,
    kGap,
    data.length
)

// 只渲染 [start, end] 范围内的 K 线
// 优势:数据量很大时(如 10000 根 K 线),只渲染可见的 50 根

面试可能问:

"如何进一步优化大数据量场景?"

答:

  1. 虚拟滚动:只加载和渲染可见区域数据
  2. 数据分片:按时间分片加载
  3. Web Worker:将数据处理放到 Worker 线程
  4. 离屏 Canvas:将静态元素绘制到离屏 Canvas 缓存

四、交互系统(难点)

1. 十字线吸附算法

typescript
// 关键:如何让十字线"吸附"到 K 线中心?

// 你的实现:
const unit = kWidth + kGap
const idx = Math.floor((scrollLeft + mouseX) / unit)

// 计算这根 K 线的中心位置
const centerX = idx * unit + kWidth / 2

// 十字线的 x 坐标吸附到 centerX

2. 命中检测(Hit Test)

typescript
// 判断鼠标是否点击了 K 线实体或影线

// 实体判定:
const inBody = 
    mouseX >= kLineStartX && 
    mouseX <= kLineStartX + kWidth &&
    mouseY >= Math.min(openY, closeY) &&
    mouseY <= Math.max(openY, closeY)

// 影线判定:
const inWick =
    Math.abs(mouseX - centerX) <= tolerance &&
    mouseY >= lowY &&
    mouseY <= highY

五、Vue 层设计(结合框架)

1. 组件职责划分

vue
<!-- KLineChart.vue:只负责 -->
- DOM 管理(refs)
- 事件转发(鼠标/滚轮 → chart.interaction)
- Tooltip 渲染(DOM)

<!-- 为什么不把 Canvas 绘制放在 Vue 里?-->
答:Vue 的响应式系统不适合高频更新(每秒 60 次),Canvas 绘制应该在纯 TS 层

2. 事件委托 vs 事件绑定

typescript
// 你的实现:在 container 上监听事件
container.addEventListener('wheel', onWheel)
container.addEventListener('mousemove', onMouseMove)

// 优势:
// 1. 事件委托,减少事件监听器数量
// 2. 方便统一处理

🎯 面试官可能深挖的问题

深挖点 1:像素对齐的数学原理

问题:

"你说做了像素对齐,具体怎么做的?为什么 K 线宽度要奇数化?"

参考回答:

typescript
// 问题背景:
// 如果 kWidth = 10(偶数),中心在 5.5
// 影线宽度 = 1,中心应该在 5.5
// 但在 Canvas 中,像素是离散的,0.5 会模糊

// 解决方案:
function calcKWidthPx(kWidth: number, dpr: number): number {
    let kWidthPx = Math.round(kWidth * dpr)
    if (kWidthPx % 2 === 0) {
        kWidthPx += 1  // 奇数化
    }
    return kWidthPx
}

// 奇数化的好处:
// kWidthPx = 21(奇数),中心在 10(整数像素)
// 影线宽度 = 1,中心也在 10,完美对齐

深挖点 2:定点缩放算法

问题:

"如何实现以鼠标位置为中心的缩放?"

参考回答:

typescript
// 核心思路:保持鼠标指向的 K 线索引不变

zoomAt(mouseX, scrollLeft, deltaY) {
    // 1. 记录缩放前,鼠标指向的 K 线索引
    const oldUnit = kWidth + kGap
    const centerIndex = (scrollLeft + mouseX) / oldUnit
    
    // 2. 调整 kWidth(根据滚轮方向)
    const newKWidth = adjustKWidth(deltaY)
    
    // 3. 计算新的 scrollLeft,让同一根 K 线仍在鼠标位置
    const newUnit = newKWidth + kGap
    const newScrollLeft = centerIndex * newUnit - mouseX
    
    // 4. 应用新的滚动位置
    container.scrollLeft = newScrollLeft
}

深挖点 3:插件的渲染器设计

问题:

"如何设计一个可扩展的渲染器系统?"

参考回答:

typescript
// 定义统一接口
interface PaneRenderer {
    draw(args: {
        ctx: CanvasRenderingContext2D
        pane: Pane
        data: KLineData[]
        range: { start: number; end: number }
        scrollLeft: number
        kWidth: number
        kGap: number
        dpr: number
    }): void
}

// 使用策略模式
class CandleRenderer implements PaneRenderer { /* ... */ }
class MARenderer implements PaneRenderer { /* ... */ }
class GridRenderer implements PaneRenderer { /* ... */ }

// 在 Pane 中维护渲染器链
class Pane {
    renderers: PaneRenderer[] = []
    
    draw(args) {
        this.renderers.forEach(r => r.draw(args))
    }
}

🔗 关联前端基础知识

1. 响应式原理

javascript
// Vue 3 的响应式 vs Canvas 的状态管理
// Vue:Proxy 自动追踪依赖,适合 UI 层
// Canvas:手动调用 draw(),适合高频渲染

// 你的实现:Vue 只负责 DOM 层,状态在 InteractionController 中

2. 事件循环

javascript
// RAF 在事件循环中的位置
// 1. 用户输入事件(wheel, mousemove)
// 2. 调用 scheduleDraw()
// 3. RAF 回调在重绘前执行
// 4. 浏览器更新 Canvas

// 优势:确保绘制在正确的时机

3. 性能监控

javascript
// 如何监控 FPS?
let lastTime = performance.now()
let frames = 0

function measureFPS() {
    const now = performance.now()
    frames++
    
    if (now - lastTime >= 1000) {
        console.log(`FPS: ${frames}`)
        frames = 0
        lastTime = now
    }
    
    requestAnimationFrame(measureFPS)
}

💡 面试策略建议

1. 项目介绍话术(2-3 分钟)

"我基于 Canvas 开发了一个 K 线图组件库。
核心亮点:
1. 分层架构:Chart-Pane-Renderer,核心逻辑与 Vue 解耦
2. 性能优化:三层 Canvas 分层渲染 + RAF 节流 + 视口裁剪
3. 坐标系统:处理了 DPR 适配、像素对齐、世界/屏幕坐标转换
4. 交互系统:实现了定点缩放、十字线吸附、精确命中检测

相比 ECharts,我自研的原因是:
- 需要深度定制量化信号标注功能
- 学习 Canvas 高性能渲染和坐标系统
- 为量化投研智能体提供可视化组件"

2. 遇到不会的问题

"这个问题我目前还没有深入实践,但我会从这几个角度考虑:
1. 先确认场景和需求
2. 参考成熟方案(如 ECharts 源码)
3. 设计 prototype 验证思路
4. 持续优化和迭代"

3. 展示学习能力

"在开发这个项目时,我系统学习了:
- Canvas API 和图形学基础
- 前端性能优化策略(RAF、视口裁剪、缓存)
- 坐标系统和数学计算
- 设计模式的应用(策略模式、适配器模式)"

需要我针对某个具体技术点(如像素对齐、定点缩放)准备更详细的代码讲解吗?