【移動端兼容問題研究】javascript事件機制詳解(涉及移動兼容)


前言

這篇博客有點長,如果你是高手請您讀一讀,能對其中的一些誤點提出來,以免我誤人子弟,並且幫助我提高

如果你是javascript菜鳥,建議您好好讀一讀,真的理解下來會有不一樣的收獲

在下才疏學淺,文中難免會有不同程度的錯誤,請您指正留言 

PS:事件階段一節請看最新博客,之前理解有誤

javascript事件基礎

我們的網頁之所以豐富多彩並具有交互功能,是因為我們的javascript腳本語言,而javascript與HTML之間的交互又是通過事件機制實現的

所以,事件是javascript一大核心,深入了解事件機制在我們遇到較困難問題時候十分有幫助

所謂事件,就是網頁發生的一些瞬間(比如點擊、滑動),在這些瞬間我們使用事件監聽器(回調函數)去訂閱事件,在事件發生時候我們的回調函數就會觸發

觀察者模式的javascript事件機制的基石,這種異步事件編程模型,就是用戶產生特定的操作,瀏覽器就會產生特定的事件,我們若是訂閱了事件,回調就會觸發

好了,我們下面就來研究下javascript事件機制的幾個關鍵點。

事件捕獲/冒泡

網頁上的布局很復雜,我們對頁面的單一操作有可能產生預計以外的影響:

比如我點擊一個span,我可能就想點擊一個span,試試上他是先點擊document,然后點擊事件傳遞到span的,而且並不會在span停下,span有子元素就會繼續往下,最后會依次回傳至document,我們這里偷一張圖:

我們這里偷了一張圖,這張圖很好的說明了事件的傳播方式

事件冒泡即由最具體的元素(文檔嵌套最深節點)接收,然后逐步上傳至document

事件捕獲會由最先接收到事件的元素然后傳向最里邊(我們可以將元素想象成一個盒子裝一個盒子,而不是一個積木堆積)

這里我們進入dom事件流,這里我們詳細看看javascript事件的傳遞方式

DOM事件流

DOM2級事件規定事件包括三個階段:

① 事件捕獲階段

② 處於目標階段

③ 事件冒泡階段

這里說起來不太明顯,我們來一個例子吧:

http://sandbox.runjs.cn/show/l31ucooa

 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3     <title></title>
 4     <style type="text/css">
 5          #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
 6          #c { width: 100px; height: 100px; border: 1px solid red; }
 7     </style>
 8 </head>
 9 <body>
10     <div id="p">
11         parent
12         <div id="c">
13             child
14         </div>
15     </div>
16     <script type="text/javascript">
17         var p = document.getElementById('p'),
18             c = document.getElementById('c');
19         c.addEventListener('click', function () {
20             alert('子節點捕獲')
21         }, true);
22 
23         c.addEventListener('click', function () {
24             alert('子節點冒泡')
25         }, false);
26     </script>
27 </body>
28 </html>

 這個代碼比較簡單,我們主要點擊child即可,這里要證明的就是點擊事件是先捕獲再冒泡,所以我們這里來一個復雜點的關系:

http://sandbox.runjs.cn/show/ij4rih6x

 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3     <title></title>
 4     <style type="text/css">
 5          #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
 6          #c { width: 100px; height: 100px; border: 1px solid red; }
 7     </style>
 8 </head>
 9 <body>
10     <div id="p">
11         parent
12         <div id="c">
13             child
14         </div>
15     </div>
16     <script type="text/javascript">
17         var p = document.getElementById('p'),
18         c = document.getElementById('c');
19         c.addEventListener('click', function () {
20             alert('子節點捕獲')
21         }, true);
22 
23         c.addEventListener('click', function () {
24             alert('子節點冒泡')
25         }, false);
26 
27         p.addEventListener('click', function () {
28             alert('父節點捕獲')
29         }, true);
30 
31         p.addEventListener('click', function () {
32             alert('父節點冒泡')
33         }, false);
34     </script>
35 </body>
36 </html>

現在這個家伙就比較實在了,不注意就容易暈的,我們來稍微理一理:

① 點擊parent,事件首先在document上然后parent捕獲到事件,處於目標階段然后event.target也等於parent,所以觸發捕獲事件

由於target與currentTarget相等了,所以認為到底了,開始冒泡,執行冒泡事件

② 點擊child情況有所不同,事件由document傳向parent執行事件,然后傳向child最后開始冒泡,所以執行順序各位一定要清晰

至此,我們事件傳輸結束,下面開始研究事件參數 

事件對象

所謂事件對象,是與特定對象相關,並且包含該事件詳細信息的對象。

事件對象作為參數傳遞給事件處理程序(IE8之前通過window.event獲得),所有事件對象都有事件類型type與事件目標target(IE8之前的srcElement我們不關注了)

各個事件的事件參數不一樣,比如鼠標事件就會有相關坐標,包含和創建他的特定事件有關的屬性和方法,觸發的事件不一樣,參數也不一樣(比如鼠標事件就會有坐標信息),我們這里題幾個較重要的

PS:以下的兄弟全部是只讀的,所以不要妄想去隨意更改,IE之前的問題我們就不關注了

bubbles

表明事件是否冒泡

cancelable

表明是否可以取消事件的默認行為

currentTarget

某事件處理程序當前正在處理的那個元素

defaultPrevented

為true表明已經調用了preventDefault(DOM3新增)

eventPhase

調用事件處理程序的階段:1 捕獲;2 處於階段;3 冒泡階段

這個屬性的變化需要在斷點中查看,不然你看到的總是0

target

事件目標(綁定事件那個dom)

