Zepto tap事件“穿透”、“點透”問題研究
首先,什麼是zepto tap事件穿透?
tap事件穿透就是,有多個層級上有繫結事件,最上層的綁定了tap事件,下層綁定了click事件,在執行完上層事件後會觸發下層事件,進而出現事件穿透。如果下層是input標籤,必穿透。
究其原因:
是因為zepto實現tap事件是冒泡到document上時才觸發的,也就是tap事件是繫結在document上,而click事件有延時執行。
下面我們貼下zepto.1.1.6 tap事件的原始碼:
;(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)
詳細分析:
根據zepto原始碼,我們很清楚地知道tap事件是通過繫結在document上的touch事件來模擬的。所以使用者在點選tap事件(touchstart、touchend)時需要冒泡到document上才會觸發。然而使用者在touchstart和touchend時會觸發click事件,但是此時click事件處於延時300ms,如果在這300ms之內tap事件已經完成,將上層元素刪除或隱藏。在300ms到來之際,根據click事件的原則(當click事件的元素處於最上層時會處於click事件,所以有的時候錯誤的z-index的設定導致無法觸發click事件),下層事件被執行,出現穿透現象。讓下層是input元素,即使沒有繫結click事件,由於其預設聚焦彈出鍵盤,穿透現象尤為嚴重。
解決方案:
1、github上有個fastclick外掛,用來規避click事件的延時執行。引入檔案後新增如下程式碼,並用click替代可能會導致穿透的tap事件元素。github地址:https://github.com/ftlabs/fastclick
$(function(){ new FastClick(document.body); })
2、監聽touchend事件來替代tap,或者touchstart,並阻止冒泡
$("#close").on("touchend",function(e){
$("#alertBox").hide();
e.preventDefault();
});
3、使用css3的pointer-events : true 和 pointer-events : none交替使用對下層元素設定,阻止觸發click事件。
4、延時消失上層元素,使得無法觸發下層click事件,儘量在延時350ms以上(本人在ios9.2上微信6.3.15上測試過)。不過這樣稍微有些體驗不好,我們可以使用css3過度來改善體驗。
setTimeout(function(){ $(#alertBox).hide(); } , 350 );
5、終極方案:用click替代所有tap。由於click的延時,導致體驗問題,最好加上fastclick外掛。
下面是我寫了個簡單的例子:可以用手機訪問http://property.pingan.com/app/test/jltest/tap-through.html?a=1
通過例子,我們可以很明顯的看到事件穿透後底層的button有按下的效果。在頻繁的測試過程中,由於微信會快取頁面導致無法看到即時修改的內容,我們可以通過給url增加一些沒用的引數如a=1,這樣瀏覽器就會重新載入。
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
<title>test-tap-through</title>
<script src="js/zepto.min.js" charset="utf-8"></script>
<style media="screen">
body{
margin: 0;
padding: 0;
}
.test1,.test2{
position: relative;
}
.button{
width: 90%;
height: 75px;
background-color: #00ffff;
margin: 5%;
line-height: 75px;
text-align: center;
font-size: 40px;
}
.box{
position: absolute;
top:0;
left: 0;
width: 50%;
height: 200px;
background-color: #ff00ff;
margin: 5%;
line-height: 100px;
text-align: center;
font-size: 40px;
z-index: 100;
}
</style>
</head>
<body>
<div class="test1">
<input class="button" type="button" id="button1" value="button1">
<input class="button" type="button" id="button2" value="button2">
<div class="box" id="box1" style="display:none">box1</div>
<div class="box" id="box2" style="display:none">box2</div>
</div>
<div class="test2">
<input class="button" type="button" id="button3" value="button3">
<input class="button" type="button" id="button4" value="button4">
<div class="box" id="box3" style="display:none">box3</div>
<div class="box" id="box4" style="display:none">box4</div>
</div>
</body>
<script type="text/javascript">
$("#button1").click(function(){
$("#box2").hide();
$("#box1").show();
});
$("#button2").click(function(){
$("#box1").hide();
$("#box2").show();
});
$("#box2").tap(function(){
$("#box2").hide();
});
$("#box1").tap(function(){
$("#box1").hide();
});
$("#button3").click(function(){
$("#box4").hide();
$("#box3").show();
});
$("#button4").click(function(){
$("#box3").hide();
$("#box4").show();
});
$("#box3").tap(function(){
setTimeout(function(){$("#box3").hide();},350);
});
$("#box4").tap(function(){
setTimeout(function(){$("#box4").hide();},350);
});
</script>
</html>