源码分析之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
需要指定默认是地图容器。如此当按下方向键或者-
/+
,地图会进行平移或者缩放。