trusted

true表明是系統的,false為開發人員自定義的(DOM3新增)

type

事件類型

view

與事件關聯的抽象視圖,發生事件的window對象

preventDefault

取消事件默認行為,cancelable是true時可以使用

stopPropagation

取消事件捕獲/冒泡,bubbles為true才能使用

stopImmediatePropagation

取消事件進一步冒泡,並且組織任何事件處理程序被調用(DOM3新增)

在我們的事件處理內部,this與currentTarget相同

createEvent

可以在document對象上使用createEvent創建一個event對象

     DOM3新增以下事件:
     UIEvents
     MouseEvents
     MutationEvents,一般化dom變動
     HTMLEvents一般dom事件

創建鼠標事件時需要創建的事件對象需要提供指定的信息(鼠標位置信息),我們這里提供以下參數:

 1 var type = 'click'; //要觸發的事件類型
 2 var bubbles = true; //事件是否可以冒泡
 3 var cancelable = true; //事件是否可以阻止瀏覽器默認事件
 4 var view = document.defaultView; //與事件關聯的視圖,該屬性默認即可,不管
 5 var detail = 0;
 6 var screenX = 0;
 7 var screenY = 0;
 8 var clientX = 0;
 9 var clientY = 0;
10 var ctrlKey = false; //是否按下ctrl
11 var altKey = false; //是否按下alt
12 var shiftKey = false;
13 var metaKey = false;
14 var button = 0;//表示按下哪一個鼠標鍵
15 var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的對象
16 
17 var event = document.createEvent('MouseEvents');
18 event.initMouseEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, 
19 ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);

如此,我們就自己創建了一個event對象,然后可以傳給我們自己創建的事件,這個知識點,我們下面再說

PS:值得注意的是,我們自己創建的event對象可以有一點不一樣的東西,比如我們的事件對象可能多了一個這種屬性:

event.flag = '葉小釵'

事件模擬

事件模擬是javascript事件機制中相當有用的功能,理解事件模擬與善用事件模擬是判別一個前端的重要依據,所以各位一定要深入理解(我理解較水)

事件一般是由用戶操作觸發,其實javascript也是可以觸發的,比較重要的是,javascript的觸發事件還會冒泡哦!!!

意思就是,javascript觸發的事件與瀏覽器本身觸發其實是一樣的(並不完全一致)

如此,我們這里來通過鍵盤事件觸發剛剛的點擊事件吧,我們這里點擊鍵盤便觸發child的點擊,看看他的表現如何

PS:由於是鍵盤觸發,便不具有相關參數了,我們可以捕捉event參數,這對我們隊事件傳輸的理解有莫大的幫助:

我們這里先創建事件參數,然后給鍵盤注冊事件,在點擊鍵盤時候便觸發child的點擊事件,各位試試看:

PS:這個可能需要打開網頁點擊空格測試了

http://sandbox.runjs.cn/show/pesvelp1

 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3     <title></title>
 4     <style type="text/css">
 5          #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
 6          #c { width: 100px; height: 100px; border: 1px solid red; }
 7     </style>
 8 </head>
 9 <body>
10     <div id="p">
11         parent
12         <div id="c">
13             child
14         </div>
15     </div>
16     <script type="text/javascript">
17         alert = function (msg) {
18             console.log(msg);
19         }
20 
21         var p = document.getElementById('p'),
22         c = document.getElementById('c');
23         c.addEventListener('click', function (e) {
24             console.log(e);
25             alert('子節點捕獲')
26         }, true);
27         c.addEventListener('click', function (e) {
28             console.log(e);
29             alert('子節點冒泡')
30         }, false);
31 
32         p.addEventListener('click', function (e) {
33             console.log(e);
34             alert('父節點捕獲')
35         }, true);
36 
37         p.addEventListener('click', function (e) {
38             console.log(e);
39             alert('父節點冒泡')
40         }, false);
41 
42         document.addEventListener('keydown', function (e) {
43             if (e.keyCode == '32') {
44                 var type = 'click'; //要觸發的事件類型
45                 var bubbles = true; //事件是否可以冒泡
46                 var cancelable = true; //事件是否可以阻止瀏覽器默認事件
47                 var view = document.defaultView; //與事件關聯的視圖,該屬性默認即可,不管
48                 var detail = 0;
49                 var screenX = 0;
50                 var screenY = 0;
51                 var clientX = 0;
52                 var clientY = 0;
53                 var ctrlKey = false; //是否按下ctrl
54                 var altKey = false; //是否按下alt
55                 var shiftKey = false;
56                 var metaKey = false;
57                 var button = 0; //表示按下哪一個鼠標鍵
58                 var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的對象
59                 var event = document.createEvent('Events');
60                 event.myFlag = '葉小釵';
61                 event.initEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY,
62 ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
63                 
64                 console.log(event);
65                 c.dispatchEvent(event);
66             }
67         }, false);
68     </script>
69 </body>
70 </html>

各位,這里看到了與之前的相同或者不同嗎???這些都是很關鍵的哦,其實主要不同就是我們的事件參數沒了鼠標位置,多了一個屬性:

 

這里有兩點容易讓各位造成錯覺:

① firefox並不會將myFlag顯示到console下面

② chrome如果使用原生alert會阻止第一次父元素捕獲,所以各位一定要注意

然后這里還有一個小小知識點:

使用dom.dispatchEvent(event)觸發模擬事件

移動端響應速度

有了以上知識點,其實對PC端來說基本夠用了,如果再稍微研究下jquery源碼就善莫大焉了,但是在移動端卻有所不同,我們這里還得來理一理

PS:我這里主要針對點擊事件

