返回笔记首页

数据实时更新方案 + 空间数据处理

主题配置

一、数据实时更新方案优化

1.1 业务场景

javascript
const 实时更新需求 = {
  场景: '配送系统实时监控',

  数据特点: {
    更新频率: '每秒推送100-500条数据',
    数据类型: ['配送员位置', '订单状态', '围栏告警', '轨迹数据'],
    峰值并发: '高峰期1000条/秒',
    实时性要求: '延迟<1秒'
  },

  技术挑战: [
    '高频数据推送导致渲染卡顿',
    '大量DOM更新性能瓶颈',
    '数据合并与去重逻辑复杂',
    '断线重连数据一致性'
  ]
}

1.2 完整实现代码

vue
<!-- RealtimeDataUpdate.vue - 实时数据更新系统 -->
<template>
  <div class="realtime-data-update">
    <div ref="mapContainer" class="map-container"></div>

    <!-- 数据监控面板 -->
    <div class="monitor-panel">
      <h3>实时数据监控</h3>

      <div class="stat-grid">
        <div class="stat-card">
          <div class="stat-label">在线配送员</div>
          <div class="stat-value">{{ onlineCouriers }}</div>
        </div>

        <div class="stat-card">
          <div class="stat-label">推送速率</div>
          <div class="stat-value">{{ pushRate }}/s</div>
        </div>

        <div class="stat-card">
          <div class="stat-label">渲染FPS</div>
          <div class="stat-value">{{ renderFPS }}</div>
        </div>

        <div class="stat-card">
          <div class="stat-label">延迟</div>
          <div class="stat-value">{{ latency }}ms</div>
        </div>
      </div>

      <div class="update-log">
        <h4>更新日志</h4>
        <div class="log-list">
          <div v-for="(log, index) in updateLogs" :key="index" class="log-item">
            <span class="log-time">{{ log.time }}</span>
            <span class="log-content">{{ log.content }}</span>
          </div>
        </div>
      </div>

      <div class="control-buttons">
        <button @click="toggleConnection" :class="['btn', isConnected ? 'btn-danger' : 'btn-success']">
          {{ isConnected ? '断开连接' : '建立连接' }}
        </button>

        <button @click="simulateHighLoad" class="btn btn-warning">
          模拟高负载
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'

const mapContainer = ref(null)
const map = ref(null)
const isConnected = ref(false)
const onlineCouriers = ref(0)
const pushRate = ref(0)
const renderFPS = ref(60)
const latency = ref(0)
const updateLogs = ref([])

// WebSocket连接
let ws = null
let reconnectTimer = null
let reconnectAttempts = 0
const MAX_RECONNECT_ATTEMPTS = 5

// 数据缓存
const courierDataCache = new Map()
const updateQueue = []

// 性能监控
let frameCount = 0
let lastFrameTime = performance.now()
let pushCount = 0
let lastPushTime = Date.now()

// 批量更新定时器
let batchUpdateTimer = null
const BATCH_UPDATE_INTERVAL = 100 // 100ms批量更新一次

onMounted(() => {
  initMap()
  startPerformanceMonitor()
})

onBeforeUnmount(() => {
  disconnect()
  if (batchUpdateTimer) clearInterval(batchUpdateTimer)
})

// 初始化地图
const initMap = () => {
  mapboxgl.accessToken = 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGV4YW1wbGUifQ.example'

  map.value = new mapboxgl.Map({
    container: mapContainer.value,
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [116.397428, 39.90923],
    zoom: 12
  })

  map.value.on('load', () => {
    setupDataLayers()
    connect()
    console.log('✅ 地图初始化完成')
  })
}

// 设置数据图层
const setupDataLayers = () => {
  // 添加配送员数据源
  map.value.addSource('couriers', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: []
    }
  })

  // 配送员图标图层
  map.value.loadImage('https://docs.mapbox.com/mapbox-gl-js/assets/custom_marker.png', (error, image) => {
    if (error) throw error
    map.value.addImage('courier-icon', image)

    map.value.addLayer({
      id: 'couriers-layer',
      type: 'symbol',
      source: 'couriers',
      layout: {
        'icon-image': 'courier-icon',
        'icon-size': 0.5,
        'text-field': ['get', 'name'],
        'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
        'text-offset': [0, 1.5],
        'text-anchor': 'top',
        'text-size': 12
      }
    })
  })

  // 启动批量更新
  startBatchUpdate()
}

