> **本文回答**：图层级事件 —— `map.on('click', layerId, handler)` 只在命中特定图层要素时触发。

## 一、图层级 vs 全局级

```js
map.on('click', handler);                // 全局：任意位置
map.on('click', 'my-layer', handler);    // 图层：只命中 my-layer 里的要素
```

图层级传第二参，要素信息在 `e.features`：

```js
map.on('click', 'roads', e => {
  const feat = e.features[0];
  console.log(feat.properties.road_name);
});
```

## 二、支持的事件

| 事件 | 说明 |
|---|---|
| `click` | 命中要素时 |
| `dblclick` | 双击要素 |
| `mouseenter` | 进入要素 |
| `mouseleave` | 离开要素 |
| `mousemove` | 在要素上移动 |
| `mousedown` / `mouseup` | |
| `contextmenu` | 右键 |
| `touchstart` / `touchend` | 移动端 |

## 三、常见用途

### 1 指针样式

```js
map.on('mouseenter', 'buildings', () => map.getCanvas().style.cursor = 'pointer');
map.on('mouseleave', 'buildings', () => map.getCanvas().style.cursor = '');
```

### 2 点击显示 Popup

```js
map.on('click', 'pois', e => {
  const f = e.features[0];
  new vjmap.Popup()
    .setLngLat(f.geometry.coordinates)
    .setHTML(`<b>${f.properties.name}</b>`)
    .addTo(map);
});
```

### 3 Hover 高亮（feature-state）

```js
let hovered = null;
map.on('mousemove', 'roads', e => {
  if (hovered !== null) map.setFeatureState({ source: 'roads-src', id: hovered }, { hover: false });
  hovered = e.features[0].id;
  map.setFeatureState({ source: 'roads-src', id: hovered }, { hover: true });
});
map.on('mouseleave', 'roads', () => {
  // 用 !== null 而非 if (hovered)：generateId 从 0 编号，0 为假值，第一个要素的 hover 会清不掉
  if (hovered !== null) map.setFeatureState({ source: 'roads-src', id: hovered }, { hover: false });
  hovered = null;
});

// 样式里
map.addLineLayer('roads', 'roads-src', {
  lineColor: ['case', ['boolean', ['feature-state', 'hover'], false], '#f00', '#888'],
  lineWidth: 3
});
```

## 四、e.features 数据

```js
map.on('click', 'my-layer', e => {
  for (const f of e.features) {
    f.id                // 数字 / 字符串（需 promoteId 或 generateId）
    f.properties        // 对象
    f.geometry          // GeoJSON 几何
    f.layer             // 图层样式定义
    f.source            // 来源 id
    f.sourceLayer       // 矢量瓦片子图层
  }
});
```

## 五、多图层监听

```js
['layer1', 'layer2', 'layer3'].forEach(id => {
  map.on('click', id, e => handle(e, id));
});
```

或用 `queryRenderedFeatures` 一次过滤：

```js
map.on('click', e => {
  const feats = map.queryRenderedFeatures(e.point, {
    layers: ['layer1', 'layer2', 'layer3']
  });
  if (feats.length === 0) return;
  handle(feats[0]);
});
```

或直接用 map 上内置的 map-gl-utils 助手（layers 可传 id / 数组 / 正则，`clickOneLayer` 只收数组；都返回解绑函数）：

```js
const offPointer = map.hoverPointer(['layer1', 'layer2']);   // 悬停变手型
const offHover = map.hoverFeatureState('roads');             // 自动维护 feature-state hover（id 为 0 的要素不生效）
const offClick = map.clickOneLayer(['layer1', 'layer2', 'layer3'],
  ({ layer, feature }) => handle(feature, layer),            // 只派发给第一个命中的图层
  e => console.log('点到空白处', e.lngLat));                  // 全部未命中
// 另有 map.clickLayer / hoverLayer / hoverPopup / clickPopup
offClick();   // 解绑
```

## 六、事件触发顺序

同时绑定全局级和图层级时，触发顺序 = **注册顺序**（图层级本质是包了一层 queryRenderedFeatures 过滤的普通监听器，源码 `_createDelegatedListener` 证实）：

```js
map.on('click', e => ...);               // 先注册先跑——即使图层级后注册也排后面
map.on('click', 'my-layer', e => ...);
```

⚠️ `e.preventDefault()` **不会**阻止后续监听器执行（Evented.fire 不检查 defaultPrevented，只标记事件）——要阻止全局 handler，在全局 handler 里自查命中：

```js
map.on('click', e => {
  if (map.queryRenderedFeatures(e.point, { layers: ['my-layer'] }).length) return;
  // 真正的"空白处"逻辑
});
```

## 七、结合 popup 关闭

```js
let popup = null;
map.on('click', 'pois', e => {
  if (popup) popup.remove();
  popup = new vjmap.Popup().setLngLat(e.lngLat).setHTML(html(e.features[0])).addTo(map);
});
map.on('click', e => {
  // 全局 click：检查是否命中图层，没命中就关 popup
  const hit = map.queryRenderedFeatures(e.point, { layers: ['pois'] });
  if (hit.length === 0 && popup) { popup.remove(); popup = null; }
});
```

## 八、off 取消

```js
const handler = e => { ... };
map.on('click', 'my-layer', handler);

// 取消
map.off('click', 'my-layer', handler);
```

必须传相同的 `layerId` 和 `handler` 引用。

## 九、常见坑

1. **可先绑后加图层**：委托监听每次事件时才 `getLayer(layerId)`，图层不存在只是不触发；先绑后 addLayer、或 removeLayer 后重加都照常生效。
2. **匿名 handler 无法 off**：保留引用。
3. **e.features 空**：可能 source 刚 setData 还没完；在 `sourcedata` 后。
4. **重叠要素**：handler 每次点击只调一次，`e.features` 含该图层在该点下的全部要素（自上而下），只要最上层取 `[0]`；不同图层各自的图层级监听都会触发。
5. **点击空白也 fire 全局 click**：`e.features` undefined，不会 fire 图层级；用 `queryRenderedFeatures` 判命中。
6. **`e.features` 只在 handler 同步执行期间有效**：`_createDelegatedListener` 在 handler 返回后立即 `delete e.features`——把事件对象存起来异步读、或 `resolve(e)` 后再访问，`features` 都是 `undefined`。要保留数据就在 handler 内同步取值/拷贝：`const tag = e.features[0].properties.tag`。
7. **刚 addSource+addLayer 立刻 `queryRenderedFeatures` 会返回空**：GeoJSON source 的瓦片由 worker 异步生成，需轮询或等 `sourcedata` 事件后再查（通常约 0.4–2s 才命中）。

## 相关

- 交互事件 → [map-interaction](map-interaction.md)
- feature-state → [batch/geojson-bulk](../batch/geojson-bulk.md)
- Popup → [popup/overview](../popup/overview.md)