PC與移動端鼠標事件差異

首先,在移動端mouse事件好像就有點不那么適用了,倒不是說touch事件要比mouse事件好,其實他們底層原理相距不大,主要不同點就是:

移動端會多點觸屏

多點觸屏就帶來了事件對象參數的差異,比如說:

 

changedTouches/touches/targetTouches

touches:為屏幕上所有手指的信息

PS:因為手機屏幕支持多點觸屏,所以這里的參數就與手機有所不同

targetTouches:手指在目標區域的手指信息

changedTouches:最近一次觸發該事件的手指信息

比如兩個手指同時觸發事件,2個手指都在區域內,則容量為2,如果是先后離開的的話,就會先觸發一次再觸發一次,這里的length就是1,只統計最新的

PS:一般changedTouches的length都是1

touchend時,touches與targetTouches信息會被刪除,changedTouches保存的最后一次的信息,最好用於計算手指信息

這里要使用哪個數據各位自己看着辦吧,我也不是十分清晰(我這里還是使用changedTouches吧)

以上就是mouse與touch主要不同點,但這些並不是太影響我們的操作,因為到現在為止,我們一般還是使用的是單擊

小貼士

國內SPA網站模式較少,目前為止還是以單個網頁為主,spa模式對javascript技術要求較高不說,首次加載量大也是不可避免的問題

加之移動端設備今年才普及,而且各自爭奪領地、爭奪入口,還有其他原因,反正現況是有時做移動端的兼容比做IE的兼容還難

就拿簡單的CSS3動畫來說,在ios下就有閃動現象,而且還是iPhone4s,就現今更新換代來說,此種情況並不會得到明顯好轉,而且CSS3動畫狀態保存問題亦是一大難題

另外,網頁想要檢測手機是否安裝APP也是有很大缺陷,移動端的fixed更不要說,這些問題都需要我們乃至開發商解決

PS:這里扯得有點遠,我們繼續下面的話題

touch與click響應速度問題

click本身在移動端響應是沒有問題的,但是我們點擊下來300ms 的延遲卻是事實,這種事實造成的原因就是

手機需要知道你是不是想雙擊放大網頁內容

所以click點擊響應慢,而touch卻不會有這樣的限制,於是移動端的touch相當受歡迎,至於鼠標慢,他究竟有多慢,我們來看看:

現在我們在手機上同時觸發兩者事件看看區別:

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 2 <html xmlns="http://www.w3.org/1999/xhtml">
 3 <head>
 4     <title></title>
 5     <script id="others_zepto_10rc1" type="text/javascript" class="library" src="http://sandbox.runjs.cn/js/sandbox/other/zepto.min.js"></script>
 6 </head>
 7 <body>
 8     <div id="d" style="width: 100px; height: 100px; border: 1px solid black;">
 9     </div>
10 </body>
11 <script type="text/javascript">
12     var startTime;
13     var log = function (msg) {
14         var div = $('<div></div>');
15         div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg)
16         $('body').append(div);
17 
18     };
19     var touchStart = function () {
20         startTime = new Date().getTime();
21         log('touchStart');
22     };
23     var touchEnd = function () {
24         log('touchEnd');
25 
26     };
27     var mouseDown = function () {
28         log('mouseDown');
29     };
30     var mouseClick = function () {
31         log('mouseClick');
32     };
33     var mouseUp = function () {
34         log('mouseUp');
35 
36     };
37     var d = $('#d');
38     d.bind('mousedown', mouseDown);
39     d.bind('click', mouseClick);
40     d.bind('mouseup', mouseUp);
41     d.bind('touchstart', touchStart);
42     d.bind('touchend', touchEnd);
43 </script>
44 </html>
View Code

測試地址:(使用手機)

http://sandbox.runjs.cn/show/ey54cgqf

此處手機與電腦有非常大的區別!!!

結論

不要同時給document綁定鼠標與touch事件

document.addEventListener('mousedown', mouseDown);
document.addEventListener('click', mouseClick);
document.addEventListener('mouseup', mouseUp);
document.addEventListener('touchstart', touchStart);
document.addEventListener('touchend', touchEnd);

這個樣子,在手機上不會觸發click事件,click事件要綁定到具體元素

PS:此處在ios與android上有不一樣的表現,我們后面會涉及

手機上mousedown響應慢

經過測試,電腦上touch與click事件的差距不大,但是手機上,當我們手觸碰屏幕時,要過300ms左右才會觸發mousedown事件

所以click事件在手機上響應就是慢一拍,我們前面說過為什么click慢了

數據說明

可以看到,在手機上使用click事件其實對用戶體驗並不好,所以我們可能會逐步使用touch事件,但是真正操作時候你就會知道click的好

好了,此處內容暫時到這,我們先看看zepto的事件機制,下面會提到如何使用touch提升click的響應速度

zepto事件機制

zepto是以輕巧的dom庫,這家伙可以說是jquery的html5版本,而且在移動端有媲美jqueryPC端的趨勢,如果jquery不予以回擊,可能移動端的份額就不行了

我們這里不討論zepto的其他地方了,我們單獨講他的事件相關提出來看看 

注冊/注銷事件

事件注冊是我們項目開發中用得最多的一塊,我們一般會使用以下幾種方式綁定事件:

el.on(type, function () {});//力推

el.bind(function(){});

el.click(function() {});//不推薦

dom.onclick = function() {};//需要淘汰

以上幾種方式用得較多,最后一種在真實的項目中基本不用,單數第二也極少使用,那么zepto內部是怎么實現的呢?

PS:這里,我就不詳細說明zepto事件機制了,這里點一下即可