// WebSocket连接
const connect = () => {
  if (isConnected.value) return

  try {
    // 实际项目中替换为真实WebSocket地址
    ws = new WebSocket('ws://localhost:8080/realtime')

    ws.onopen = () => {
      isConnected.value = true
      reconnectAttempts = 0
      addLog('✅ WebSocket连接成功')
      console.log('✅ WebSocket连接成功')
    }

    ws.onmessage = (event) => {
      handleRealtimeData(event.data)
    }

    ws.onerror = (error) => {
      addLog('❌ WebSocket连接错误')
      console.error('❌ WebSocket错误:', error)
    }

    ws.onclose = () => {
      isConnected.value = false
      addLog('⚠️ WebSocket连接断开')
      console.log('⚠️ WebSocket连接断开')

      // 自动重连
      if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
        reconnectAttempts++
        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
        addLog(`🔄 ${delay/1000}秒后尝试重连(${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`)

        reconnectTimer = setTimeout(() => {
          connect()
        }, delay)
      } else {
        addLog('❌ 重连失败,已达到最大重连次数')
      }
    }
  } catch (error) {
    console.error('❌ WebSocket连接失败:', error)
    addLog('❌ WebSocket连接失败')
  }
}

// 断开连接
const disconnect = () => {
  if (ws) {
    ws.close()
    ws = null
  }
  if (reconnectTimer) {
    clearTimeout(reconnectTimer)
    reconnectTimer = null
  }
  isConnected.value = false
}

// 切换连接状态
const toggleConnection = () => {
  if (isConnected.value) {
    disconnect()
  } else {
    connect()
  }
}

// 处理实时数据
const handleRealtimeData = (data) => {
  try {
    const message = JSON.parse(data)

    // 统计推送速率
    pushCount++

    // 根据消息类型处理
    switch (message.type) {
      case 'courier_location':
        handleCourierLocation(message.data)
        break
      case 'batch_update':
        handleBatchUpdate(message.data)
        break
      case 'order_status':
        handleOrderStatus(message.data)
        break
      default:
        console.warn('未知消息类型:', message.type)
    }

    // 计算延迟
    if (message.timestamp) {
      latency.value = Date.now() - message.timestamp
    }
  } catch (error) {
    console.error('❌ 数据解析失败:', error)
  }
}

// 处理配送员位置更新
const handleCourierLocation = (data) => {
  // 数据验证
  if (!data.courierId || !data.lng || !data.lat) {
    console.warn('⚠️ 无效的配送员数据:', data)
    return
  }

  // 更新缓存
  const existingData = courierDataCache.get(data.courierId)

  const newData = {
    ...existingData,
    ...data,
    lastUpdate: Date.now()
  }

  courierDataCache.set(data.courierId, newData)

  // 加入更新队列(批量更新)
  updateQueue.push({
    type: 'update',
    courierId: data.courierId,
    data: newData
  })

  addLog(`📍 ${data.courierName || data.courierId} 位置更新`)
}

// 批量更新处理
const handleBatchUpdate = (dataList) => {
  dataList.forEach(item => {
    handleCourierLocation(item)
  })

  addLog(`📦 批量更新 ${dataList.length} 条数据`)
}

// 启动批量更新
const startBatchUpdate = () => {
  batchUpdateTimer = setInterval(() => {
    if (updateQueue.length === 0) return

    // 去重 - 同一个配送员只保留最新的更新
    const latestUpdates = new Map()

    updateQueue.forEach(update => {
      latestUpdates.set(update.courierId, update)
    })

    // 清空队列
    updateQueue.length = 0

    // 批量更新地图
    updateMapData(Array.from(latestUpdates.values()))

  }, BATCH_UPDATE_INTERVAL)
}

// 更新地图数据
const updateMapData = (updates) => {
  const features = Array.from(courierDataCache.values()).map(courier => ({
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [courier.lng, courier.lat]
    },
    properties: {
      id: courier.courierId,
      name: courier.courierName || courier.courierId,
      status: courier.status || 'active',
      speed: courier.speed || 0
    }
  }))

  const source = map.value.getSource('couriers')
  if (source) {
    source.setData({
      type: 'FeatureCollection',
      features
    })
  }

  // 更新统计
  onlineCouriers.value = courierDataCache.size
}

// 订单状态更新
const handleOrderStatus = (data) => {
  addLog(`📦 订单 ${data.orderId} 状态: ${data.status}`)
}

