1. 移動端點擊事件click出現延遲
工作中接觸了移動端,發現同事們都會用如下代碼去寫移動端的點擊事件,嘗試使用,屢試不爽,一旦沒有用下邊這段代碼,點擊事件就會出現各種各樣的問題,在連續使用了N多次之后(本人並沒有愛鑽研的精神~~有點兒懶),終於決定自己上網查一些資料,看看到底是什么原因唄。(解釋jquery的方法data():在匹配元素上存儲任意相關數據 或 返回匹配的元素集合中的第一個元素的給定名稱的數據存儲的值。trigger():規定被選元素要觸發的事件。)
//自定義tap
$(document).on("touchstart", function(e) {
if(!$(e.target).hasClass("disable")) $(e.target).data("isMoved", 0);
});
$(document).on("touchmove", function(e) {
if(!$(e.target).hasClass("disable")) $(e.target).data("isMoved", 1);
});
$(document).on("touchend", function(e) {
if(!$(e.target).hasClass("disable") && $(e.target).data("isMoved") == 0) $(e.target).trigger("tap");
});
2. 問題出在哪了
為什么要用tap事件代替click事件?答案:300 毫秒延遲
這要追溯至 2007 年初。蘋果公司在發布首款 iPhone 前夕,遇到一個問題 —— 當時的網站都是為大屏幕設備所設計的。於是蘋果的工程師們做了一些約定,應對 iPhone 這種小屏幕訪問電腦版的網頁的問題。這當中最出名的,當屬雙擊縮放(double tap to zoom)。
當用戶一次點擊屏幕之后,瀏覽器並不能立刻判斷用戶是要進行雙擊縮放,還是想要進行單擊操作。因此,iOS Safari 就等待 300 毫秒,以判斷用戶是否再次點擊了屏幕。
於是,300 毫秒延遲就這么誕生了。
鑒於iPhone的成功,其他移動瀏覽器都復制了 iPhone Safari 瀏覽器的多數約定,包括雙擊縮放,幾乎現在所有的移動端瀏覽器都有這個功能(嗯~嘗試了一下好像微信瀏覽器沒有再遵守這個雙擊縮放的約定)。之前人們剛剛接觸移動端的頁面,在欣喜的時候往往不會care這個300ms的延時問題,可是如今移動端界面如雨后春筍,用戶對體驗的要求也更高,這300ms帶來的卡頓慢慢變得讓人難以接受。
3. 實例操作300ms出現的過程
一開始觸摸事件touchstart、touchmove和touchend是iOS版Safari瀏覽器為了向開發人員傳達一些信息新添加的事件。因為iOs設備既沒有鼠標也沒有鍵盤,所以在為移動Safari瀏覽器開發交互性網頁的時候,PC端的鼠標和鍵盤事件是不夠用的。
在iPhone 3Gs發布的時候,其自帶的移動Safari瀏覽器就提供了一些與觸摸(touch)操作相關的新事件。隨后,Android上的瀏覽器也實現了相同的事件(大家統一了,移動端瀏覽器都有了touch事件啦)。觸摸事件(touch)會在用戶手指放在屏幕上面的時候、在屏幕上滑動的時候或者是從屏幕上移開的時候觸發。下面具體說明:
touchstart事件:當手指觸摸屏幕時候觸發,即使已經有一個手指放在屏幕上也會觸發。
touchmove事件:當手指在屏幕上滑動的時候連續地觸發。這個事件發生期間,調用preventDefault()事件可以阻止滾動。
touchend事件:當手指從屏幕上離開的時候觸發。
touchcancel事件:當系統停止跟蹤觸摸的時候觸發。關於這個事件的確切時間,文檔中並沒有具體說明,這里不再詳述。
touch事件及click事件同時綁定在一個元素上demo:(一言不合上代碼~~e.type指代事件類型)
$(document).on('touchstart', '.wantUp', function(e) {
$(".upArtical1 h5.up").append(e.type);
})
$(document).on('touchmove', '.wantUp', function(e) {
$(".upArtical1 h5.up").append(e.type);
})
$(document).on('touchend', '.wantUp', function(e) {
$(".upArtical1 h5.up").append(e.type);
})
$(document).on('click', '.wantUp', function(e) {
$(".upArtical1 h5.up").append(e.type);
});
可以看到,我在一個元素上邊綁定了 touchstart , touchmove , touchend ,click事件,奇妙的事情發生了:
touch, click事件的執行順序: touchstart > touchmove > touchend > click。很明顯,touch事件執行完畢后才會到click事件,這就是我們所說的300ms的延遲。
touchmove ,click事件互斥,即touchmove觸發執行,click事件不再執行 ,事件的執行順序就為touchstart > touchmove(可以多次執行) > touchend 。
短暫觸摸(點)一下屏幕,上述代碼的事件的執行順序:touchstart > touchend > click。
OK,到目前為止我們已經闡述了300ms的產生,而這300ms的產生還帶來一個巨大的問題,點透事件。
4. 點透事件
點透發生的條件:
A 和 B不是后代繼承關系(如果是后代繼承關系的話,就直接是冒泡之類的話題了(在此不再贅述 ))
A發生touch(也可以是click)后立即消失,B事件綁定click
A z-index大於B,即A顯示在B浮層之上
點透發生的原因:
當手指觸摸到屏幕的時候,系統生成兩個事件,一個是touch 一個是click,touch先執行,touch執行完成后,A從文檔樹上面消失了,而且由於移動端click還有延遲200-300ms的關系,當系統要觸發click的時候,發現在用戶點擊的位置上面,目前離用戶最近的元素是B,所以就直接把click事件作用在B元素上面了
A.addEventListener('touch', function(e) {
A.style.display = 'none';
});
A.onclick = function() {
console.log('B莫名被點擊了');
}
解決方案是touch階段取消掉 click 事件:touch事件內調用:e.preventDefault()
5. 解決方案
以上問題的解決方案就是:既然瀏覽器統一了touch事件,就用touch事件去模擬click事件。
還記得文章最開始的那段代碼嗎?
//自定義tap
$(document).on("touchstart", function(e) {
if(!$(e.target).hasClass("disable")) $(e.target).data("isMoved", 0);
});
$(document).on("touchmove", function(e) {
if(!$(e.target).hasClass("disable")) $(e.target).data("isMoved", 1);
});
$(document).on("touchend", function(e) {
if(!$(e.target).hasClass("disable") && $(e.target).data("isMoved") == 0) $(e.target).trigger("tap");
});
解釋一下吧:現在原理就很明顯了~
基於touchstart、touchmove、touchend這三個事件,通過事件委托的方式來實現tap事件。
e.target是事件源的觸發節點,$(e.target)是該節點的jQuery封裝對象,isMoved這個就相當於一個開關,移動了置為1,沒移動置為0。
第一步:監聽touchstart事件,事件觸發后通過jQuery的data方法設置該對象的isMoved狀態為0。
第二步:監聽touchmove事件,事件觸發后通過jQuery的data方法設置該對象的isMoved狀態為1。
第三步:監聽touchend事件,事件觸發后判斷該對象是否touchMove過,沒有則觸發tap事件。
如何算tap事件:手指點上去 不移動 快速松開 。
6. 如何使用這段代碼?
復制上述代碼到頁面js處
對該使用click事件的元素統一換成tap事件
https://blog.csdn.net/weixin_40756572/article/details/81776615
https://www.cnblogs.com/fly_dragon/p/8663609.html
1. PC端事件在移動端的兼容問題
1.1 click事件的200~300ms延遲問題
由於移動端默認的布局視口寬度是980像素,所以網頁文字非常小,為了快速讓網頁還原到原來的大小,Safari最新引入了雙擊縮放功能:用戶雙擊手機頁面的時候,瀏覽器會智能的縮放當前頁面到原始大小。
雙擊縮放的原理就是,當用戶click一次之后,瀏覽器會經過約300ms之后檢測是否再有一次click,如果有的話,就會縮放頁面。否則的話就是一個click事件。
由於雙擊縮放功能存在,click事件觸發就會有大約200~300ms的延遲。
1.2 dblclick事件失效
由於雙擊縮放的存在,pc端的dblclick事件也失效了。
2. 移動端特有的touch事件
由於移動端設備大都具備觸摸功能,所以移動端瀏覽器都引入了觸摸(touch)事件。
touch相關的事件跟普通的其他dom事件一樣使用,可以直接用addEventListener來監聽和處理。
最基本的touch事件包括4個事件:
-
touchstart: 當在屏幕上按下手指時觸發
-
touchmove: 當在屏幕上移動手指時觸發
-
touchend: 當在屏幕上抬起手指時觸發
-
touchcancel 當一些更高級別的事件發生的時候(如電話接入或者彈出信息)會取消當前的touch操作,即觸發touchcancel。一般會在touchcancel時暫停游戲、存檔等操作。
2.1 touch事件與click事件同時觸發
在很多情況下,觸摸事件和鼠標事件會同時被觸發(目的是讓沒有對觸摸設備優化的代碼仍然可以在觸摸設備上正常工作)。
因為雙擊縮放檢測的存在,在移動設備屏幕上點擊操作的事件執行順序:
touchstart(瞬間觸發) → touchend → click(200-300ms延遲)
如果你使用了觸摸事件,可以調用 event.preventDefault()來阻止鼠標事件被觸發。
2.2 touchstart事件
當用戶手指觸摸到的觸摸屏的時候觸發。事件對象的 target 就是touch 發生位置的那個元素。
<div> 點擊我! </div> <script> var box = document.querySelector("div"); box.addEventListener("touchstart", function (e) { console.log('touchstart'); }); </script>
2.3 touchmove事件
當用戶在觸摸屏上移動觸點(手指)的時候,觸發這個事件。一定是先要觸發touchstart事件,再有可能觸發 touchmove 事件。
touchmove 事件的target 與最先觸發的 touchstart 的 target 保持一致。touchmove事件和鼠標的mousemove事件一樣都會多次重復調用,所以,事件處理時不能有太多耗時操作。不同的設備,移動同樣的距離 touchmove 事件的觸發頻率是不同的。
注意:
- 即使手指移出了 原來的target 元素,則 touchmove 仍然會被一直觸發,而且 target 仍然是原來的 target 元素。
- touchmove事件會多次重復觸發,由於移動端計算資源寶貴,盡量保證事件節流
<div> <p></p> </div> <script> var i = 1; var box = document.querySelector("div"); var p = document.querySelector("p"); box.addEventListener("touchmove", function (e){ p.innerHTML = e.target.tagName + ", " + i++; }) </script>