zepto事件機制其實比較簡單,他具體流程如下:

① 事件注冊時在全局保存事件句柄(handlers = {})

② 提供全局的事件注冊點

 1 //給元素綁定監聽事件,可同時綁定多個事件類型,如['click','mouseover','mouseout'],也可以是'click mouseover mouseout'
 2 function add(element, events, fn, selector, getDelegate, capture) {
 3     var id = zid(element),
 4       set = (handlers[id] || (handlers[id] = [])) //元素上已經綁定的所有事件處理函數
 5     eachEvent(events, fn, function (event, fn) {
 6         var handler = parse(event)
 7         //保存fn,下面為了處理mouseenter, mouseleave時,對fn進行了修改
 8         handler.fn = fn
 9         handler.sel = selector
10         // 模仿 mouseenter, mouseleave
11         if (handler.e in hover) fn = function (e) {
12             /* 
13             relatedTarget為事件相關對象,只有在mouseover和mouseout事件時才有值
14             mouseover時表示的是鼠標移出的那個對象,mouseout時表示的是鼠標移入的那個對象
15             當related不存在,表示事件不是mouseover或者mouseout,mouseover時!$.contains(this, related)當相關對象不在事件對象內
16             且related !== this相關對象不是事件對象時,表示鼠標已經從事件對象外部移入到了對象本身,這個時間是要執行處理函數的
17             當鼠標從事件對象上移入到子節點的時候related就等於this了,且!$.contains(this, related)也不成立,這個時間是不需要執行處理函數的
18             */
19             var related = e.relatedTarget
20             if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments)
21         }
22         //事件委托
23         handler.del = getDelegate && getDelegate(fn, event)
24         var callback = handler.del || fn
25         handler.proxy = function (e) {
26             var result = callback.apply(element, [e].concat(e.data))
27             //當事件處理函數返回false時,阻止默認操作和冒泡
28             if (result === false) e.preventDefault(), e.stopPropagation()
29             return result
30         }
31         //設置處理函數的在函數集中的位置
32         handler.i = set.length
33         //將函數存入函數集中
34         set.push(handler)
35         element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
36     })
37 }
View Code

③ 提供全局的事件注銷點

 1 $.fn.off = function (event, selector, callback) {
 2     return !selector || $.isFunction(selector) ? this.unbind(event, selector || callback) : this.undelegate(selector, event, callback)
 3 }
 4 
 5 $.fn.unbind = function (event, callback) {
 6     return this.each(function () {
 7         remove(this, event, callback)
 8     })
 9 }
10 
11 function remove(element, events, fn, selector, capture) {
12     var id = zid(element)
13     eachEvent(events || '', fn, function (event, fn) {
14         findHandlers(element, event, fn, selector).forEach(function (handler) {
15             delete handlers[id][handler.i]
16             element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
17         })
18     })
19 }
View Code

④ 提供簡便寫法

$.fn.click = function (fn) {
    this.bind('click', callback)
}

如果需要詳細了解的朋友請看此篇博客:

http://www.cnblogs.com/yexiaochai/p/3448500.html

我這里就不詳細說明了,這里需要說明的是,zepto提供了兩個語法糖:

創建事件參數/觸發事件

這兩個方法,完全是我們上面代碼的縮寫,當然他更加健壯,我們后面就可以使用他了

 1 $.fn.trigger = function (event, data) {
 2     if (typeof event == 'string' || $.isPlainObject(event)) event = $.Event(event)
 3     fix(event)
 4     event.data = data
 5     return this.each(function () {
 6         // items in the collection might not be DOM elements
 7         // (todo: possibly support events on plain old objects)
 8         if ('dispatchEvent' in this) this.dispatchEvent(event)
 9     })
10 }
 1 specialEvents = {}
 2 specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'
 3 
 4 //根據參數創建一個event對象
 5 $.Event = function (type, props) {
 6     //當type是個對象時
 7     if (typeof type != 'string') props = type, type = props.type
 8     //創建一個event對象,如果是click,mouseover,mouseout時,創建的是MouseEvent,bubbles為是否冒泡
 9     var event = document.createEvent(specialEvents[type] || 'Events'),
10     bubbles = true
11     //確保bubbles的值為true或false,並將props參數的屬性擴展到新創建的event對象上
12     if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
13     //初始化event對象,type為事件類型,如click,bubbles為是否冒泡,第三個參數表示是否可以用preventDefault方法來取消默認操作
14     event.initEvent(type, bubbles, true, null, null, null, null, null, null, null, null, null, null, null, null)
15     //添加isDefaultPrevented方法,event.defaultPrevented返回一個布爾值,表明當前事件的默認動作是否被取消,也就是是否執行了 event.preventDefault()方法.
16     event.isDefaultPrevented = function () {
17         return this.defaultPrevented
18     }
19     return event
20 }

zepto模擬tap事件