// 模拟高负载
const simulateHighLoad = () => {
  addLog('🚀 开始模拟高负载...')

  let count = 0
  const interval = setInterval(() => {
    // 每次生成50个更新
    for (let i = 0; i < 50; i++) {
      const mockData = {
        courierId: `courier-${Math.floor(Math.random() * 100)}`,
        courierName: `配送员${Math.floor(Math.random() * 100)}`,
        lng: 116.397428 + (Math.random() - 0.5) * 0.1,
        lat: 39.90923 + (Math.random() - 0.5) * 0.1,
        speed: Math.random() * 60,
        status: 'delivering'
      }

      handleCourierLocation(mockData)
    }

    count++
    if (count >= 20) { // 模拟1秒
      clearInterval(interval)
      addLog('✅ 高负载模拟完成')
    }
  }, 50)
}

// 性能监控
const startPerformanceMonitor = () => {
  setInterval(() => {
    // 计算推送速率
    const now = Date.now()
    const timeDiff = (now - lastPushTime) / 1000
    pushRate.value = Math.round(pushCount / timeDiff)

    pushCount = 0
    lastPushTime = now
  }, 1000)

  // 监控FPS
  const monitorFPS = () => {
    const now = performance.now()
    frameCount++

    if (now - lastFrameTime >= 1000) {
      renderFPS.value = Math.round((frameCount * 1000) / (now - lastFrameTime))
      frameCount = 0
      lastFrameTime = now
    }

    requestAnimationFrame(monitorFPS)
  }

  monitorFPS()
}

