摘要:分別存儲事件的定時器。事件定時器延時時間存儲事件對象滑動方向判斷我們根據下圖以及對應的代碼來理解滑動的時候方向是如何判定的。取消長按,以及取消所有事件取消長按取消所有事件方式都是類似,先調用取消定時器,然后釋放對應的變量,等候垃圾回收。
前言
移動端原生支持touchstart、touchmove、touchend等事件,但是在平常業務中我們經常需要使用swipe、tap、doubleTap、longTap等事件去實現想要的效果,對于這種自定義事件他們底層是如何實現的呢?讓我們從Zepto.js的touch模塊去分析其原理。您也可以直接查看touch.js源碼注釋
源碼倉庫
原文鏈接
事件簡述Zepto的touch模塊實現了很多與手勢相關的自定義事件,分別是swipe, swipeLeft, swipeRight, swipeUp, swipeDown,doubleTap, tap, singleTap, longTap
事件名稱 | 事件描述 |
---|---|
swipe | 滑動事件 |
swipeLeft | ←左滑事件 |
swipeRight | →右滑事件 |
swipeUp | ↑上滑事件 |
swipeDown | ↓下滑事件 |
doubleTap | 雙擊事件 |
tap | 點擊事件(非原生click事件) |
singleTap | 單擊事件 |
longTap | 長按事件 |
;["swipe", "swipeLeft", "swipeRight", "swipeUp", "swipeDown", "doubleTap", "tap", "singleTap", "longTap"].forEach(function(eventName){ $.fn[eventName] = function(callback){ return this.on(eventName, callback) } })
可以看到Zepto把這些方法都掛載到了原型上,這意味著,你可以直接用簡寫的方式例如$("body").tap(callback)
前置條件在開始分析這些事件如何實現之前,我們先了解一些前置條件
部分內部變量
var touch = {}, touchTimeout, tapTimeout, swipeTimeout, longTapTimeout, // 長按事件定時器時間 longTapDelay = 750, gesture
touch: 用以存儲手指操作的相關信息,例如手指按下時的位置,離開時的坐標等。
touchTimeout,tapTimeout, swipeTimeout,longTapTimeout分別存儲singleTap、tap、swipe、longTap事件的定時器。
longTapDelay:longTap事件定時器延時時間
gesture: 存儲ieGesture事件對象
滑動方向判斷(swipeDirection)
我們根據下圖以及對應的代碼來理解滑動的時候方向是如何判定的。需要注意的是瀏覽器中的“坐標系”和數學中的坐標系還是不太一樣,Y軸有點反過來的意思。
/** * 判斷移動的方向,結果是Left, Right, Up, Down中的一個 * @param {} x1 起點的橫坐標 * @param {} x2 終點的橫坐標 * @param {} y1 起點的縱坐標 * @param {} y2 終點的縱坐標 */ function swipeDirection(x1, x2, y1, y2) { /** * 1. 第一個三元運算符得到如果x軸滑動的距離比y軸大,那么是左右滑動,否則是上下滑動 * 2. 如果是左右滑動,起點比終點大那么往左滑動 * 3. 如果是上下滑動,起點比終點大那么往上滑動 * 需要注意的是這里的坐標和數學中的有些不一定 縱坐標有點反過來的意思 * 起點p1(1, 0) 終點p2(1, 1) */ return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? "Left" : "Right") : (y1 - y2 > 0 ? "Up" : "Down") }
觸發長按事件
function longTap() { longTapTimeout = null if (touch.last) { // 觸發el元素的longTap事件 touch.el.trigger("longTap") touch = {} } }
在觸發長按事件之前先將longTapTimeout定時器取消,如果touch.last還存在則觸發之,為什么要判斷touch.last呢,因為swip, doubleTap,singleTap會將touch對象置空,當這些事件發生的時候,自然不應該發生長按事件。
取消長按,以及取消所有事件
// 取消長按 function cancelLongTap() { if (longTapTimeout) clearTimeout(longTapTimeout) longTapTimeout = null } // 取消所有事件 function cancelAll() { if (touchTimeout) clearTimeout(touchTimeout) if (tapTimeout) clearTimeout(tapTimeout) if (swipeTimeout) clearTimeout(swipeTimeout) if (longTapTimeout) clearTimeout(longTapTimeout) touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null touch = {} }
方式都是類似,先調用clearTimeout取消定時器,然后釋放對應的變量,等候垃圾回收。
整體結構分析$(document).ready(function(){ /** * now 當前觸摸時間 * delta 兩次觸摸的時間差 * deltaX x軸變化量 * deltaY Y軸變化量 * firstTouch 觸摸點相關信息 * _isPointerType 是否是pointerType */ var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType $(document) .bind("MSGestureEnd", function(e){ // xxx 先不看這里 }) .on("touchstart MSPointerDown pointerdown", function(e){ // xxx 關注這里 }) .on("touchmove MSPointerMove pointermove", function(e){ // xxx 關注這里 }) .on("touchend MSPointerUp pointerup", function(e){ // xxx 關注這里 }) .on("touchcancel MSPointerCancel pointercancel", cancelAll) $(window).on("scroll", cancelAll) })
這里將詳細代碼暫時省略了,留出整體框架,可以看出Zepto在dom,ready的時候在document上添加了MSGestureEnd,touchstart MSPointerDown pointerdown,touchmove MSPointerMove pointermove,touchcancel MSPointerCancel pointercancel等事件,最后還給在window上加了scroll事件。我們將目光聚焦在touchstart,touchmove,touchend對應的邏輯,其他相對少見的事件在暫不討論
touchstartif((_isPointerType = isPointerEventType(e, "down")) && !isPrimaryTouch(e)) return
要走到touchstart事件處理程序后續邏輯中,需要先滿足一些條件。到底是哪些條件呢?先來看看isPointerEventType, isPrimaryTouch兩個函數做了些什么。
**isPointerEventType
function isPointerEventType(e, type){ return (e.type == "pointer"+type || e.type.toLowerCase() == "mspointer"+type) }
Pointer Event相關知識點擊這里
isPrimaryTouch
function isPrimaryTouch(event){ return (event.pointerType == "touch" || event.pointerType == event.MSPOINTER_TYPE_TOUCH) && event.isPrimary }
根據mdn pointerType,其類型可以是mouse,pen,touch,這里只處理其值為touch并且isPrimary為true的情況。
接著回到
if((_isPointerType = isPointerEventType(e, "down")) && !isPrimaryTouch(e)) return
其實就是過濾掉非觸摸事件。
觸摸點信息兼容處理
// 如果是pointerdown事件則firstTouch保存為e,否則是e.touches第一個 firstTouch = _isPointerType ? e : e.touches[0]
這里只清楚e.touches[0]的處理邏輯,另一種不太明白,望有知曉的同學告知一下,感謝感謝。
復原終點坐標
// 一般情況下,在touchend或者cancel的時候,會將其清除,如果用戶調阻止了默認事件,則有可能清空不了,但是為什么要將終點坐標清除呢? if (e.touches && e.touches.length === 1 && touch.x2) { // Clear out touch movement data if we have it sticking around // This can occur if touchcancel doesn"t fire due to preventDefault, etc. touch.x2 = undefined touch.y2 = undefined }
存儲觸摸點部分信息
// 保存當前時間 now = Date.now() // 保存兩次點擊時候的時間間隔,主要用作雙擊事件 delta = now - (touch.last || now) // touch.el 保存目標節點 // 不是標簽節點則使用該節點的父節點,注意有偽元素 touch.el = $("tagName" in firstTouch.target ? firstTouch.target : firstTouch.target.parentNode) // touchTimeout 存在則清除之,可以避免重復觸發 touchTimeout && clearTimeout(touchTimeout) // 記錄起始點坐標(x1, y1)(x軸,y軸) touch.x1 = firstTouch.pageX touch.y1 = firstTouch.pageY
判斷雙擊事件
// 兩次點擊的時間間隔 > 0 且 < 250 毫秒,則當做doubleTap事件處理 if (delta > 0 && delta <= 250) touch.isDoubleTap = true
處理長按事件
// 將now設置為touch.last,方便上面可以計算兩次點擊的時間差 touch.last = now // longTapDelay(750毫秒)后觸發長按事件 longTapTimeout = setTimeout(longTap, longTapDelay)touchmove
.on("touchmove MSPointerMove pointermove", function(e){ if((_isPointerType = isPointerEventType(e, "move")) && !isPrimaryTouch(e)) return firstTouch = _isPointerType ? e : e.touches[0] // 取消長按事件,都移動了,當然不是長按了 cancelLongTap() // 終點坐標 (x2, y2) touch.x2 = firstTouch.pageX touch.y2 = firstTouch.pageY // 分別記錄X軸和Y軸的變化量 deltaX += Math.abs(touch.x1 - touch.x2) deltaY += Math.abs(touch.y1 - touch.y2) })
手指移動的時候,做了三件事情。
取消長按事件
記錄終點坐標
記錄x軸和y軸的移動變化量
touchend.on("touchend MSPointerUp pointerup", function(e){ if((_isPointerType = isPointerEventType(e, "up")) && !isPrimaryTouch(e)) return // 取消長按事件 cancelLongTap() // 滑動事件,只要X軸或者Y軸的起始點和終點的距離超過30則認為是滑動,并觸發滑動(swip)事件, // 緊接著馬上觸發對應方向的swip事件(swipLeft, swipRight, swipUp, swipDown) // swipe if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) || (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) swipeTimeout = setTimeout(function() { if (touch.el){ touch.el.trigger("swipe") touch.el.trigger("swipe" + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2))) } touch = {} }, 0) // touch對象的last屬性,在touchstart事件中添加,所以觸發了start事件便會存在 // normal tap else if ("last" in touch) // don"t fire tap when delta position changed by more than 30 pixels, // for instance when moving to a point and back to origin // 只有當X軸和Y軸的變化量都小于30的時候,才認為有可能觸發tap事件 if (deltaX < 30 && deltaY < 30) { // delay by one tick so we can cancel the "tap" event if "scroll" fires // ("tap" fires before "scroll") tapTimeout = setTimeout(function() { // trigger universal "tap" with the option to cancelTouch() // (cancelTouch cancels processing of single vs double taps for faster "tap" response) // 創建自定義事件 var event = $.Event("tap") // 往自定義事件中添加cancelTouch回調函數,這樣使用者可以通過該方法取消所有的事件 event.cancelTouch = cancelAll // [by paper] fix -> "TypeError: "undefined" is not an object (evaluating "touch.el.trigger"), when double tap // 當目標元素存在,觸發tap自定義事件 if (touch.el) touch.el.trigger(event) // trigger double tap immediately // 如果是doubleTap事件,則觸發之,并清除touch if (touch.isDoubleTap) { if (touch.el) touch.el.trigger("doubleTap") touch = {} } // trigger single tap after 250ms of inactivity // 否則在250毫秒之后。觸發單擊事件 else { touchTimeout = setTimeout(function(){ touchTimeout = null if (touch.el) touch.el.trigger("singleTap") touch = {} }, 250) } }, 0) } else { // 不是tap相關的事件 touch = {} } // 最后將變化量信息清空 deltaX = deltaY = 0 })
touchend事件觸發時,相應的注釋都在上面了,但是我們來分解一下這段代碼。
swip事件相關
if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) || (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) swipeTimeout = setTimeout(function() { if (touch.el){ touch.el.trigger("swipe") touch.el.trigger("swipe" + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2))) } touch = {} }, 0)
手指離開后,通過判斷x軸或者y軸的位移,只要其中一個跨度大于30便會觸發swip及其對應方向的事件。
tap,doubleTap,singleTap
這三個事件可能觸發的前提條件是touch對象中還存在last屬性,從touchstart事件處理程序中知道last在其中記錄,而在touchend之前被清除的時機是長按事件被觸發longTap,取消所有事件被調用cancelAll
if (deltaX < 30 && deltaY < 30) { // delay by one tick so we can cancel the "tap" event if "scroll" fires // ("tap" fires before "scroll") tapTimeout = setTimeout(function() { // trigger universal "tap" with the option to cancelTouch() // (cancelTouch cancels processing of single vs double taps for faster "tap" response) var event = $.Event("tap") event.cancelTouch = cancelAll // [by paper] fix -> "TypeError: "undefined" is not an object (evaluating "touch.el.trigger"), when double tap if (touch.el) touch.el.trigger(event) } }
只有當x軸和y軸的變化量都小于30的時候才會觸發tap事件,注意在觸發tap事件之前,Zepto還將往事件對象上添加了cancelTouch屬性,對應的也就是cancelAll方法,即你可以通過他取消所有的touch相關事件。
// trigger double tap immediately if (touch.isDoubleTap) { if (touch.el) touch.el.trigger("doubleTap") touch = {} } // trigger single tap after 250ms of inactivity else { touchTimeout = setTimeout(function(){ touchTimeout = null if (touch.el) touch.el.trigger("singleTap") touch = {} }, 250) }
在發生觸發tap事件之后,如果是doubleTap,則會緊接著觸發doubleTap事件,否則250毫秒之后觸發singleTap事件,并且都會講touch對象置為空對象,以便下次使用
// 最后將變化量信息清空 deltaX = deltaY = 0touchcancel
.on("touchcancel MSPointerCancel pointercancel", cancelAll)
當touchcancel被觸發的時候,取消所有的事件。
scroll$(window).on("scroll", cancelAll)
當滾動事件被觸發的時候,取消所有的事件(這里有些不解,滾動事件觸發,完全有可能是要觸發tap或者swip等事件啊)。
結尾最后說一個面試中經常會問的問題,touch擊穿現象。如果對此有興趣可以查看移動端click延遲及zepto的穿透現象, [新年第一發--深入不淺出zepto的Tap擊穿問題參考
](https://zhuanlan.zhihu.com/p/...
移動端click延遲及zepto的穿透現象
[新年第一發--深入不淺出zepto的Tap擊穿問題
](https://zhuanlan.zhihu.com/p/...
讀Zepto源碼之Touch模塊
pointerType
[[翻譯]整合鼠標、觸摸 和觸控筆事件的Html5 Pointer Event Api](https://juejin.im/post/594e06...
文章目錄
touch.js
如何實現swipe、tap、longTap等自定義事件 (2017-12-22)
ie.js
Zepto源碼分析之ie模塊(2017-11-03)
data.js
Zepto中數據緩存原理與實現(2017-10-03)
form.js
Zepto源碼分析之form模塊(2017-10-01)
zepto.js
這些Zepto中實用的方法集(2017-08-26)
Zepto核心模塊之工具方法拾遺 (2017-08-30)
看Zepto如何實現增刪改查DOM (2017-10-2)
Zepto這樣操作元素屬性(2017-11-13)
向Zepto學習關于"偏移"的那些事(2017-12-10)
event.js
mouseenter與mouseover為何這般糾纏不清?(2017-06-05)
向Zepto.js學習如何手動觸發DOM事件(2017-06-07)
誰說你只是"會用"jQuery?(2017-06-08)
ajax.js
原來你是這樣的jsonp(原理與具體實現細節)(2017-06-11)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93115.html
摘要:在觸發事件前,先將保存定時器的變量釋放,如果對象中存在,則觸發事件,保存的是最后觸摸的時間。如果有觸發的定時器,清除定時器即可阻止事件的觸發。其實就是清除所有相關的定時器,最后將對象設置為。進入時,立刻清除定時器的執行。 大家都知道,因為歷史原因,移動端上的點擊事件會有 300ms 左右的延遲,Zepto 的 touch 模塊解決的就是移動端點擊延遲的問題,同時也提供了滑動的 swip...
摘要:微信小程序手勢事件庫由于微信小程序只能夠支持時間,對于比較復雜的事件只能自己實現因此自己對庫進行了改造,開發了時候微信小程序手勢事件庫使用進行編寫手勢庫支持以下事件倉庫地址點我點我使用由于和微信小程序強綁定,因此需要在元素上面綁定好所有的事 WxTouchEvent 微信小程序手勢事件庫 由于微信小程序只能夠支持 tap,longtap,touchstart,touchmove,tou...
摘要:移動端兼容端手勢操作庫,支持的事件單擊雙擊長按滑動開始滑動結束滑動向左劃向右劃向上劃向下劃提供的接口配置項單擊事件允許的滑動距離雙擊事件的延時時長兩次單擊的最大時間間隔長按事件的最小時長觸發方向滑動的最小距離觸發方向滑動允許的最長時長以上是 mTouch mTouch移動端 ( 兼容pc端) 手勢操作庫,view on github 支持的事件: tap 單擊 doubletap ...
閱讀 2232·2021-09-22 15:25
閱讀 3617·2019-08-30 12:48
閱讀 2205·2019-08-30 11:25
閱讀 2338·2019-08-30 11:05
閱讀 725·2019-08-29 17:28
閱讀 3284·2019-08-26 12:16
閱讀 2608·2019-08-26 11:31
閱讀 1701·2019-08-23 17:08