Jinuss's blog Jinuss's blog
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

东流

前端可视化
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • React

  • Vue

  • JavaScript文章

  • 学习笔记

  • openlayers

    • Openlayers用图片填充面
    • Openlayers用图片填充线
    • openlayers处理大量Overlay渲染问题
    • openlayers的比例尺
    • openlayers水印
    • openlayer实现矢量图层点击高亮效果
    • openlayers实现自定义路径
    • Openlayers加载渲染矢量切片
    • openlayers实现角度测量
    • openlayers实现长度测量
    • openlayers实现面积测量
    • Openlayers实现方位角测量
    • Openlayers中的动画
    • Openlayers的多边形高级交互
    • Openlayers地图底图换色
    • Openlayers种的默认交互事件
    • Openlayers默认键盘交互实现
    • 源码分析之Openlayers中默认键盘事件触发机制
      • 概述
      • 源码剖析
        • defaultsInteractions入口函数
        • 键盘keydown事件的回调
        • handleTargetChanged_回调函数
        • listen方法
        • handleBrowserEvent方法
        • handleMapBrowserEvent的核心代码,如下:
        • Map类的继承
        • 事件注册
        • change:target事件触发
        • setProperties方法
        • dispatchEvent方法
      • 总结
    • 源码分析之Openlayers中的Collection类
  • threejs

  • MapboxGL

  • 工具

  • 源码合集

  • 前端
  • openlayers
东流
2024-11-28
目录

源码分析之Openlayers中默认键盘事件触发机制

# 概述

在源码分析 Openlayers 默认键盘交互实现 (opens new window)文中介绍了KeyboardZoom和 KeyboardPan的实现,它们都是继承Interaction类,封装了自己的handleEvent方法,该方法接受一个mapBrowserEvent参数,计算出地图视图的变化量,最后调用view的animate方法实现地图的缩放或平移。

本文将介绍 Openlayers 是如何实现Interaction类,以及键盘事件流的全过程即如何映射到 Openlayers 的自定义事件。

# 源码剖析

# defaultsInteractions入口函数

当实例化一个地图,即new Map(options)时,会调用一个内部方法createOptionsInternal,该方法接受options参数经过一些处理,返回一个新的变量,如下:

function createOptionsInternal(options) {
  /**.... **/
  let interactions;
  if (options.interactions !== undefined) {
    if (Array.isArray(options.interactions)) {
      interactions = new Collection(options.interactions.slice());
    } else {
      assert(
        typeof (/** @type {?} */ (options.interactions).getArray) ===
          "function",
        "Expected `interactions` to be an array or an `ol/Collection.js`"
      );
      interactions = options.interactions;
    }
  }

  return {
    controls: controls,
    interactions: interactions,
    keyboardEventTarget: keyboardEventTarget,
    overlays: overlays,
    values: values,
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

由此可知,如果参数options对象的interactions值为空,则createOptionsInternal内部不对其有任何处理,在Map的构造函数里会有如下的判断:

const optionsInternal = createOptionsInternal(options);
this.interactions =
  optionsInternal.interactions ||
  defaultInteractions({
    onFocusOnly: true,
  });
1
2
3
4
5
6

在构造内部给optionsInternal变量赋值后,会判断它的interactions是否存在,如果不存在,则调用defaultsInteractions,这个方法就是决定了 Openlayers 采用默认的交互事件的入口函数。

# 键盘keydown事件的回调

构造函数Map内部会调用this.addChangeListener(MapProperty.TARGET, this.handleTargetChanged_);,这个注册后面会讲到,现在只需要知道,change:target变化会执行回调函数handleTargetChanged_

# handleTargetChanged_回调函数

handleTargetChanged_函数是Map类中的一个内部方法,主要用于监听到地图target的变化进行一些逻辑处理。在其中有如下一段逻辑:

this.targetChangeHandlerKeys_ = [
  listen(keyboardEventTarget, EventType.KEYDOWN, this.handleBrowserEvent, this),
];
1
2
3

上述代码定义了一个数组targetChangeHandlerKeys_,数组项是调用了两次listen()方法,用于监听键盘按键的keydown和keypress事件。

# listen方法

listen方法本质上就是element.addEventListener,其实现如下:

export function listen(target, type, listener, thisArg, once) {
  if (once) {
    const originalListener = listener;
    /**
     * @this {typeof target}
     */
    listener = function () {
      target.removeEventListener(type, listener);
      originalListener.apply(thisArg ?? this, arguments);
    };
  } else if (thisArg && thisArg !== target) {
    listener = listener.bind(thisArg);
  }
  const eventsKey = {
    target: target,
    type: type,
    listener: listener,
  };
  target.addEventListener(type, listener);
  return eventsKey;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# handleBrowserEvent方法

handleBrowserEvent就是对应listen方法的第三个参数,执行的那个回调函数。其实现如下:

  handleBrowserEvent(browserEvent, type) {
    type = type || browserEvent.type;
    const mapBrowserEvent = new MapBrowserEvent(type, this, browserEvent);
    this.handleMapBrowserEvent(mapBrowserEvent);
  }
1
2
3
4
5

browserEvent就是原生的事件参数,而MapBrowserEvent将原生事件包装一层,多了一些和坐标地图有关的参数信息,它们对比如下图所示:

然后实例对象mapBrowserEvent就是会在各个Interaction类中handleEvent方法接受的参数,在KeyboardPan和KeyboardZoom的实现中有提到,最后调用handleMapBrowserEvent方法。

# handleMapBrowserEvent的核心代码,如下:
if (this.dispatchEvent(mapBrowserEvent) !== false) {
  const interactionsArray = this.getInteractions().getArray().slice();
  for (let i = interactionsArray.length - 1; i >= 0; i--) {
    const interaction = interactionsArray[i];
    if (
      interaction.getMap() !== this ||
      !interaction.getActive() ||
      !this.getTargetElement()
    ) {
      continue;
    }
    const cont = interaction.handleEvent(mapBrowserEvent);
    if (!cont || mapBrowserEvent.propagationStopped) {
      break;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上述代码会去调用dispatchEvent,如果返回值不为false,就会遍历interactions,执行interaction.handleEvent(mapBrowserEvent),即KeyboardPan或KeyboardZoom的handleEvent方法。

# Map类的继承

dispatchEvent顾名思义就是派发事件,用于通知所有注册的事件listener。在弄清楚这个脉络清,我们先搞清楚Map类的继承关系。

  • 继承关系:Map类继承BaseObject类,BaseObject类继承Observable类,Observable类继承EventTarget类.

# 事件注册

在前面提到this.addChangeListener(MapProperty.TARGET, this.handleTargetChanged_);是注册了一个listener,MapProperty.TARGET就是实例化Map的参数target,对应地图的容器, Map类中注册方法addChangeListener实际上就是Observable类中定义事件addChangeListener,如下:

  addChangeListener(key, listener) {
    this.addEventListener(`change:${key}`, listener);
  }
1
2
3

可知,如果地图实例化时容器target是#map,则注册的事件类型就是change:target。addEventListener方法实际上是在EventTarget中定义的,如下:

  addEventListener(type, listener) {
    if (!type || !listener) {
      return;
    }
    const listeners = this.listeners_ || (this.listeners_ = {});
    const listenersForType = listeners[type] || (listeners[type] = []);
    if (!listenersForType.includes(listener)) {
      listenersForType.push(listener);
    }
  }
1
2
3
4
5
6
7
8
9
10

注册事件最后会放在this.listeners_变量中,类似于这种

this.listeners_ = {
  "change:target": this.handleTargetChanged_,
};
1
2
3

# change:target事件触发

通过Map类实例化map时,还会执行this.setProperties(optionsInternal.values);,而optionsInternal.values是在createOptionsInternal方法中赋值的,它就包含如下几个值

  values[MapProperty.LAYERGROUP] = layerGroup;

  values[MapProperty.TARGET] = options.target;

  values[MapProperty.VIEW] =
    options.view instanceof View ? options.view : new View();
1
2
3
4
5
6

因此可以得知this.setProperties(optionsInternal.values)中包含setProperties({[MapProperty.TARGET]:'#map'}),这个就和this.addChangeListener(MapProperty.TARGET, this.handleTargetChanged_);对应起来了。那么this.setProperties(optionsInternal.values)具体是如何执行的呢?

# setProperties方法

setProperties方法就是设置事例对象属性,它是在BaseObject类中定义的,如下:

  setProperties(values, silent) {
    for (const key in values) {
      this.set(key, values[key], silent);
    }
  }

  set(key, value, silent) {
    const values = this.values_ || (this.values_ = {});
    if (silent) {
      values[key] = value;
    } else {
      const oldValue = values[key];//silent值没传参,进入这步
      values[key] = value;
      if (oldValue !== value) {
        this.notify(key, oldValue);
      }
    }
  }

  notify(key, oldValue) {
    let eventType;
    eventType = `change:${key}`;
    if (this.hasListener(eventType)) {
      this.dispatchEvent(new ObjectEvent(eventType, key, oldValue));
    }
    eventType = ObjectEventType.PROPERTYCHANGE;
    if (this.hasListener(eventType)) {
      this.dispatchEvent(new ObjectEvent(eventType, key, oldValue));
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

由上述源码可知,setProperties方法中就是循环遍历参数values,调用set方法,而在set方法中就是进行新值和旧值比较,若二者不等,则调用notify方法进行通知。

在notify方法中会调用dispatchEvent方法,在前面提过setProperties({[MapProperty.TARGET]:'#map'}),即setProperties({taget:'#map'})所以到notify这步的key就是target,eventType就是change:target。而在事件注册时,this.listeners_变量中保存有{"change:target": this.handleTargetChanged_,},因此在条件判断this.hasListener('change:target')是会调用this.dispatchEvent方法。

# dispatchEvent方法

dispatchEvent的定义如下:

dispatchEvent(event) {
    const isString = typeof event === 'string';
    const type = isString ? event : event.type;
    const listeners = this.listeners_ && this.listeners_[type];
    if (!listeners) {
      return;
    }

    const evt = isString ? new Event(event) : /** @type {Event} */ (event);
    if (!evt.target) {
      evt.target = this.eventTarget_ || this;
    }
    const dispatching = this.dispatching_ || (this.dispatching_ = {});
    const pendingRemovals =
      this.pendingRemovals_ || (this.pendingRemovals_ = {});
    if (!(type in dispatching)) {
      dispatching[type] = 0;
      pendingRemovals[type] = 0;
    }
    ++dispatching[type];
    let propagate;
    for (let i = 0, ii = listeners.length; i < ii; ++i) {
      if ('handleEvent' in listeners[i]) {
        propagate = /** @type {import("../events.js").ListenerObject} */ (
          listeners[i]
        ).handleEvent(evt);
      } else {
        propagate = /** @type {import("../events.js").ListenerFunction} */ (
          listeners[i]
        ).call(this, evt);
      }
      if (propagate === false || evt.propagationStopped) {
        propagate = false;
        break;
      }
    }
    if (--dispatching[type] === 0) {
      let pr = pendingRemovals[type];
      delete pendingRemovals[type];
      while (pr--) {
        this.removeEventListener(type, VOID);
      }
      delete dispatching[type];
    }
    return propagate;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

dispatchEvent方法就是派发事件,满足条件后,执行事件类型对应的注册事件,handleTargetChanged_方法中不存在handleEvent,因此在循环遍历中执行(handleTargetChanged_).call(this,evt),而在前面this.dispatchEvent(mapBrowserEvent) !== false调用dispatchEvent方法时,其type是keyDown,返回值是undefined.

(handleTargetChanged_).call(this.evt)时会立即执行handleTargetChanged_方法,在其中就会调用listen(keyboardEventTarget,EventType.KEYDOWN,this.handleBrowserEvent,this)进行键盘按键keydown的监听。

# 总结

通过Map类实例化地图对象时,会调用createOptionsInternal,进行values[MapProperty.TARGET]的赋值,然后是this.interactions的默认赋值,this.addChangeListener(MapProperty.TARGET, this.handleTargetChanged_);注册事件this.listener={'change:target':handleTargetChanged_},紧接着就是调用setProperties(optionsInternal.values)方法,这操作会触发handleTargetChanged_回调,在handleTargetChanged_其中会调用listen方法监听键盘的keydown事件且当前target需要指定默认是地图容器。如此当按下方向键或者-/+,地图会进行平移或者缩放。

编辑 (opens new window)
上次更新: 2024/12/03, 14:43:37
Openlayers默认键盘交互实现
源码分析之Openlayers中的Collection类

← Openlayers默认键盘交互实现 源码分析之Openlayers中的Collection类→

最近更新
01
GeoJSON
05-08
02
Circle
04-15
03
CircleMarker
04-15
更多文章>
Theme by Vdoing | Copyright © 2024-2025 东流 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式