Leaflet源码解析系列(六-7):Map 对象解读——简单 DOM 事件

384 阅读3分钟

注册了键盘、鼠标和手势交互事件在地图上的响应方法,这些事件只是原始的dom事件,与拖拽、拉框这种组合 dom 事件有差别。

_mouseEvents 包括 ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu']

export const Map = Evented.extend({
  // ......
  _mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'],

  /** 绑定 dom 交互事件 */
	_initEvents(remove) {
		this._targets = {};
		this._targets[Util.stamp(this._container)] = this;

		const onOff = remove ? DomEvent.off : DomEvent.on;

		// @event click: MouseEvent
		// 当用户点击 (or taps) 地图时触发
		// @event dblclick: MouseEvent
		// 当用户双击 (or double-taps) 地图时触发
		// @event mousedown: MouseEvent
		// 当用户按下鼠标按钮的时候触发
		// @event mouseup: MouseEvent
		// 当用户松开鼠标按钮的时候触发
		// @event mouseover: MouseEvent
		// 当鼠标悬浮进入地图时候触发
		// @event mouseout: MouseEvent
		// 当鼠标悬浮移出地图时候触发
		// @event mousemove: MouseEvent
		// 当暑校在地图上悬浮移动时候触发
		// @event contextmenu: MouseEvent
		// 当用户在地图上点击鼠标右键时候触发,注册事件后会组织浏览器的默认右键菜单。移动端长按也能触发
		// @event keypress: KeyboardEvent
		// 当地图是 focused 状态时,响应用户的键盘事件,按下键盘上产生字符值的键时触发(如Alt,Shift,Ctrl,或Meta 不会触发)
		// @event keydown: KeyboardEvent
		// 当地图是 focused 状态时,用户按下键盘按键时候触发. 和`keypress`不同,keydown 在按下键盘上产生字符值和不产生字符值的键时都会触发
		// @event keyup: KeyboardEvent
		// 当地图是 focused 状态时,用户松开键盘按键时候触发
		onOff(this._container, 'click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup', this._handleDOMEvent, this);

		// 监听浏览器窗口大小变化,地图也跟着调整
		if (this.options.trackResize) {
			if (!remove) {
				if (!this._resizeObserver) {
					this._resizeObserver = new ResizeObserver(this._onResize.bind(this));
				}
				this._resizeObserver.observe(this._container);
			} else {
				this._resizeObserver.disconnect();
			}
		}

		if (Browser.any3d && this.options.transform3DLimit) {
			(remove ? this.off : this.on).call(this, 'moveend', this._onMoveEnd);
		}
	},

  /** 处理 dom 事件 */
	_handleDOMEvent(e) {
		const el = e.target || e.srcElement;
		// 如果地图没有初始化或者禁用了事件,则不做处理
		if (!this._loaded || el['_leaflet_disable_events'] || (e.type === 'click' && this._isClickDisabled(el))) {
			return;
		}

		const type = e.type;

		if (type === 'mousedown') {
			// 阻止点击 keyboard-focusable element 时候显示边框
			DomUtil.preventOutline(el);
		}

		// 触发事件
		this._fireDOMEvent(e, type);
	},

	/** 响应 dom 事件 */
	_fireDOMEvent(e, type, canvasTargets) {
		if (e.type === 'click') {
			// Fire a synthetic 'preclick' event which propagates up (mainly for closing popups).
			// 触发一个合成的 preclick 事件,这个事件会向上冒泡(主要用于关闭弹出框popups的窗口)。
			// @event preclick: MouseEvent
			// Fired before mouse click on the map (sometimes useful when you want something to happen on click before any existing click handlers start running).
			// 在单击 map 之前触发(如果你想要在点击事件之前处理一些事情,这种方式很有用)
			const synth = Util.extend({}, e);
			synth.type = 'preclick';
			this._fireDOMEvent(synth, synth.type, canvasTargets);
		}

		// Find the layer the event is propagating from and its parents.
		// 找出事件冒泡的初始图层和它的父级。target 的内容是有顺序的(按照目标图层,从下到父级依次)
		let targets = this._findEventTargets(e, type);

		// 只把注册了监听函数的 canvas target 添加进来
		if (canvasTargets) {
			const filtered = [];
			for (let i = 0; i < canvasTargets.length; i++) {
				if (canvasTargets[i].listens(type, true)) {
					filtered.push(canvasTargets[i]);
				}
			}
			targets = filtered.concat(targets);
		}

		// 事件没有目标,就直接返回
		if (!targets.length) {
			return;
		}

		// 阻止浏览器默认的右键
		if (type === 'contextmenu') {
			DomEvent.preventDefault(e);
		}

		const target = targets[0];
		const data = {
			originalEvent: e
		};

		// 不是键盘事件,是鼠标或者手势事件
		if (e.type !== 'keypress' && e.type !== 'keydown' && e.type !== 'keyup') {
			const isMarker = target.getLatLng && (!target._radius || target._radius <= 10); // 是不是 marker 图层
			// 为事件 data 添加 leaflet 封装的属性
			data.containerPoint = isMarker ? this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e);
			data.layerPoint = this.containerPointToLayerPoint(data.containerPoint);
			data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint);
		}

		for (let i = 0; i < targets.length; i++) {
			// 依次触发各个 target 上的事件,data 中有 leaflet 添加的属性
			targets[i].fire(type, data, true);
			if (data.originalEvent._stopped || (targets[i].options.bubblingMouseEvents === false && this._mouseEvents.includes(type))) {
				// 阻止了冒泡,就不触发后续的事件了
				return;
			}
		}
	},

	/** 查找事件的目标图层 */
	_findEventTargets(e, type) {
		let targets = [],
		target,
		src = e.target || e.srcElement,
		dragging = false;
		const isHover = type === 'mouseout' || type === 'mouseover';

		while (src) {
			// 如果 src 绑定过 dom 事件,会在 this._targets 中
			// layer 在创建的时候会注册 dom 事件,也会向 map的 _targets 添加属性
			target = this._targets[Util.stamp(src)];
			if (target && (type === 'click' || type === 'preclick') && this._draggableMoved(target)) {
				// Prevent firing click after you just dragged an object.
				// 如果是 drag,就阻止点击事件,停止后续的父级循环
				dragging = true;
				break;
			}
			if (target && target.listens(type, true)) {
				// 是 hover 类型事件,但是鼠标进入或者移出的目标元素不是 src,就停止后续的父级循环
				if (isHover && !DomEvent.isExternalTarget(src, e)) {
					break;
				}
				// 将 target 存入数组
				targets.push(target);
				// 如果是 hover 就停止后续的父级循环,不会响应到父节点上
				// 例如 map 和 marker 都注册了 mouseover 事件,当鼠标在 marker 上的时候,map的 mouseover 就不会继续响应了
				if (isHover) {
					break;
				}
			}
			// 如果循环到了根节点,没有父节点了,就停止
			if (src === this._container) {
				break;
			}
			// 设置为父节点继续循环
			src = src.parentNode;
		}
		// 如果 targets 为空且是 dragging 且不是 hover 且 map 已经注册过当前类型的事件,那么 target 就是 map
		if (!targets.length && !dragging && !isHover && this.listens(type, true)) {
			targets = [this];
		}
		return targets;
	},

	/** 判断是不是在 drag */
	_draggableMoved(obj) {
		obj = obj.dragging && obj.dragging.enabled() ? obj : this;
		return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved());
	},

  /** move 结束,检查下transform的值,如果超限了就重置一下 */
  _onMoveEnd() {
    const pos = this._getMapPanePos();
    if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) {
      // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have
      // a pixel offset on very high values, see: https://jsfiddle.net/dg6r5hhb/
      this._resetView(this.getCenter(), this.getZoom());
    }
  },
  
	/** 处理地图 resize */
	_onResize() {
		Util.cancelAnimFrame(this._resizeRequest);
		this._resizeRequest = Util.requestAnimFrame(function () {
			this.invalidateSize({debounceMoveend: true});
		}, this);
	},

	// @method invalidateSize(options: Zoom/pan options): this
	// @alternative
	// @method invalidateSize(animate: Boolean): this
	// 检查地图容器的 size 是否改变,如果改变了就更新 map,动态改变 map 的大小后调用这个方法。默认是进行平移处理。
	// 如果 options.pan 是 false,panning 不会发生
	// 如果 options.debounceMoveend 是 true`, 会延迟 moveend 事件响应,即使连续多次调用该方法也不会响应
	invalidateSize(options) {
		if (!this._loaded) {
			return this;
		}

		options = Util.extend(
			{
				animate: false,
				pan: true
			},
			options === true ? {animate: true} : options
		);

		const oldSize = this.getSize();
		this._sizeChanged = true;
		this._lastCenter = null;

		const newSize = this.getSize(),
		oldCenter = oldSize.divideBy(2).round(),
		newCenter = newSize.divideBy(2).round(), // 计算新的地图中心
		offset = oldCenter.subtract(newCenter); // 计算平移距离

		if (!offset.x && !offset.y) {
			return this;
		}

		// 平移地图
		if (options.animate && options.pan) {
			this.panBy(offset);
		} else {
			if (options.pan) {
				this._rawPanBy(offset);
			}

			// 触发 move 事件
			this.fire('move');

			// 200 毫秒内连续多次改变 map size,不会重复触发 moveend 事件
			if (options.debounceMoveend) {
				clearTimeout(this._sizeTimer);
				this._sizeTimer = setTimeout(this.fire.bind(this, 'moveend'), 200);
			} else {
				this.fire('moveend');
			}
		}

		// @section Map state change events
		// @event resize: ResizeEvent
		// 触发地图 resize 事件
		return this.fire('resize', {
			oldSize,
			newSize
		});
	},

  /** 根节点上scroll的时候,保证根节点位置一直是(0,0) */
  _onScroll() {
    this._container.scrollTop = 0;
    this._container.scrollLeft = 0;
  },
  // ......
})