2.4 touchend事件
當用戶的手指抬起的時候,會觸發 touchend 事件。如何用戶的手指從觸屏設備的邊緣移出了觸屏設備,也會觸發 touchend 事件。
touchend 事件的 target 也是與 touchstart 的 target 一致,即使已經移出了元素。

2.5 touchcancel事件
當觸點由於某些原因被中斷時觸發。有幾種可能的原因如下(具體的原因根據不同的設備和瀏覽器有所不同):
- 由於某個事件取消了觸摸:例如觸摸過程被一個模態的彈出框打斷。
- 觸點離開了文檔窗口,而進入了瀏覽器的界面元素、插件或者其他外部內容區域。
- 當用戶產生的觸點個數超過了設備支持的個數,從而導致 TouchList 中最早的 Touch對象被取消
touchcancel 事件一般用於保存現場數據。比如:正在玩游戲,如果發生了 。touchcancel 事件,則應該把游戲當前狀態相關的一些數據保存起來。
3. 觸摸事件對象
TouchEvent
是一類描述手指在觸摸平面(觸摸屏、觸摸板等)的狀態變化的事件。這類事件用於描述一個或多個觸點,使開發者可以檢測觸點的移動,觸點的增加和減少,等等。
每 個 Touch
對象代表一個觸點; 每個觸點都由其位置,大小,形狀,壓力大小,和目標 element
描述。 TouchList
對象代表多個觸點的一個列表.
3.1 TouchEvent
TouchEvent
的屬性繼承了 UIEvent
和 Event
。
屬性列表:
-
TouchEvent.changedTouches
: 一個TouchList
對象,包含了代表所有從上一次觸摸事件到此次事件過程中,狀態發生了改變的觸點的Touch
對象。 -
TouchEvent.targetTouches
: 一個TouchList
對象,是包含了如下觸點的Touch
對象:觸摸起始於當前事件的目標element
上,並且仍然沒有離開觸摸平面的觸點。 -
TouchEvent.touches
: 一 個TouchList
對象,包含了所有當前接觸觸摸平面的觸點的Touch
對象,無論它們的起始於哪個element
上,也無論它們狀態是否發生了變化。
<style> .box { width: 100px; height: 100px; border: 1px solid #09c; background-color: #0dc; } </style> <div class="box"></div> <script> window.onload = function() { var box = document.querySelector('.box'); box.addEventListener('touchstart', function(e) { console.dir(e); // 查看TouchEvent對象的屬性和方法 }); } </script>

3.2 TouchList詳解
一個TouchList
代表一個觸摸屏幕上所有觸點的列表。
舉例來講, 如果一個用戶用三根手指接觸屏幕(或者觸控板), 與之相關的TouchList
對於每根手指都會生成一個 Touch
對象, 共計 3 個.
-
只讀屬性:
length
返回這個
TouchList
中Touch
對的個數。(就是有幾個手指接觸到了屏幕) -
方法:
item(index)
返回
TouchList
中指定索引的Touch
對象。
<div> <p style="font-size: 50px; color: #ffffff;"></p> </div> <script> var box = document.querySelector("div"); var p = document.querySelector("p"); box.addEventListener("touchend", function (e){ p.innerHTML = e.changedTouches.length; //返回Touch對象的個數 for(var i = 0; i < e.changedTouches.length; i++){ //遍歷出來每個Touch對象 console.log(e.changedTouches.item(i)); } }) </script>

測試多個手機觸摸屏幕:
<div></div> <p></p> <script> var div = document.querySelector("div"); var p = document.querySelector("p"); div.addEventListener("touchstart", function (e){ var msg = "touches.length: " + e.touches.length + "<br> targetTouches.length: " + e.targetTouches.length + "<br> changedTouches.length: " + e.changedTouches.length; p.innerHTML = msg; }) </script>
操作:
-
放1個手指在div上
- 先放1個手指在其他地方,然后再放1個手指在
div
上
- 先放1個手指在其他地方,然后再逐漸放2個手指在
div
上
3.3 Touch詳解
Touch
表示用戶和觸摸設備之間接觸時單獨的交互點(a single point of contact
)。 這個交互點通常是一個手指或者觸摸筆, 觸摸設備通常是觸摸屏或者觸摸板。
基本屬性列表(都是只讀):
編號 | 屬性名 | 屬性說明 |
---|---|---|
1. | identifier |
表示每 1 個 Touch 對象 的獨一無二的 identifier 。有了這個 identifier 可以確保你總能追蹤到這個 Touch 對象。 |
2. | screenX |
觸摸點相對於屏幕左邊緣的 x 坐標。 |
3. | scre enY |
觸摸點相對於屏幕上邊緣的 y 坐標。 |
4. | clientX |
觸摸點相對於瀏覽器的 viewport 左邊緣的 x 坐標。不會包括左邊的滾動距離。 |
5. | clientY |
觸摸點相對於瀏覽器的 viewport 上邊緣的 y 坐標。不會包括上邊的滾動距離。 |
6. | pageX |
觸摸點相對於 document 的左邊緣的 x 坐標。 與 clientX 不同的是,他包括左邊滾動的距離,如果有的話。 |
7. | pageY |
觸摸點相對於 document 的左邊緣的 y 坐標。 與 clientY 不同的是,他包括上邊滾動的距離,如果有的話。 |
8. | target |
總是表示 手指最開始放在觸摸設備上的觸發點所在位置的 element 。 即使已經移出了元素甚至移出了document , 他表示的element 仍然不變 |
案例:
var box = document.querySelector("div"); var p = document.querySelector("p"); box.ontouchstart = function (e){ var touchList = e.changedTouches; for (var i = 0; i < touchList.length; i++){ var touch = touchList[i]; var msg = `id : ${touch.identifier} <br> screenX : ${touch.screenX} <br> screenY : ${touch.screenY} <br> clientX : ${touch.clientX} <br> clientY : ${touch.clientY} <br> pageX : ${touch.pageX} <br> pageY : ${touch.pageY} <br> target: ${touch.target.nodeName} <br> `; p.innerHTML = msg; } }
沒有左右滾動:

左右滾動:pageX
明顯大於 clientX

4. 封裝移動端tap事件
由於點擊事件經常使用,如果用click會有延遲問題,一般我們會用touch事件模擬移動端的點擊事件, 以下是封裝的幾個事件,僅供參考。
(function (window){ //傳入window,提高變量的查找效率 function myQuery(selector){ //這個函數就是對外提供的接口。 //調用這個函數的原型對象上的_init方法,並返回 return myQuery.prototype._init(selector); } myQuery.prototype = { /*初始化方法,獲取當前query對象的方法*/ _init: function (selector){ if (typeof selector == "string"){ //把查找到的元素存入到這個原型對象上。 this.ele = window.document.querySelector(selector); //返回值其實就是原型對象。 return this; } }, /*單擊事件: * 為了規避click的300ms的延遲,自定義一個單擊事件 * 觸發時間: * 當抬起手指的時候觸發 * 需要判斷手指落下和手指抬起的事件間隔,如果小於500ms表示單擊時間。 * 如果是大於等於500ms,算是長按時間 * */ tap: function (handler){ this.ele.addEventListener("touchstart", touchFn); this.ele.addEventListener("touchend", touchFn); var startTime, endTime; function touchFn(e){ e.preventDefault() switch (e.type){ case "touchstart": startTime = new Date().getTime(); break; case "touchend": endTime = new Date().getTime(); if (endTime - startTime < 500){ handler.call(this, e); } break; } } }, /** * 長按 * @param handler */ longTag: function (handler){ this.ele.addEventListener("touchstart", touchFn); this.ele.addEventListener("touchmove", touchFn); this.ele.addEventListener("touchend", touchFn); var timerId; function touchFn(e){ switch (e.type){ case "touchstart" : //500ms之后執行 timerId = setTimeout(function (){ handler.call(this, e); }, 500) break; case "touchmove" : //如果中間有移動也清除定時器 clearTimeout(timerId) break; case "touchend" : //如果在500ms之內抬起了手指,則需要定時器 clearTimeout(timerId); break; } } }, /** * 左側滑動。 * 記錄手指按下的左邊,在離開的時候計算 deltaX是否滿足左滑的條件 */ slideLeft: function (handler){ this.ele.addEventListener("touchstart", touchFn); this.ele.addEventListener("touchend", touchFn); var startX, startY, endX, endY; function touchFn(e){ e.preventDefault(); var firstTouch = e.changedTouches[0]; switch (e.type){ case "touchstart": startX = firstTouch.pageX; startY = firstTouch.pageY; break; case "touchend": endX = firstTouch.pageX; endY = firstTouch.pageY; //x方向移動大於y方向的移動,並且x方向的移動大於25個像素,表示在向左側滑動 if (Math.abs(endX - startX) >= Math.abs(endY - startY) && startX - endX >= 25){ handler.call(this, e); } break; } } }, /* 右側滑動 */ rightLeft: function (e){