唯杰地图 logo唯杰地图
VJMAP
EN

event-throttling

vjmap/references/perf/event-throttling.md 4.6 KB172 行
原文

本文回答:mousemove / move / render 等高频事件的节流策略 —— throttle / debounce / RAF 合并。

一、哪些事件是高频

事件 典型 fire 频率
map.on('mousemove', ...) 每秒 60+
map.on('move', ...) 拖动中每帧
map.on('zoom', ...) 缩放中每帧
map.on('render', ...) 仅地图重绘的帧(动画 / 交互中每帧,静止时不触发)
eventBus.on('entity:mouseenter', ...) 每帧 hover 的实体
fx.on('render', ...) 每一帧

不高频:click / moveend / zoomend / command:ended / styledata / load。

二、throttle(固定时间间隔)

function throttle(fn, ms) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last > ms) { last = now; fn(...args); }
  };
}

map.on('mousemove', throttle(e => {
  // 贵的查询:queryRenderedFeatures、HTTP 请求
}, 100));

适合: 节奏稳定的 UI 更新(状态栏、tooltip)。

三、debounce(静止后触发)

function debounce(fn, ms) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), ms);
  };
}

map.on('moveend', debounce(() => {
  fetchDataInBounds(map.getBounds());
}, 300));

适合: 用户操作停止后的一次性响应(搜索请求、持久化)。

四、requestAnimationFrame 合并

let pending = false;
map.on('mousemove', () => {
  if (pending) return;
  pending = true;
  requestAnimationFrame(() => {
    // 最多每帧执行一次
    update();
    pending = false;
  });
});

适合: UI 渲染(DOM 更新、Canvas 绘制)。优势:对齐浏览器渲染时序,不卡动画。

五、三种方案对比

方案 频率 适合
throttle 固定(如 100ms) 查询、面板更新
debounce 静止后 1 次 网络请求、保存
RAF 每帧最多 1 次 DOM / Canvas 渲染

六、实际场景

鼠标坐标显示

const displayCoord = throttle(e => {
  coordHUD.textContent = `${e.lngLat.lng.toFixed(6)}, ${e.lngLat.lat.toFixed(6)}`;
}, 16);           // ~60fps,肉眼流畅就够

map.on('mousemove', displayCoord);

视口内数据查询

const fetchInView = debounce(async () => {
  const b = map.getBounds();
  const data = await api.get(`/features?bbox=${b.toArray()}`);
  source.setData(data);
}, 500);

map.on('moveend', fetchInView);

跨组件状态同步

let pending = false;
map.on('move', () => {
  if (pending) return;
  pending = true;
  requestAnimationFrame(() => {
    syncMiniMap(map.getCenter(), map.getZoom(), map.getBearing());
    pending = false;
  });
});

七、eventBus 高频 handler

const heavy = throttle(({ entity }) => {
  updatePropertyPanel(entity);
}, 100);

mapcadLayer.eventBus.on(VjmapextEvents.EntityMouseEnter, heavy);

enter/leave 也可能每帧 fire(hover 在相邻实体间跳转)。

八、组合:throttle + 跳过重复

let lastId = null;
map.on('mousemove', throttle(e => {
  const feats = map.queryRenderedFeatures(e.point, { layers: ['pois'] });
  if (feats.length === 0) return;
  const id = feats[0].id;
  if (id === lastId) return;     // 没变,跳过
  lastId = id;
  tooltip.show(feats[0]);
}, 50));

九、性能采样里 FPS 统计

let frameCount = 0;
let lastTs = performance.now();
map.on('render', () => {
  frameCount++;
  const now = performance.now();
  if (now - lastTs >= 1000) {
    console.log(`FPS: ${frameCount}`);
    frameCount = 0;
    lastTs = now;
  }
});

⚠️ 引擎按需渲染,render 只在地图重绘时 fire,静止时不触发——这里统计的是地图重绘帧率(静止时读数趋近 0 并不代表卡),页面 FPS 用独立的 requestAnimationFrame 计数或 DevTools FPS meter。handler 里只 ++ 无 IO → 便宜。不要在 handler 里做 DOM 操作。

十、常见坑

  1. throttle 内闭包共享太重:每次 handler new 大对象 → GC 压力;提前声明。
  2. debounce 延迟后组件卸载:组件已销毁但定时器还没跑;卸载时 clearTimeout。
  3. RAF 嵌 RAF:每一层延迟一帧;避免。
  4. mousemove 里 queryRenderedFeatures 没节流:每秒 60 次查询,明显卡;必 throttle。
  5. eventBus.off 时 handler 是 throttle 包装后的函数:对,要 off 这个包装的引用,不是原函数。

相关