注册
web

移动端的双击事件好不好用?

前言


2023年了,我不允许还有人不会自己实现移动端的双击事件。


过来,看这里,不足 50 行的代码实现的双击事件。


听笔者娓娓道来。


dblclick


js原生有个dblclick双击事件,但是几乎不支持移动端。


developer.mozilla.org_zh-CN_docs_Web_API_Element_dblclick_event.png


而且,该dblclick事件在pc端鼠标双击时,会触发两次click与一次dblclick


window.addEventListener('click', () => {
console.log('click')
});
window.addEventListener('dblclick', () => {
console.log('dblclick')
});

// 双击页面,打印:click✖️2 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间隔也可使用。


自定义事件名大家可以随意设置,满足语义化即可。





  1. 监听click事件,并在捕获阶段监听,目的是为了后续能够阻止click事件传播。


    window.addEventListener('click', handler, true);



  2. 监听函数中,第1次点击时,记录点击位置,并设置200ms倒计时。如果第2次点击在200ms后,则重新派发当前事件,让事件继续传播,使其它的监听函数可以继续处理对应事件。


    // 标识是否在等待第2次点击
    let isWaiting = false;

    // 记录点击位置
    let prevPosition = {};

    function handler(evt) {
    const { pageX, pageY } = evt;
    prevPostion = { pageX, pageY };
    // 阻止冒泡,不让事件继续传播
    evt.stopPropagation();
    // 开始等待第2次点击
    isWaiting = true;
    // 设置200ms倒计时,200ms后重新派发当前事件
    timer = setTimeout(() => {
    isWaiting = false;
    evt.target.dispatchEvent(evt);
    }, 200)
    }

    注意: 倒计时结束时evt.target.dispatchEvent(evt)派发的事件仍是原来的事件对象,即仍是click事件,会触发继续handler函数,进入了循环。


    这里需要破局,已知Event事件对象下有一个 isTrusted 属性,是一个只读属性,是一个布尔值。当事件是由用户行为生成的时候,这个属性的值为 true ,而当事件是由脚本创建、修改、通过 EventTarget.dispatchEvent()派发的时候,这个属性的值为 false 。


    因此,此处脚本派发的事件是希望继续传递的事件,不用handler内处理。


    function handler(evt) {
    // 如果事件是脚本派发的则不处理,将该事件继续传播
    if(!evt.isTrusted){
    return;
    }
    }



  3. 处理完第1次点击后,接着处理在200ms内的第2次点击事件。如果满足位移小于10px的条件,则视为双击。


    // 标识是否在等待第2次点击
    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);
    // 如果满足位移小于10,则是双击
    if(diffX <= 10 && diffY <= 10) {
    // 取消当前事件传递,并派发1个自定义双击事件
    evt.stopPropagation();
    evt.target.dispatchEvent(
    new PointerEvent('dbltouch', {
    cancelable: false,
    bubbles: true,
    })
    )
    }
    } else {
    prevPostion = { pageX, pageY };
    // 阻止冒泡,不让事件继续传播
    evt.stopPropagation();
    // 开始等待第2次点击
    isWaiting = true;
    // 设置200ms倒计时,200ms后重新派发当前事件
    timer = setTimeout(() => {
    isWaiting = false;
    evt.target.dispatchEvent(evt);
    }, 200)
    }
    }



  4. 以上便实现了双击事件,全局任意地方监听双击。


    window.addEventListener('dbltouch', () => {
    console.log('dbltouch');
    })
    window.addEventListener('click', () => {
    console.log('click');
    })
    // 使用鼠标、手指双击
    // 打印出 dbltouch
    // 而且不会打印有click



笔者要在这里说句 但是: 由于200ms的延时,虽不多,但是对于操作迅速的用户来讲,还是会有不好的体验。


优化双击事件


由于是在window上注册的click函数,虽说注册双击事件像单击事件一样简单了,但却也导致整个产品页面的click事件都会推迟200ms执行。


因此,我们应该只对需要处理双击的地方添加双击事件,至少只在局部发生延迟情况。稍微调整下代码,将需要注册双击事件的元素由开发决定,通过参数传递。而且事件处理函数也可以通过参数传递,即可以通过监听双击事件,也可以通过回调函数执行。


以下是完整的代码。


class RegisterDbltouchEvent {
constructor(el, fn) {
this.el = el || window;
this.callback = fn;
this.timer = null;
this.prevPosition = {};
this.isWaiting = false;

// 注册click事件,注意this指向
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);
// 如果满足位移小于10,则是双击
if(diffX <= 10 && diffY <= 10) {
// 取消当前事件传递,并派发1个自定义双击事件
evt.stopPropagation();
evt.target.dispatchEvent(
new PointerEvent('dbltouch', {
cancelable: false,
bubbles: true,
})
);
// 也可以采用回调函数的方式
this.callback && this.callback(evt);
}
} else {
this.prevPostion = { pageX, pageY };
// 阻止冒泡,不让事件继续传播
evt.stopPropagation();
// 开始等待第2次点击
this.isWaiting = true;
// 设置200ms倒计时,200ms后重新派发当前事件
this.timer = setTimeout(() => {
this.isWaiting = false;
evt.target.dispatchEvent(evt);
}, 200)
}
}
}

只为需要实现双击逻辑的元素注册双击事件。可以通过传递回调函数的方式执行业务逻辑,也可以通过监听dbltouch事件的方式,也可以同时使用,it's up to you.


const el = document.querySelector('#dbltouch');
new RegisterDbltouchEvent(el, (evt) => {
// 实现双击逻辑
})

最后


采用的click事件模拟双击事件,因此在移动端和pc端都可以使用该构造函数。


作者:Yue栎廷
来源:juejin.cn/post/7274043371731796003

0 个评论

要回复文章请先登录注册