前面,我們提到過,我們移動端的點擊響應很慢,但是touch不會有這種限制,所以zepto為我們封裝了一個touch庫:

  1 (function ($) {
  2     var touch = {},
  3     touchTimeout, tapTimeout, swipeTimeout,
  4     longTapDelay = 750, longTapTimeout
  5 
  6     function parentIfText(node) {
  7         return 'tagName' in node ? node : node.parentNode
  8     }
  9 
 10     function swipeDirection(x1, x2, y1, y2) {
 11         var xDelta = Math.abs(x1 - x2), yDelta = Math.abs(y1 - y2)
 12         return xDelta >= yDelta ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
 13     }
 14 
 15     function longTap() {
 16         longTapTimeout = null
 17         if (touch.last) {
 18             touch.el.trigger('longTap')
 19             touch = {}
 20         }
 21     }
 22 
 23     function cancelLongTap() {
 24         if (longTapTimeout) clearTimeout(longTapTimeout)
 25         longTapTimeout = null
 26     }
 27 
 28     function cancelAll() {
 29         if (touchTimeout) clearTimeout(touchTimeout)
 30         if (tapTimeout) clearTimeout(tapTimeout)
 31         if (swipeTimeout) clearTimeout(swipeTimeout)
 32         if (longTapTimeout) clearTimeout(longTapTimeout)
 33         touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null
 34         touch = {}
 35     }
 36 
 37     $(document).ready(function () {
 38         var now, delta
 39 
 40         $(document.body)
 41       .bind('touchstart', function (e) {
 42           now = Date.now()
 43           delta = now - (touch.last || now)
 44           touch.el = $(parentIfText(e.touches[0].target))
 45           touchTimeout && clearTimeout(touchTimeout)
 46           touch.x1 = e.touches[0].pageX
 47           touch.y1 = e.touches[0].pageY
 48           if (delta > 0 && delta <= 250) touch.isDoubleTap = true
 49           touch.last = now
 50           longTapTimeout = setTimeout(longTap, longTapDelay)
 51       })
 52       .bind('touchmove', function (e) {
 53           cancelLongTap()
 54           touch.x2 = e.touches[0].pageX
 55           touch.y2 = e.touches[0].pageY
 56           if (Math.abs(touch.x1 - touch.x2) > 10)
 57               e.preventDefault()
 58       })
 59       .bind('touchend', function (e) {
 60           cancelLongTap()
 61 
 62           // swipe
 63           if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
 64             (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))
 65 
 66               swipeTimeout = setTimeout(function () {
 67                   touch.el.trigger('swipe')
 68                   touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
 69                   touch = {}
 70               }, 0)
 71 
 72           // normal tap
 73           else if ('last' in touch)
 74 
 75           // delay by one tick so we can cancel the 'tap' event if 'scroll' fires
 76           // ('tap' fires before 'scroll')
 77               tapTimeout = setTimeout(function () {
 78 
 79                   // trigger universal 'tap' with the option to cancelTouch()
 80                   // (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
 81                   var event = $.Event('tap')
 82                   event.cancelTouch = cancelAll
 83                   touch.el.trigger(event)
 84 
 85                   // trigger double tap immediately
 86                   if (touch.isDoubleTap) {
 87                       touch.el.trigger('doubleTap')
 88                       touch = {}
 89                   }
 90 
 91                   // trigger single tap after 250ms of inactivity
 92                   else {
 93                       touchTimeout = setTimeout(function () {
 94                           touchTimeout = null
 95                           touch.el.trigger('singleTap')
 96                           touch = {}
 97                       }, 250)
 98                   }
 99 
100               }, 0)
101 
102       })
103       .bind('touchcancel', cancelAll)
104 
105         $(window).bind('scroll', cancelAll)
106     })
107 
108   ; ['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function (m) {
109       $.fn[m] = function (callback) { return this.bind(m, callback) }
110   })
111 })(Zepto)
View Code

這個touch庫個人覺得寫得不行,雖然我寫不出來......

拋開其他東西,我們將其中點擊的核心給剝離出來

 1 tapTimeout = setTimeout(function () {
 2 
 3     // trigger universal 'tap' with the option to cancelTouch()
 4     // (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
 5     var event = $.Event('tap')
 6     event.cancelTouch = cancelAll
 7     touch.el.trigger(event)
 8 
 9     // trigger double tap immediately
10     if (touch.isDoubleTap) {
11         touch.el.trigger('doubleTap')
12         touch = {}
13     }
14 
15     // trigger single tap after 250ms of inactivity
16     else {
17         touchTimeout = setTimeout(function () {
18             touchTimeout = null
19             touch.el.trigger('singleTap')
20             touch = {}
21         }, 250)
22     }
23 
24 }, 0)

拋開其他問題,這里5-7行就是觸發TAP事件的核心,我們這里簡單說下流程:

① 我們在程序過程中為dom(包裝過的)tap事件(使用addEventListener方式注冊/zepto使用bind即可)

② 點擊目標元素,觸發document的touchstart與touchend,在end時候判斷是否為一次點擊事件(是否touchmove過多)

③ 如果是便觸發tap事件,於是我們的事件監聽器便會觸發了

以程序邏輯來說,他這個是沒問題的,他甚至考慮了雙擊與滑動事件,結合前面的知識點,這里應該很好理解

但就是這段代碼卻帶來了這樣那樣的問題,這些問題就是移動端兼容的血淚史,且聽我一一道來 

tap事件的問題一覽

body區域外點擊無效

我們看看我們的touch事件的綁定點

$(document.body).bind(......)

這段代碼本身沒什么問題,在PC端毫無問題,但就是這樣的代碼在手機端(多個手機/多個瀏覽器)下產生了一些區域不可點擊的現象

這其實不完全是兼容問題,是因為我們在手機端時候往往喜歡將body設置為height: 100%,於是這樣會產生一個問題

如果我們的view長度過程那么body區域事實上不會增加,所以我們點擊下面區域時候手機就不認為我們點擊的是body了......

這個BUG只能說無語,但是min-height雖然可以解決點擊BUG卻會帶來全局布局的問題,所以這個問題依然糾結

好在后面zepto意識到了這個問題將事件綁定改成了這個:

$(document).bind(......)

於是修復了這個問題

e.preventDefault失效(settimeout小貼士)

如果說第一個問題導致點是我們自己的布局的話,第二個問題的引發點我就覺得是開發人員的問題了