// 添加日志
const addLog = (content) => {
  const now = new Date()
  const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`

  updateLogs.value.unshift({ time, content })

  // 只保留最近20条
  if (updateLogs.value.length > 20) {
    updateLogs.value.pop()
  }
}
</script>

<style scoped>
.realtime-data-update {
  position: relative;
  width: 100%;
  height: 700px;
}

.map-container {
  width: 100%;
  height: 100%;
}

.monitor-panel {
  position: absolute;
  top: 10px;
  right: 10px;
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  min-width: 350px;
  max-height: 650px;
  overflow-y: auto;
  z-index: 1000;
}

.monitor-panel h3 {
  margin: 0 0 15px 0;
  font-size: 16px;
  color: #333;
}

.stat-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 20px;
}

.stat-card {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 15px;
  border-radius: 8px;
  text-align: center;
}

.stat-label {
  font-size: 12px;
  opacity: 0.9;
  margin-bottom: 8px;
}

.stat-value {
  font-size: 24px;
  font-weight: bold;
}

.update-log {
  margin-bottom: 15px;
}

.update-log h4 {
  font-size: 14px;
  color: #666;
  margin: 0 0 10px 0;
}

.log-list {
  max-height: 200px;
  overflow-y: auto;
  background: #f5f5f5;
  padding: 10px;
  border-radius: 4px;
}

.log-item {
  font-size: 12px;
  padding: 5px 0;
  border-bottom: 1px solid #e0e0e0;
}

.log-item:last-child {
  border-bottom: none;
}

.log-time {
  color: #999;
  margin-right: 10px;
}

.log-content {
  color: #333;
}

.control-buttons {
  display: flex;
  gap: 10px;
}

.btn {
  flex: 1;
  padding: 10px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.btn-success {
  background: #4CAF50;
  color: white;
}

.btn-success:hover {
  background: #45a049;
}

.btn-danger {
  background: #f44336;
  color: white;
}

.btn-danger:hover {
  background: #da190b;
}

.btn-warning {
  background: #FF9800;
  color: white;
}

.btn-warning:hover {
  background: #e68900;
}
</style>

1.3 增量更新优化方案

javascript
// 增量更新管理器
class IncrementalUpdateManager {
  constructor() {
    this.dataCache = new Map()
    this.updateQueue = []
    this.batchSize = 50
    this.updateInterval = 100
    this.timer = null
  }

  // 添加更新
  addUpdate(key, data) {
    // 如果已经在队列中,只更新数据
    const existing = this.updateQueue.find(item => item.key === key)

    if (existing) {
      existing.data = { ...existing.data, ...data }
    } else {
      this.updateQueue.push({ key, data })
    }

    // 队列过长立即处理
    if (this.updateQueue.length >= this.batchSize * 2) {
      this.flush()
    }
  }

  // 启动批量更新
  start() {
    this.timer = setInterval(() => {
      this.flush()
    }, this.updateInterval)
  }

  // 刷新更新队列
  flush() {
    if (this.updateQueue.length === 0) return

    // 取出一批数据
    const batch = this.updateQueue.splice(0, this.batchSize)

    // 合并到缓存
    batch.forEach(({ key, data }) => {
      const existing = this.dataCache.get(key)
      this.dataCache.set(key, {
        ...existing,
        ...data,
        _updateTime: Date.now()
      })
    })

    // 触发更新回调
    if (this.onUpdate) {
      this.onUpdate(Array.from(this.dataCache.values()))
    }

    console.log(`📊 批量更新${batch.length}条数据,队列剩余${this.updateQueue.length}条`)
  }

  // 停止更新
  stop() {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }

  // 清理过期数据
  cleanExpired(maxAge = 60000) {
    const now = Date.now()
    const expired = []

    this.dataCache.forEach((data, key) => {
      if (now - data._updateTime > maxAge) {
        expired.push(key)
      }
    })

    expired.forEach(key => {
      this.dataCache.delete(key)
    })

    if (expired.length > 0) {
      console.log(`🗑️ 清理${expired.length}条过期数据`)
    }
  }
}

// 使用示例
const manager = new IncrementalUpdateManager()

manager.onUpdate = (data) => {
  updateMapMarkers(data)
}

manager.start()

// 接收实时数据
ws.onmessage = (event) => {
  const data = JSON.parse(event.data)
  manager.addUpdate(data.id, data)
}

二、空间数据处理

2.1 GeoJSON数据处理

vue
<!-- GeoJSONProcessor.vue - GeoJSON数据处理 -->
<script setup>
import * as turf from '@turf/turf'

// 坐标系转换工具
class CoordinateConverter {
  // WGS84 -> GCJ02 (GPS -> 高德)
  static wgs84ToGcj02(lng, lat) {
    const a = 6378245.0
    const ee = 0.00669342162296594323

    let dLat = this.transformLat(lng - 105.0, lat - 35.0)
    let dLng = this.transformLng(lng - 105.0, lat - 35.0)

    const radLat = lat / 180.0 * Math.PI
    let magic = Math.sin(radLat)
    magic = 1 - ee * magic * magic
    const sqrtMagic = Math.sqrt(magic)

    dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI)
    dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Math.PI)

    return [lng + dLng, lat + dLat]
  }

  // GCJ02 -> WGS84 (高德 -> GPS)
  static gcj02ToWgs84(lng, lat) {
    const [gcjLng, gcjLat] = this.wgs84ToGcj02(lng, lat)
    return [lng * 2 - gcjLng, lat * 2 - gcjLat]
  }

  // GCJ02 -> BD09 (高德 -> 百度)
  static gcj02ToBd09(lng, lat) {
    const z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * Math.PI * 3000.0 / 180.0)
    const theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * Math.PI * 3000.0 / 180.0)

    const bdLng = z * Math.cos(theta) + 0.0065
    const bdLat = z * Math.sin(theta) + 0.006

    return [bdLng, bdLat]
  }

  // BD09 -> GCJ02 (百度 -> 高德)
  static bd09ToGcj02(lng, lat) {
    const x = lng - 0.0065
    const y = lat - 0.006
    const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * Math.PI * 3000.0 / 180.0)
    const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * Math.PI * 3000.0 / 180.0)

    const gcjLng = z * Math.cos(theta)
    const gcjLat = z * Math.sin(theta)

    return [gcjLng, gcjLat]
  }

  static transformLat(lng, lat) {
    let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat +
              0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng))
    ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0
    ret += (20.0 * Math.sin(lat * Math.PI) + 40.0 * Math.sin(lat / 3.0 * Math.PI)) * 2.0 / 3.0
    ret += (160.0 * Math.sin(lat / 12.0 * Math.PI) + 320 * Math.sin(lat * Math.PI / 30.0)) * 2.0 / 3.0
    return ret
  }

  static transformLng(lng, lat) {
    let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng +
              0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng))
    ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0
    ret += (20.0 * Math.sin(lng * Math.PI) + 40.0 * Math.sin(lng / 3.0 * Math.PI)) * 2.0 / 3.0
    ret += (150.0 * Math.sin(lng / 12.0 * Math.PI) + 300.0 * Math.sin(lng / 30.0 * Math.PI)) * 2.0 / 3.0
    return ret
  }
}

// GeoJSON处理工具类
class GeoJSONProcessor {
  // 数据压缩 - 保留6位小数
  static compressCoordinates(geojson) {
    const compress = (coords) => {
      if (typeof coords[0] === 'number') {
        return coords.map(c => Number(c.toFixed(6)))
      }
      return coords.map(compress)
    }

    const result = JSON.parse(JSON.stringify(geojson))

    if (result.type === 'FeatureCollection') {
      result.features = result.features.map(feature => ({
        ...feature,
        geometry: {
          ...feature.geometry,
          coordinates: compress(feature.geometry.coordinates)
        }
      }))
    } else if (result.type === 'Feature') {
      result.geometry.coordinates = compress(result.geometry.coordinates)
    }

    return result
  }

  // 数据简化 - Douglas-Peucker算法
  static simplify(geojson, tolerance = 0.0001) {
    if (geojson.type === 'FeatureCollection') {
      return {
        ...geojson,
        features: geojson.features.map(feature =>
          this.simplifyFeature(feature, tolerance)
        )
      }
    }
    return this.simplifyFeature(geojson, tolerance)
  }

  static simplifyFeature(feature, tolerance) {
    if (feature.geometry.type === 'LineString') {
      const simplified = turf.simplify(feature, { tolerance, highQuality: true })
      return simplified
    }
    return feature
  }

  // 缓冲区分析
  static buffer(geojson, radius, units = 'kilometers') {
    return turf.buffer(geojson, radius, { units })
  }

  // 判断点是否在多边形内
  static pointInPolygon(point, polygon) {
    return turf.booleanPointInPolygon(point, polygon)
  }

  // 计算距离
  static distance(from, to, units = 'kilometers') {
    return turf.distance(from, to, { units })
  }

  // 计算面积
  static area(polygon) {
    return turf.area(polygon)
  }

  // 计算中心点
  static center(geojson) {
    return turf.center(geojson)
  }

  // 合并多个几何体
  static union(...polygons) {
    let result = polygons[0]
    for (let i = 1; i < polygons.length; i++) {
      result = turf.union(result, polygons[i])
    }
    return result
  }

  // 裁剪
  static clip(polygon, clipPolygon) {
    return turf.intersect(polygon, clipPolygon)
  }
}

// 使用示例
const processGeoJSON = () => {
  // 1. 坐标系转换
  const gpsCoord = [116.397428, 39.90923]
  const gcj02Coord = CoordinateConverter.wgs84ToGcj02(...gpsCoord)
  console.log('GPS转高德:', gcj02Coord)

  // 2. 数据压缩
  const geojson = {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [116.397428123456, 39.909231234567]
        },
        properties: { name: '测试点' }
      }
    ]
  }

  const compressed = GeoJSONProcessor.compressCoordinates(geojson)
  console.log('压缩后:', compressed)

  // 3. 缓冲区分析
  const point = turf.point([116.397428, 39.90923])
  const buffered = GeoJSONProcessor.buffer(point, 1, 'kilometers')
  console.log('1公里缓冲区:', buffered)

  // 4. 距离计算
  const from = turf.point([116.397428, 39.90923])
  const to = turf.point([116.407428, 39.91923])
  const distance = GeoJSONProcessor.distance(from, to)
  console.log('距离:', distance, 'km')

  // 5. 点在多边形内判断
  const polygon = turf.polygon([[
    [116.39, 39.90],
    [116.40, 39.90],
    [116.40, 39.91],
    [116.39, 39.91],
    [116.39, 39.90]
  ]])

  const isInside = GeoJSONProcessor.pointInPolygon(point, polygon)
  console.log('点在多边形内:', isInside)
}
</script>

2.2 性能监控系统

vue
<!-- PerformanceMonitor.vue - 性能监控 -->
<script setup>
class PerformanceMonitor {
  constructor() {
    this.metrics = {
      fps: 0,
      memory: 0,
      renderTime: 0,
      networkLatency: 0,
      dataSize: 0
    }

    this.observers = []
  }

  // 开始监控
  start() {
    this.monitorFPS()
    this.monitorMemory()
    this.monitorNetwork()
  }

  // FPS监控
  monitorFPS() {
    let frames = 0
    let lastTime = performance.now()

    const measureFPS = () => {
      frames++
      const currentTime = performance.now()

      if (currentTime >= lastTime + 1000) {
        this.metrics.fps = Math.round((frames * 1000) / (currentTime - lastTime))
        frames = 0
        lastTime = currentTime
        this.notify()
      }

      requestAnimationFrame(measureFPS)
    }

    measureFPS()
  }

  // 内存监控
  monitorMemory() {
    setInterval(() => {
      if (performance.memory) {
        this.metrics.memory = Math.round(
          performance.memory.usedJSHeapSize / 1048576
        )
        this.notify()
      }
    }, 1000)
  }

  // 网络延迟监控
  monitorNetwork() {
    // 使用Performance API
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'resource') {
          this.metrics.networkLatency = Math.round(entry.duration)
        }
      }
    })

    observer.observe({ entryTypes: ['resource'] })
  }

  // 测量渲染时间
  measureRenderTime(callback) {
    const start = performance.now()
    callback()
    const end = performance.now()
    this.metrics.renderTime = Math.round(end - start)
    this.notify()
  }

  // 订阅通知
  subscribe(callback) {
    this.observers.push(callback)
  }

  // 通知观察者
  notify() {
    this.observers.forEach(callback => {
      callback(this.metrics)
    })
  }

  // 获取性能报告
  getReport() {
    return {
      ...this.metrics,
      timestamp: Date.now(),
      status: this.getStatus()
    }
  }

  // 评估性能状态
  getStatus() {
    if (this.metrics.fps < 30) return '差'
    if (this.metrics.fps < 50) return '中'
    return '优'
  }
}

// 使用示例
const monitor = new PerformanceMonitor()

monitor.subscribe((metrics) => {
  console.log('📊 性能指标:', metrics)

  // 性能告警
  if (metrics.fps < 30) {
    console.warn('⚠️ FPS过低:', metrics.fps)
  }

  if (metrics.memory > 500) {
    console.warn('⚠️ 内存占用过高:', metrics.memory, 'MB')
  }
})

monitor.start()
</script>

三、面试准备

3.1 简历描述

plain
【实时数据更新系统】高频数据推送 + 性能优化

【技术实现】
1. WebSocket实时通信
   - 心跳保活机制
   - 断线自动重连(指数退避策略)
   - 数据压缩传输(Gzip)

2. 批量更新优化
   - 100ms批量更新策略
   - 数据去重与合并
   - 增量更新机制
   - 队列溢出保护

3. 性能优化
   - 防抖节流控制
   - 离屏渲染优化
   - 内存泄漏监控
   - FPS实时监控

4. 空间数据处理
   - 坐标系转换(WGS84/GCJ02/BD09)
   - GeoJSON数据压缩
   - Turf.js空间分析
   - Douglas-Peucker简化算法

【优化成果】
✓ 支持1000条/秒高频推送,延迟<100ms
✓ 批量更新将渲染次数减少90%
✓ 内存占用降低60%
✓ FPS保持60,流畅无卡顿

【业务价值】
• 实现10万+配送员实时监控
• 数据延迟从3秒降至<1秒
• 系统稳定性从95%提升至99.9%

3.2 面试话术

面试官:高频数据推送如何保证性能?

plain
"核心是批量更新和去重策略。

问题分析:
- 高峰期每秒1000条推送
- 直接更新会触发1000次渲染
- 导致严重卡顿和性能问题

我的解决方案:

1. 批量更新
   - 设置100ms的批量更新间隔
   - 收集这100ms内的所有更新
   - 统一处理后一次性渲染
   - 将1000次渲染降到10次

2. 数据去重
   - 同一个配送员的多次更新只保留最新
   - 用Map结构快速去重
   - 减少无效渲染

3. 队列保护
   - 队列长度超过阈值立即处理
   - 防止内存占用过高
   - 优先级排序

实际效果:
- 渲染次数减少90%
- FPS从20提升到60
- 延迟<100ms
- 内存占用稳定

这个方案后来推广到订单状态、告警通知等多个实时场景。"
面试官:坐标系转换为什么这么复杂?
plain
"这是中国特有的问题,涉及国家安全。

背景知识:
- WGS84: GPS原始坐标,国际标准
- GCJ02: 高德/腾讯坐标,国家加密算法
- BD09: 百度坐标,在GCJ02基础上二次加密

为什么需要转换:
1. GPS设备返回WGS84
2. 高德地图需要GCJ02
3. 百度地图需要BD09
4. 不转换会偏移300-500米

我的实现:
- 封装CoordinateConverter工具类
- 实现WGS84<->GCJ02<->BD09互转
- 批量转换优化性能
- 缓存转换结果

实际应用:
我们系统同时使用高德和百度地图,
配送员GPS数据需要转换两次:
GPS -> GCJ02(高德)
GPS -> GCJ02 -> BD09(百度)

转换算法是国家保密的,我们用的是开源实现。"