這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 2023年了,我不允許還有人不會自己實現移動端的雙擊事件。 過來,看這裡,不足 50 行的代碼實現的雙擊事件。 聽筆者娓娓道來。 dblclick js原生有個dblclick雙擊事件,但是幾乎不支持移動端。 而且,該dblclic ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
前言
2023年了,我不允許還有人不會自己實現移動端的雙擊事件。
過來,看這裡,不足 50 行的代碼實現的雙擊事件。
聽筆者娓娓道來。
dblclick
js原生有個dblclick
雙擊事件,但是幾乎不支持移動端。
而且,該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間隔也可使用。
自定義事件名大家可以隨意設置,滿足語義化即可。
-
監聽
click
事件,併在捕獲階段監聽,目的是為了後續能夠阻止click
事件傳播。
window.addEventListener('click', handler, true);
監聽函數中,第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; } }
處理完第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) } }
以上便實現了雙擊事件,全局任意地方監聽雙擊。
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) => { // 實現雙擊邏輯 })