PS:zepto多數是抄寫jquery,touch是自己寫的,就是這個touch就搞了很多問題出來......

這里我們先不忙看tap代碼本身帶來的問題,我這里出一個題各位試試:

 1 var sum1 = 0, sum2 = 0, sum3 = 0; len = 2;
 2 var arr = [];
 3 for (var i = 0; i < len; i++) {
 4     arr.push(i)
 5 }
 6 for (var i = 0; i < len; i++) {
 7     setTimeout(function () {
 8         sum1 += arr[i];
 9     }, 0);
10 }
11 $.each(arr, function (i, v) {
12     setTimeout(function () {
13         sum2 += v;
14     }, 0);
15 });
16 for (var i = 0; i < len; i++) {
17     sum3++;
18 }
19 //sum3不管,答出len=2與len=200000時,sum1,sum2的值
20 console.log(sum1);
21 console.log(sum2);
22 console.log(sum3);

各位仔細觀察這個題,會有不一樣的感覺,在sum很大的時候第三個循環肯定會耗費超過一秒的時間

按道理說這里的sum1/sum2會進行相關計算,事實卻是:

settimeout將優先級降到了最低,他會在主干流程執行結束后才會執行

於是我們這里引出了一個非常有趣的問題,且看zepto源碼:

1 tapTimeout = setTimeout(function () {
2     var event = $.Event('tap')
3     event.cancelTouch = cancelAll
4     touch.el.trigger(event)
5 }, 0)

各位小伙伴,你認為我們在第四行后執行e.preventDefault()等操作會有效么???

或者說,我們在觸發tap事件后,會執行我們的回調函數我們在我們的回調函數中執行e.preventDefault()等操作會有效么???

各位小伙伴可以去試試,我這里就不做說明了

PS:標題好像泄露了我的行蹤......

點透問題

其實上面的問題是導致點透的因素之一,所謂點透就是:

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 2 <html xmlns="http://www.w3.org/1999/xhtml">
 3 <head>
 4     <title></title>
 5     <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
 6     <style>
 7         #list { border: 1px solid black; position: absolute; top: 0px; left: 10px; width: 200px; height: 100px; }
 8         #d { border: 1px solid black; height: 300px; width: 100%; }
 9     </style>
10 </head>
11 <body>
12     <div id="d">
13         <input type="text" id="input" style="width: 80px; height: 200px;" />
14         <div id="list">
15         </div>
16     </div>
17 </body>
18 <script src="res/libs/zepto.js" type="text/javascript"></script>
19 <script type="text/javascript">
20     window.log = function (msg) {
21         console.log(msg);
22         var div = $('#myMsg');
23         if (!div[0]) div = $('<div id="myMsg"></div>')
24         $('#d').append(div);
25         div.click(function () {
26             div.html('');
27         });
28         div.append($('<div>' + msg + '</div>'));
29     }
30     var list = $('#list');
31     var d = $('#d');
32     var input = $('#input');
33 
34     input.tap(function (e) {
35         input.val(new Date().getTime());
36     });
37 
38     list.tap(function (e) {
39         list.hide();
40         setTimeout(function () {
41             list.show();
42         }, 1000);
43 
44     });
45 
46     d.tap(function () {
47         log('div tap');
48     });
49 </script>
50 </html>
View Code

這個頁面有三個元素

① 父容器div,我們為他綁定了一個tap事件,會打印文字

② 在上的div,我們為其綁定了一個tap事件,點擊便消失

③ input,主要用於測試focus問題

現在開啟touch事件的情況下,我們點擊上面的div,他會消失,於是:

div消失會觸發div(list)的tap事件

div消失會觸發input獲取焦點事件

提示層一閃而過

表單提交頁,用戶提交時如果信息有誤,會彈出一個提示,並且為蒙版添加click的關閉事件

但是有tap在的情況效果就不一樣了,我們極有可能點擊提交,彈出提示層,觸發蒙版點擊事件,蒙版關閉!!!

input獲取焦點彈出鍵盤

我們可能遇到這種情況,我們在彈出層上做了一些操作后,點擊彈出層關閉彈出層,但是下面有一個input(div有事件也行)

於是觸發了div事件,於是input獲取了焦點,某明奇妙的彈出來鍵盤!!!

以上都屬於touch事件導致的點透現象,有問題就有解決方案,於是我們就來說針對zepto如何解決點透現象

神奇菊花解決點透

此方案只針對zepto的tap事件

其實並不是所有的tap事件都會產生點透,只不過在頁面切換/有彈出層時候容易出現這個問題

根據zepto事件注冊機制我這里做了一點修改便可以解決zepto點透問題:於是這里便引進一個新的事件lazyTap

lazyTap只是名字變了,其實他還是tap,首先我們說事件注冊:

el.on('lazyTap', function () {
});

