发现问题
在开发webmap时遇到一个需求,即鼠标在Mapbox GL JS创建的地图中移动时不执行mousemove事件,等鼠标停止后执行。那么很容易想到的代码是下面这样的。
let timeoutfn;
function throttleMousemove(e){
console.log(e.features); // 输出 features
if (timeoutfn) {
clearTimeout(timeoutfn);
}
timeoutfn = setTimeout(function () {
console.log(e.features); // 输出 undefined
}, 50);
}
// 添加mousemove事件
map.on("mousemove", '测试图层', throttleMousemove);
很简单的实现,在setTimeout方法中获取Event对象,然后输入该对象的features属性。
但是,setTimeout中的console.log(e.features)
输出的是 undefined
,没错,确定是undefined
。
解决问题
因为业务代码比较复杂,首先想到的是数据在上下文变更了,然后排查上下文业务代码,各种业务逻辑都注释掉,也没发现问题,最后精简代码,只有上面的简单的监听事件,问题还是存在。
难道,Mapbox GL JS可能需要时间来解析鼠标所在位置下的地图特性(features),这些特性可能不是立即可用的?这也不可能啊,明显可以知道setTimeout内部的执行时间比外部要靠后,外部可以获取,内部不可能获取不到。
难道因为数据量大被垃圾回收机制自动给回收了?这个更不可能了,setTimeout 内部可以获取到e
对象的,没道理只回收features属性。这个时候考虑是不是e.features被Mapbox GL JS库的开发者销毁了?
难道这是Mapbox GL JS内部机制的问题?Mapbox GL JS可能在事件处理的某些阶段对事件对象进行了修改或清理操作,特别是在处理大量数据交互时,为了性能考虑,可能释放了非必需的资源?这确实也可能导致某些属性在异步回调中不可用。
然后我去扒了Mapbox GL JS的源码,确认了我的想法,确实是Mapbox GL JS内部机制的问题,在事件执行完之后,确实将e.features给回收了。贴一下部分源码:
_createDelegatedListener(type: keyof MapEventType | string, layerId: string, listener: Listener): {
layer: string;
listener: Listener;
delegates: {[type in keyof MapEventType]?: (e: any) => void};
} {
if (type === 'mouseenter' || type === 'mouseover') {
let mousein = false;
const mousemove = (e) => {
const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : [];
if (!features.length) {
mousein = false;
} else if (!mousein) {
mousein = true;
listener.call(this, new MapMouseEvent(type, this, e.originalEvent, {features}));
}
};
const mouseout = () => {
mousein = false;
};
return {layer: layerId, listener, delegates: {mousemove, mouseout}};
} else if (type === 'mouseleave' || type === 'mouseout') {
let mousein = false;
const mousemove = (e) => {
const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : [];
if (features.length) {
mousein = true;
} else if (mousein) {
mousein = false;
listener.call(this, new MapMouseEvent(type, this, e.originalEvent));
}
};
const mouseout = (e) => {
if (mousein) {
mousein = false;
listener.call(this, new MapMouseEvent(type, this, e.originalEvent));
}
};
return {layer: layerId, listener, delegates: {mousemove, mouseout}};
} else {
const delegate = (e) => {
const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : [];
if (features.length) {
// Here we need to mutate the original event, so that preventDefault works as expected.
e.features = features;
listener.call(this, e);
delete e.features;
}
};
return {layer: layerId, listener, delegates: {[type]: delegate}};
}
}
看源码,作者的的确确通过delete e.features
删除了features属性,所以在setTimeout方法中获取不到features。作者删除可以,我再次赋值也可以吧,你做初一我做十五,解决代码如下:
let timeoutfn;
function throttleMousemove(e){
console.log(e.features); // 输出 features
const featuresSnapshot = e.features.slice();
if (timeoutfn) {
clearTimeout(timeoutfn);
}
timeoutfn = setTimeout(function () {
e.features = featuresSnapshot;
console.log(e.features); // 输出 features
}, 50);
}
// 添加mousemove事件
map.on("mousemove", '测试图层', throttleMousemove);
至此,问题解决。下面是吐槽时间。
Here we need to mutate the original event, so that preventDefault works as expected.
你说为了preventDefault按预期工作需要更改原始事件。好,你事件传递完了,有必要将e.features delete掉吗?啊,你用完了,不给别人用是吧。