前言
2023年了,我不允许还有人不会自己实现移动端的双击事件。
过来,看这里,不足 50 行的代码实现的双击事件。
听笔者娓娓道来。
dblclick
js原生有个dblclick
双击事件,但是几乎不支持移动端。
而且,该dblclick
事件在pc端鼠标双击时,会触发两次click
与一次dblclick
。
1
2
3
4
5
6
7
8
|
window.addEventListener( 'click' , () => {
console.log( 'click' )
});
window.addEventListener( 'dblclick' , () => {
console.log( 'dblclick' )
});
|
我们期望可以在移动端也能有双击事件,并且隔离单击与双击事件,双击时只触发双击事件,只执行双击回调函数,让注册双击事件像注册原生事件一样简单。
点击穿透
简单聊聊移动端的点击穿透。
在移动端单击会依次触发touchstart
->touchmove
->touchend
->click
事件。
有这样一段逻辑,在touchstart
时出现全屏弹框,在click
弹框时关闭弹框。实际上,在点击页面时,弹框会一闪而过,并没有出现正确的交互。在移动端单击时touchstart
早于click
,当弹框出现了,后来的click
事件就落在了弹框上,导致弹框被关闭。这就是点击穿透的一种表现。
笔者的业务需求是双击元素,出现全屏弹框,单击弹框时关闭弹框。因此基于这样的业务需求与现实的点击穿透问题,笔者选择采用click
事件来模拟双击事件,并且适配pc端使用。大家也可以选择解决点击穿透问题,并采用touchstart
模拟双击事件,可以更快地响应用户操作。
采用touchstart
模拟时,可以再考虑排除双指点击的情况。
在实现上与下文代码除了事件对象获取位置属性有所不同外,其它代码基本一致,实现思路无差别。
模拟双击事件
采用click
事件来模拟实现双击。
双击事件定义:2次点击事件间隔小于200ms
,并且点击范围小于10px
的视为双击。这里的双击事件是自定义事件,为了区分原生的 dblclick
,又优先满足移动端使用,则事件名定义为 dbltouch
,后续可以使用window.addEventListener('dbltouch', ()=>{})
来监听双击事件。
这个间隔与位移限制大家可以根据自己的业务需求调整。通常采用的是300ms的间隔与10px的位移,笔者业务中发现200ms间隔也可使用。
自定义事件名大家可以随意设置,满足语义化即可。
-
监听click
事件,并在捕获阶段监听,目的是为了后续能够阻止click
事件传播。
1
|
window.addEventListener( 'click' , handler, true );
|
监听函数中,第1次点击时,记录点击位置,并设置200ms
倒计时。如果第2次点击在200ms
后,则重新派发当前事件,让事件继续传播,使其它的监听函数可以继续处理对应事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
let isWaiting = false ;
let prevPosition = {};
function handler(evt) {
const { pageX, pageY } = evt;
prevPostion = { pageX, pageY };
evt.stopPropagation();
isWaiting = true ;
timer = setTimeout(() => {
isWaiting = false ;
evt.target.dispatchEvent(evt);
}, 200)
}
|
注意: 倒计时结束时evt.target.dispatchEvent(evt)
派发的事件仍是原来的事件对象,即仍是click
事件,会触发继续handler
函数,进入了循环。
这里需要破局,已知Event
事件对象下有一个 isTrusted
属性,是一个只读属性,是一个布尔值。当事件是由用户行为生成的时候,这个属性的值为 true
,而当事件是由脚本创建、修改、通过 EventTarget.dispatchEvent()
派发的时候,这个属性的值为 false
。
因此,此处脚本派发的事件是希望继续传递的事件,不用handler
内处理。
1
2
3
4
5
6
|
function handler(evt) {
if (!evt.isTrusted){
return ;
}
}
|
处理完第1次点击后,接着处理在200ms
内的第2次点击事件。如果满足位移小于10px
的条件,则视为双击。
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
|
let isWaiting = false ;
const prevPosition = {};
function handler(evt) {
if (!evt.isTrusted){
return ;
}
const { pageX, pageY } = evt;
if (isWaiting) {
isWaiting = false ;
const diffX = Math.abs(pageX - prevPosition.pageX);
const diffY = Math.abs(pageY - prevPosition.pageY);
if (diffX <= 10 && diffY <= 10) {
evt.stopPropagation();
evt.target.dispatchEvent(
new PointerEvent( 'dbltouch' , {
cancelable: false ,
bubbles: true ,
})
)
}
} else {
prevPostion = { pageX, pageY };
evt.stopPropagation();
isWaiting = true ;
timer = setTimeout(() => {
isWaiting = false ;
evt.target.dispatchEvent(evt);
}, 200)
}
}
|
以上便实现了双击事件,全局任意地方监听双击。
1
2
3
4
5
6
7
8
9
|
window.addEventListener( 'dbltouch' , () => {
console.log( 'dbltouch' );
})
window.addEventListener( 'click' , () => {
console.log( 'click' );
})
|
笔者要在这里说句 但是: 由于200ms
的延时,虽不多,但是对于操作迅速的用户来讲,还是会有不好的体验。
优化双击事件
由于是在window
上注册的click
函数,虽说注册双击事件像单击事件一样简单了,但却也导致整个产品页面的click
事件都会推迟200ms
执行。
因此,我们应该只对需要处理双击的地方添加双击事件,至少只在局部发生延迟情况。稍微调整下代码,将需要注册双击事件的元素由开发决定,通过参数传递。而且事件处理函数也可以通过参数传递,即可以通过监听双击事件,也可以通过回调函数执行。
以下是完整的代码。
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
47
48
49
50
|
class RegisterDbltouchEvent {
constructor(el, fn) {
this .el = el || window;
this .callback = fn;
this .timer = null ;
this .prevPosition = {};
this .isWaiting = false ;
this .el.addEventListener( 'click' , this .handleClick.bind( this ), true );
}
handleClick(evt){
if ( this .timer) {
clearTimeout( this .timer);
this .timer = null ;
}
if (!evt.isTrusted) {
return ;
};
if ( this .isWaiting){
this .isWaiting = false ;
const diffX = Math.abs(pageX - this .prevPosition.pageX);
const diffY = Math.abs(pageY - this .prevPosition.pageY);
if (diffX <= 10 && diffY <= 10) {
evt.stopPropagation();
evt.target.dispatchEvent(
new PointerEvent( 'dbltouch' , {
cancelable: false ,
bubbles: true ,
})
);
this .callback && this .callback(evt);
}
} else {
this .prevPostion = { pageX, pageY };
evt.stopPropagation();
this .isWaiting = true ;
this .timer = setTimeout(() => {
this .isWaiting = false ;
evt.target.dispatchEvent(evt);
}, 200)
}
}
}
|
只为需要实现双击逻辑的元素注册双击事件。可以通过传递回调函数的方式执行业务逻辑,也可以通过监听dbltouch
事件的方式,也可以同时使用,it's up to you.
1
2
3
4
|
const el = document.querySelector( '#dbltouch' );
new RegisterDbltouchEvent(el, (evt) => {
})
|
本文转载于:
https://juejin.cn/post/7274043371731796003
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。
__EOF__
-
本文作者: 林恒
本文链接: https://www.cnblogs.com/smileZAZ/p/17677788.html