如此我們就注冊了一個lazyTap事件,但是我們的zepto並不會因此而買賬,而且我也說了他就是tap事件,於是我們進入事件注冊入口:

 1 function add(element, events, fn, selector, getDelegate, capture) {
 2     var id = zid(element),
 3       set = (handlers[id] || (handlers[id] = [])) //元素上已經綁定的所有事件處理函數
 4     eachEvent(events, fn, function (event, fn) {
 5 if (event == 'lazyTap') event = 'tap'; 6 element.lazyTap = true;  7         var handler = parse(event)
 8         //保存fn,下面為了處理mouseenter, mouseleave時,對fn進行了修改
 9         handler.fn = fn
10         handler.sel = selector
11         // 模仿 mouseenter, mouseleave
12         if (handler.e in hover) fn = function (e) {
13             /* 
14             relatedTarget為事件相關對象,只有在mouseover和mouseout事件時才有值
15             mouseover時表示的是鼠標移出的那個對象,mouseout時表示的是鼠標移入的那個對象
16             當related不存在,表示事件不是mouseover或者mouseout,mouseover時!$.contains(this, related)當相關對象不在事件對象內
17             且related !== this相關對象不是事件對象時,表示鼠標已經從事件對象外部移入到了對象本身,這個時間是要執行處理函數的
18             當鼠標從事件對象上移入到子節點的時候related就等於this了,且!$.contains(this, related)也不成立,這個時間是不需要執行處理函數的
19             */
20             var related = e.relatedTarget
21             if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments)
22         }
23         //事件委托
24         handler.del = getDelegate && getDelegate(fn, event)
25         var callback = handler.del || fn
26         handler.proxy = function (e) {
27             var result = callback.apply(element, [e].concat(e.data))
28             //當事件處理函數返回false時,阻止默認操作和冒泡
29             if (result === false) e.preventDefault(), e.stopPropagation()
30             return result
31         }
32         //設置處理函數的在函數集中的位置
33         handler.i = set.length
34         //將函數存入函數集中
35         set.push(handler)
36         element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
37     })
38 }

這里5、6行,我們對我們傳入的事件類型進行了出來,將它改成了tap事件,並且在dom上打了一個標記

PS:zepto記錄事件句柄的zid也是記錄至dom屬性的

