目的:記錄 Zepto.js touch模塊 源碼閱讀
源碼:

// Zepto.js // (c) 2010-2015 Thomas Fuchs // Zepto.js may be freely distributed under the MIT license. ; (function($) { var touch = {}, touchTimeout, tapTimeout, swipeTimeout, longTapTimeout, longTapDelay = 750, gesture function swipeDirection(x1, x2, y1, y2) { 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) { touch.el.trigger('longTap') 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 = {} } function isPrimaryTouch(event) { return (event.pointerType == 'touch' || event.pointerType == event.MSPOINTER_TYPE_TOUCH) && event.isPrimary } function isPointerEventType(e, type) { return (e.type == 'pointer' + type || e.type.toLowerCase() == 'mspointer' + type) } $(document).ready(function() { var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType if ('MSGesture' in window) { gesture = new MSGesture() gesture.target = document.body } $(document) .bind('MSGestureEnd', function(e) { var swipeDirectionFromVelocity = e.velocityX > 1 ? 'Right' : e.velocityX < -1 ? 'Left' : e.velocityY > 1 ? 'Down' : e.velocityY < -1 ? 'Up' : null; if (swipeDirectionFromVelocity) { touch.el.trigger('swipe') touch.el.trigger('swipe' + swipeDirectionFromVelocity) } }) .on('touchstart MSPointerDown pointerdown', function(e) { if ((_isPointerType = isPointerEventType(e, 'down')) && !isPrimaryTouch(e)) return firstTouch = _isPointerType ? e : e.touches[0] 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 = $('tagName' in firstTouch.target ? firstTouch.target : firstTouch.target.parentNode) touchTimeout && clearTimeout(touchTimeout) touch.x1 = firstTouch.pageX touch.y1 = firstTouch.pageY if (delta > 0 && delta <= 250) touch.isDoubleTap = true touch.last = now longTapTimeout = setTimeout(longTap, longTapDelay) // adds the current touch contact for IE gesture recognition if (gesture && _isPointerType) gesture.addPointer(e.pointerId); }) .on('touchmove MSPointerMove pointermove', function(e) { if ((_isPointerType = isPointerEventType(e, 'move')) && !isPrimaryTouch(e)) return firstTouch = _isPointerType ? e : e.touches[0] cancelLongTap() touch.x2 = firstTouch.pageX touch.y2 = firstTouch.pageY deltaX += Math.abs(touch.x1 - touch.x2) deltaY += Math.abs(touch.y1 - touch.y2) }) .on('touchend MSPointerUp pointerup', function(e) { if ((_isPointerType = isPointerEventType(e, 'up')) && !isPrimaryTouch(e)) return cancelLongTap() // swipe if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) || (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) swipeTimeout = setTimeout(function() { touch.el.trigger('swipe') touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2))) touch = {} }, 0) // 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 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 touch.el.trigger(event) // 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) } }, 0) } else { touch = {} } deltaX = deltaY = 0 }) // when the browser window loses focus, // for example when a modal dialog is shown, // cancel all ongoing events .on('touchcancel MSPointerCancel pointercancel', cancelAll) // scrolling the window indicates intention of the user // to scroll, not tap or swipe, so cancel all ongoing events $(window).on('scroll', cancelAll) }) ; ['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap' ].forEach(function(eventName) { $.fn[eventName] = function(callback) { return this.on(eventName, callback) } }) })(Zepto)
分析:
var now, delta, touch = {}; $(document) .on('touchstart', startListener) .on('touchmove', moveListener) .on('touchend', endListener);
1、是單擊還是雙擊
function startListener(e){ now = Date.now(); delta = now - (touch.last || now); // 手指連續輕觸兩次,時間間隔大於0,小於等於.25s,則為雙擊,反之單擊 if ( delta > 0 && delta <= 250 ) { touch.isDoubleTap = true; } touch.last = now; }
2、處理手指長按
var longTapTimeout, longTapDelay = 750; function longTap() { longTapTimeout = null if (touch.last) { touch.el.trigger('longTap') touch = {} } } function cancelLongTap() { if (longTapTimeout) clearTimeout(longTapTimeout) longTapTimeout = null } function startListener(e){ // 默認就是長按,如果手指未移動和離開,超過.75s就觸發longTap longTapTimeout = setTimeout(longTap, longTapDelay) } function moveListener(e){ // 如果手指輕觸屏幕后未超過.75s,則取消手指長按監聽 longTapTimeout = setTimeout(longTap, longTapDelay) } function endListener(e){ // 如果手指輕觸屏幕后未超過.75s,則取消手指長按監聽 longTapTimeout = setTimeout(longTap, longTapDelay) }
3、是滑動(swipe)還是輕觸(tap)
// 如果手指移動屏幕超過30像素,則觸發相應的滑動事件,swipeLeft, swipeRight, swipeUp, swipeDown function endListener(e){ // swipe if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) || (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) { swipeTimeout = setTimeout(function() { touch.el.trigger('swipe') touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2))) touch = {} }, 0); } else { // handle tap // 關於處理tap事件,請看第四點 } }
4、輕觸 tap, singleTap, doubleTap
4.1、何時觸發 tap ?
條件1:手指移動不超過30像素
if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) || (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) { // swipe } else { // tap }
條件2:依據條件1,基本上可以觸發tap了,但是還考慮了另一種情況,手指滑動屏幕后又滑動到起始點,那么:
!Math.abs(touch.x1 - touch.x2) > 30) === !Math.abs(touch.y1 - touch.y2) > 30) === true;
為了不觸發tap事件,這里又加了條件限制,理解這點很重要
if (deltaX < 30 && deltaY < 30) { // handle tap }
注意:
deltaX !== Math.abs(touch.x1 - touch.x2);
deltaY !== Math.abs(touch.y1 - touch.y2);
請看 moveListener 中的代碼:
function moveListener(e){ // ... touch.x2 = firstTouch.pageX touch.y2 = firstTouch.pageY deltaX += Math.abs(touch.x1 - touch.x2) deltaY += Math.abs(touch.y1 - touch.y2) }
例: deltaX的計算,你懂得...
if ( Math.abs(touch.x1 - touch.x2) === 10 ) { deltaX = 10 + 9 + 8 + ... + 0; }
4.2、處理tap,doubleTap,singleTap三者之間的關系
function cancelAll() { if (touchTimeout) clearTimeout(touchTimeout) if (tapTimeout) clearTimeout(tapTimeout) if (swipeTimeout) clearTimeout(swipeTimeout) if (longTapTimeout) clearTimeout(longTapTimeout) touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null // 這句很重要,將影響所有需要對touch對象屬性判斷的語句 touch = {} } function endListener(e){ tapTimeout = setTimeout(function() { var event = $.Event('tap') // tap事件對象event可以取消后續綁定的doubleTap, singleTap處理器 event.cancelTouch = cancelAll touch.el.trigger(event) // 立即觸發雙擊事件 if (touch.isDoubleTap) { if (touch.el) touch.el.trigger('doubleTap') touch = {} } // 定時.25s后再觸發單擊事件 else { touchTimeout = setTimeout(function() { touchTimeout = null if (touch.el) touch.el.trigger('singleTap') touch = {} }, 250) } }, 0) }
例如:如何在tap事件處理器中取消 doubleTap或singleTap事件監聽器
$('body') .on('tap', function(e){ console.log('tap'); // 執行下面語句將影響是否觸發綁定的 doubleTap或singleTap 處理器 e.cancelTouch(); }) .on('doubleTap', function(e){ console.log('doubleTap'); }) .on('singleTap', function(e){ console.log('singleTap'); }); // 'tap'
5、兼容指針事件系統
// 判斷是否是指針事件類型 function isPointerEventType(e, type) { return (e.type == 'pointer' + type || e.type.toLowerCase() == 'mspointer' + type) } // 判斷是否是第一個touch或pointer事件對象 function isPrimaryTouch(event) { return (event.pointerType == 'touch' || event.pointerType == event.MSPOINTER_TYPE_TOUCH) && event.isPrimary } // 如果是指針類型是 pointerdown 或 pointermove 或 pointerup 且 不是第一個touch 或 pointer 事件對象,返回空, // 直接屏蔽了第二個、第三...的觸摸處理 if ((_isPointerType = isPointerEventType(e, 'down')) && !isPrimaryTouch(e)) return if ((_isPointerType = isPointerEventType(e, 'move')) && !isPrimaryTouch(e)) return if ((_isPointerType = isPointerEventType(e, 'up')) && !isPrimaryTouch(e)) return
6、快捷注冊事件
['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap' ].forEach(function(eventName) { $.fn[eventName] = function(callback) { return this.on(eventName, callback) } });
你可以用 on 方法注冊事件,也可以快捷注冊,下面兩種方式都是一樣的,類似jQuery用法
$('body').on('tap', function(){ console.log('body trigger tap event'); }); $('body').tap(function(){ console.log('body trigger tap event'); });
篇尾總結:
源碼中大部分代碼都已經解析完畢,如有不合理的地方,還請賜教,touch模塊的中所有的事件都支持冒泡,但是不會對原生的touch事件產生影響,另外所有元素綁定的事件都是在文檔document元素的touchend處理中觸發,
如果頁面中有一元素在原生touch事件中阻止了冒泡,那么頁面中所有元素注冊的 zepto touch事件都不會被觸發,慎重慎重...,至此完畢,感謝閱讀!!