源码分析之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,
};
}
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,
});
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),
];
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;
}
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);
}
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;
}
}
}
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);
}
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);
}
}
2
3
4
5
6
7
8
9
10
注册事件最后会放在this.listeners_变量中,类似于这种
this.listeners_ = {
"change:target": this.handleTargetChanged_,
};
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();
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));
}
}
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;
}
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需要指定默认是地图容器。如此当按下方向键或者-/+,地图会进行平移或者缩放。