於是我們在觸發的時候可以這樣干:

 1 $.showLazyTap = function (e) {
 2     var forTap = $('#forTap');
 3     if (!forTap[0]) {
 4         forTap = $('<div id="forTap" style="background: black;color: White; display: none;  border-radius: 60px; position: absolute; 
 5 z-index: 99999; width: 60px; height: 60px"></div>');
 6         $('body').append(forTap);
 7     }
 8     forTap.css({
 9         top: (e.changedTouches[0].pageY - 30) + 'px',
10         left: (e.changedTouches[0].pageX - 30) + 'px'
11     })
12     forTap.show();
13     setTimeout(function () {
14         forTap.hide();
15     }, 350);
16 }
17 
18 tapTimeout = setTimeout(function () {
19     var event = $.Event('tap')
20     event.cancelTouch = cancelAll
21     touch.el.trigger(event)
22     if (touch.el.lazyTap) {
23         $.showLazyTap(e);
24     }
25 }, 0)

如此一來,在我們tap事件執行后,我們會彈出一朵菊花,阻止我們與下面的元素觸碰,然后350ms后消失

這里去掉菊花的背景就完全沒有影響了,然后我們就解決了tap事件帶來的點透問題

放棄tap

最后我們開始評估,評估后的結果是放棄tap事件,放棄他主要有以下原因:

① 兼容問題,使用tap事件在電腦上操作不便,自動化測試無法進行

② 兼容問題,IE內核的手機會完蛋

③ 點透解決方案不完美,蒙版形式不是所有人能接受,並且憑空多出一個lazyTap事件更是不該

所以我們放棄了這一方案,開始從根本上追尋問題,這正是我們最初的知識點的交合了 

fastclick思想提升點擊響應

程序界是一個神奇的地方,每當方案不夠完美時便會更加靠近真相,但當你真的想對真相着手時候,卻發現已經有人干了!

前面已經說過tap的種種弊端,所以原生的click事件依舊是最優方案,於是我們可以在click上面打主意了

實現原理

依舊使用touch事件模擬點擊,卻在tap觸發那一步自己創建一個click的Event對象觸發之:

PS:這里需要手機測試了

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 2 <html xmlns="http://www.w3.org/1999/xhtml">
 3 <head>
 4     <title></title>
 5     <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
 6     <style type="text/css">
 7          #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
 8          #c { width: 100px; height: 100px; border: 1px solid red; }
 9     </style>
10 </head>
11 <body>
12     <input id="tap1" type="button" value="我是tap" /><br />
13     <input id="click1" type="button" value="我是click" />
14     
15     <script type="text/javascript">
16         var tap1 = document.getElementById('tap1');
17         var click1 = document.getElementById('click1');
18         var t = 0, el;
19         document.addEventListener('touchstart', function (e) {
20             t = e.timeStamp;
21             el = e.target;
22         });
23         //注意,此處鼠標信息我沒有管他
24         function createEvent(type) {
25             var bubbles = true; //事件是否可以冒泡
26             var cancelable = true; //事件是否可以阻止瀏覽器默認事件
27             var view = document.defaultView; //與事件關聯的視圖,該屬性默認即可,不管
28             var detail = 0;
29             var screenX = 0;
30             var screenY = 0;
31             var clientX = 0;
32             var clientY = 0;
33             var ctrlKey = false; //是否按下ctrl
34             var altKey = false; //是否按下alt
35             var shiftKey = false;
36             var metaKey = false;
37             var button = 0; //表示按下哪一個鼠標鍵
38             var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的對象
39             var event = document.createEvent('MouseEvents');
40             event.initMouseEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
41             return event;
42         }
43         document.addEventListener('touchend', function (e) {
44             t = e.timeStamp;
45             var event = createEvent('tap')
46             //觸發tap事件
47             el.dispatchEvent(event);
48             //觸發click
49             var cEvent = createEvent('click');
50             el.dispatchEvent(cEvent);
51         });
52         function fnDom(el, msg, e) {
53             el.value = msg + '(' + (e.timeStamp - t) + ')';
54         }
55         tap1.addEventListener('tap', function (e) {
56             fnDom(this, '我是tap,我響應時間:', e);
57         });
58         click1.addEventListener('click', function (e) {
59             fnDom(this, '我是click,我響應時間:', e);
60         });
61     </script>
62 </body>
63 </html>

http://sandbox.runjs.cn/show/8ruv88rb

 

 

這里我們點擊按鈕后就明顯看到了按鈕開始響應時間是80左右,馬上變成了300多ms,因為click事件被執行了兩次

 

一次是touchend我們手動執行,一次是系統自建的click,這就是傳說中的鬼點擊,於是我們接下來說一說這個鬼點擊

鬼點擊

所謂鬼點擊,就是一次點擊執行了兩次,以程序來說,他這個是正常的現象,沒有問題的,但是我們的業務邏輯不允許這個事情存在

初步解決鬼點擊是比較容易的,直接在touchend處阻止瀏覽器默認事件即可:

 1 document.addEventListener('touchend', function (e) {
 2     t = e.timeStamp;
 3     var event = createEvent('tap')
 4     //觸發tap事件
 5     el.dispatchEvent(event);
 6     //觸發click
 7     var cEvent = createEvent('click');
 8     el.dispatchEvent(cEvent);
 9     e.preventDefault();
10 });

按道理來說,這個代碼是沒有問題的(而且同時可以解決我們的點透問題),但是在android上情況有所不同

我們的click依舊執行了兩次!!!!!由此又引入了下一話題,android與ios鼠標事件差異 

ios與android鼠標事件差異

PS:此點還要做詳細研究,今天淺淺的說幾點

在android上獲得的結果是驚人的,這個勞什子android里面moveover事件偶然比尼瑪touchstart還快!!!

而ios壓根就不理睬mouseover事件,這是主要問題產生原因!!!

而android在movedown時候,開開心心觸發了input的focus事件,然后鍵盤就彈起來了!!!

所以針對android,我們還得將mousedown干掉才行!!!!

而事實上,我們input獲取焦點,就是通過mousedown觸發的,ios也是,所以要解決android下面的問題還得從其它層面抓起 

事件捕獲解決鬼點擊

現在回到我們最初的知識點:

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 2 <html xmlns="http://www.w3.org/1999/xhtml">
 3 <head>
 4     <title></title>
 5     <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
 6     <style type="text/css">
 7         .bt { position: absolute; top: 250px; display: block; height: 50px; }
 8     </style>
 9 </head>
10 <body>
11     <input type="button" class="bt" value="我是快速點擊事件" id="fastclick" />
12     <input type="text" style="width: 150px; height: 200px;" />
13     <div id="div" style="width: 200px; height: 200px; border: 1px solid black">
14     </div>
15 </body>
16 <script type="text/javascript">
17     var fastclick = document.getElementById('fastclick');
18     var div = document.getElementById('div');
19     var touch = {};
20     var t = new Date().getTime();
21 
22     window.log = function (msg) {
23         var d = document.createElement('div');
24         d.innerHTML = msg;
25         div.appendChild(d);
26         console.log(msg);
27     };
28 
29     document.addEventListener('click', function (event) {
30         if (event.myclick == true) {
31             return true;
32         }
33         if (event.stopImmediatePropagation) {
34             event.stopImmediatePropagation();
35         } else {
36             event.propagationStopped = true;
37         }
38         event.stopPropagation();
39         event.preventDefault();
40         return true;
41     }, true);
42 
43     document.addEventListener('touchstart', function (e) {
44         touch.startTime = e.timeStamp;
45         touch.el = e.target;
46         t = e.timeStamp;
47     });
48     document.addEventListener('touchmove', function (e) { });
49     document.addEventListener('touchend', function (e) {
50         touch.last = e.timeStamp;
51         var event = document.createEvent('Events');
52         event.initEvent('click', true, true, window, 1, e.changedTouches[0].screenX, e.changedTouches[0].screenY, e.changedTouches[0].clientX, e.changedTouches[0].clientY, false, false, false, false, 0, null);
53         event.myclick = true;
54         touch.el && touch.el.dispatchEvent(event);
55         return true;
56     });
57 
58     function fnDom(el, msg, e) {
59         el.value = msg + '(' + (e.timeStamp - t) + ')';
60                 el.style.display = 'none';
61                 setTimeout(function () {
62                     el.style.display = '';
63                 }, 1000)
64     }
65 
66     fastclick.addEventListener('click', function (e) {
67         fnDom(this, '我是快速點擊事件', e);
68         log('快速點擊');
69     });
70 
71     div.addEventListener('click', function (e) {
72         this.innerHTML += 'div<br/>'
73     });
74 </script>
75 </html>
View Code

http://sandbox.runjs.cn/show/muk6q2br

最后追尋很久找到一個解決方案,該方案將上述知識點全部聯系起來了:

① 我們程序邏輯時先觸發touch事件,在touchend時候模擬click事件

② 這時我們給click事件對象一個屬性:

1 var event = document.createEvent('Events');
2 event.initEvent('click', true, true, window, 1, e.changedTouches[0].screenX,
3  e.changedTouches[0].screenY, e.changedTouches[0].clientX, e.changedTouches[0].clientY, false, false, false, false, 0, null);
4 event.myclick = true;
5 touch.el && touch.el.dispatchEvent(event);

③ 然后按照我們基礎篇的邏輯,我們事實上會先執行document上的click事件

我們這里做了一個操作,判斷是否包含myclick屬性,有就直接跳出(事件會向下傳遞),如果沒有就阻止傳遞

到此,我們就解決了鬼點擊問題,當然,不夠完善

結語

此文有點過長,但是對javascript事件機制描述較為詳細,希望對各位有幫助。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM