【初窺javascript奧秘之事件機制】論“點透”與“鬼點擊”


前言

最近好好的研究了一番移動設備的點擊響應速度,期間不斷的被自己坑,最后搞得焦頭爛額,就是現在可能還有一些問題,但是過程中感覺自己成長不少,

最后居然感覺對javascript事件機制有了更好的認識,回頭來看,還是不錯的,所以今天將近期的學習記錄下來供后期查詢

今天我們再來重新回顧下javascript的事件機制

注意:下面說的android瀏覽器,意思是android下多數瀏覽器,不包括chrome

事件基礎

javascript與html之間的交互式通過事件實現的,事件是文檔(窗口)中發生的一些特定交互,這些交互可以使用監聽器(處理程序)預定,事件發生時就會回調我們的函數

PS:這就是傳說中的觀察者模式,我們這里先不管他

因為我們需要確定頁面哪一部分會擁有特定事件,比如內部一個span外部一個div,我們點擊span時候事實上瀏覽器也任務div被點擊了,甚至整個document也被點擊了,所以引入了事件流的概念

事件流是描述從頁面接收事件的順序,現在統一有事件冒泡與事件捕獲兩種事件捕獲流

事件冒泡/捕獲

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

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

DOM事件流

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

① 事件捕獲階段

② 處於目標階段

③ 事件冒泡階段

所以說,我們同時為一個元素綁定事件(冒泡與捕獲)先執行的是捕獲,然后會執行冒泡

 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>

這個樣子點擊子元素會先執行捕獲階段注冊的事件,然后執行冒泡階段執行的事件,我們這里做一點改變

 1 var p = document.getElementById('p'),
 2     c = document.getElementById('c');
 3 c.addEventListener('click', function () {
 4     alert('子節點捕獲')
 5 }, true);
 6 
 7 c.addEventListener('click', function () {
 8     alert('子節點冒泡')
 9 }, false);
10 
11 p.addEventListener('click', function () {
12     alert('父節點捕獲')
13 }, true);
14 
15 p.addEventListener('click', function () {
16     alert('父節點冒泡')
17 }, false);

① 這個時間點擊父元素會先執行父元素捕獲再執行父元素冒泡

② 點擊子元素會執行父元素捕獲,子元素捕獲,子元素冒泡,父元素冒泡

至此,我們對事件流機制應該了解一些了,於是繼續往下(注意:此點知識與“鬼點擊”有莫大的關系

 

事件對象

事件就是用戶或瀏覽器自身執行的某種動作(click、load),響應事件的函數就是事件處理程序(監聽器)

而我們的事件往往會自帶一個參數——事件對象(IE那勞什子就不管了)

1 c.addEventListener('click', function (e) {
2     alert('子節點捕獲')
3 }, true);

注意我們的e,他就是我們的event object了

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

PS:以下的兄弟全部是只讀的,所以不要妄想去隨意更改

bubbles

表明事件是否冒泡

cancelable

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

currentTarget

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

defaultPrevented

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

eventPhase

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

target

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

trusted

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

type

事件類型

view

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

preventDefault

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

stopPropagation

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

stopImmediatePropagation

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

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

思考事件參數

這里有一個比較有意思的問題,說他有意思,是因為我覺得可能各位平時沒有思考過:

我們一次點擊事件,各個事件處理的Event Object是否相同?

答案是肯定的,這里我們用IE的事件對象來說,我們是這樣獲得的window.event,IE這樣干不是沒有道理的,因為我們一次點擊這個家伙是共用的!!!

事實上,我們每次鼠標操作,這個事件參數都是相同的,不信??

以PC來說,我們為movedown綁定一個事件,並且動態為e增加一個屬性,newArg

 1 window.log = function (msg) {
 2     console.log(msg)
 3 }
 4 
 5 var p = document.getElementById('p'),
 6     c = document.getElementById('c');
 7 
 8 document.addEventListener('click', function (e) {
 9     e.newArg = '葉小釵';
10 }, true);
11 
12 c.addEventListener('click', function (e) {
13     log(e);
14     log('子節點捕獲')
15 }, true);
16 
17 c.addEventListener('click', function (e) {
18     log(e);
19     log('子節點冒泡')
20 }, false);
21 
22 p.addEventListener('click', function (e) {
23     log(e);
24     log('父節點捕獲')
25 }, true);
26 
27 p.addEventListener('click', function (e) {
28     log(e);
29     log('父節點冒泡')
30 }, false);

在最后的事件冒泡階段,我們可以看到,我們是多了一個newArg屬性的,由此我們可以證明我們整個過程中event是一樣的

模擬事件

事件,就是網頁中某個值得關注的瞬間,事件經常由用戶操作或瀏覽器功能觸發,其實我們可以使用javascript在任意時刻來觸發特定事件

而,此時的事件就和瀏覽器創建的事件一樣,也就是說我們的事件會冒泡會導致瀏覽器默認行為觸發,模擬事件是出現鬼點擊主要原因

createEvent

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

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

模擬鼠標事件

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

var type = 'click'; //要觸發的事件類型
var bubbles = true; //事件是否可以冒泡
var cancelable = true; //事件是否可以阻止瀏覽器默認事件
var view = document.defaultView; //與事件關聯的視圖,該屬性默認即可,不管
var detail = 0;
var screenX = 0;
var screenY = 0;
var clientX = 0;
var clientY = 0;
var ctrlKey = false; //是否按下ctrl
var altKey = false; //是否按下alt
var shiftKey = false;
var metaKey = false;
var button = 0;//表示按下哪一個鼠標鍵
var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的對象

var event = document.createEvent('MouseEvents');
event.initMouseEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, 
ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);

如此我們就可以用到這個東西了,好了,我們后面會用到他的,至此,我們基礎復習完畢,進入今天的正題吧

“鬼點擊”何來

我們一般的移動設備在瀏覽網頁時候都會有這樣的功能:連續點擊兩次頁面,整個頁面會放大!

這是我們click事件在移動端會延遲300ms的主要原因

但是,我們真正開發移動站點時候,會限制我們的viewport,所以雙擊放大的效果便沒有意義,這個效果反而讓我們整個網頁看起來“遲鈍”

而我們的touch事件並不會有任何延遲,所以他就成了我們解決click響應速度的利刃,而且我也暫時只知道他能干(chrome30更新后,解決了這個問題)

所以javascript提出了幾個解決方案, 第一個方案當然是我們的tap事件

tap事件

tap事件的由來是系統自建了一個事件,叫做tap事件,他在touchend時候會觸發

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="tap" type="button" value="我是tap" /><br />
13     <input id="click" type="button" value="我是click" />
14     <script type="text/javascript">
15         var tap = document.getElementById('tap');
16         var click = document.getElementById('click');
17         var t = 0, el;
18 
19         document.addEventListener('touchstart', function (e) {
20             t = e.timeStamp;
21             el = e.target;
22         });
23         document.addEventListener('touchend', function (e) {
24             t = e.timeStamp;
25             var type = 'tap'; //要觸發的事件類型
26             var bubbles = true; //事件是否可以冒泡
27             var cancelable = true; //事件是否可以阻止瀏覽器默認事件
28             var view = document.defaultView; //與事件關聯的視圖,該屬性默認即可,不管
29             var detail = 0;
30             var screenX = 0;
31             var screenY = 0;
32             var clientX = 0;
33             var clientY = 0;
34             var ctrlKey = false; //是否按下ctrl
35             var altKey = false; //是否按下alt
36             var shiftKey = false;
37             var metaKey = false;
38             var button = 0; //表示按下哪一個鼠標鍵
39             var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的對象
40             var event = document.createEvent('MouseEvents');
41             event.initMouseEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
42             //觸發tap事件
43             el.dispatchEvent(event);
44         });
45         function fnDom(el, msg, e) {
46             el.value = msg + '(' + (e.timeStamp - t) + ')';
47         }
48         tap.addEventListener('tap', function (e) {
49             fnDom(this, '我是tap,我響應時間:', e);
50         });
51         click.addEventListener('click', function (e) {
52             fnDom(this, '我是click,我響應時間:', e);
53         });
54     </script>
55 </body>
56 </html>
View Code

這里的響應時間計算可能有點誇張,但是各位用手機瀏覽器打開點擊后應該會有感覺:

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

測試過的朋友會發現明顯的順暢感和堵塞感,然后我們來看看如何改寫本身事件:

鬼點擊之因

我們的項目可能已經做過一半了,也許我們的項目已經完成,所以,我們並不想將click事件一個個換成tap,誰知道tap會出什么勞什子問題!

PS:事實上tap確實搞了不少事情出來

所以,我們想到了改寫click事件觸發點,直接全站提升click的響應速度,於是我們將上面的代碼這樣一改

 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 
20         document.addEventListener('touchstart', function (e) {
21             t = e.timeStamp;
22             el = e.target;
23         });
24 
25         //注意,此處鼠標信息我沒有管他
26         function createEvent(type) {
27             var bubbles = true; //事件是否可以冒泡
28             var cancelable = true; //事件是否可以阻止瀏覽器默認事件
29             var view = document.defaultView; //與事件關聯的視圖,該屬性默認即可,不管
30             var detail = 0;
31             var screenX = 0;
32             var screenY = 0;
33             var clientX = 0;
34             var clientY = 0;
35             var ctrlKey = false; //是否按下ctrl
36             var altKey = false; //是否按下alt
37             var shiftKey = false;
38             var metaKey = false;
39             var button = 0; //表示按下哪一個鼠標鍵
40             var relatedTarget = 0; //模擬mousemove或者out時候用到,與事件相關的對象
41             var event = document.createEvent('MouseEvents');
42             event.initMouseEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
43             return event;
44         }
45         document.addEventListener('touchend', function (e) {
46             t = e.timeStamp;
47 
48             var event = createEvent('tap')
49             //觸發tap事件
50             el.dispatchEvent(event);
51 
52             //觸發click
53             var cEvent = createEvent('click');
54             el.dispatchEvent(cEvent);
55         });
56         function fnDom(el, msg, e) {
57             el.value = msg + '(' + (e.timeStamp - t) + ')';
58         }
59         tap1.addEventListener('tap', function (e) {
60             fnDom(this, '我是tap,我響應時間:', e);
61         });
62         click1.addEventListener('click', function (e) {
63             fnDom(this, '我是click,我響應時間:', e);
64         });
65     </script>
66 </body>
67 </html>
View Code

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

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

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

初步解決鬼點擊

起初,我認為解決鬼點擊比較簡單:直接在touchend處阻止瀏覽器默認事件即可:

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

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

我們的click依舊執行了兩次!!!!!不信,您去試試......

PS:不要問我為什么android不行,我這個事情沒搞透,如果您知道,請給我留言

現在回到我們最初(昨天吧,自己做的demo)的例子:

 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屬性,有就直接跳出(事件會向下傳遞),如果沒有就阻止傳遞

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

點透問題

我們如果剛剛點擊按鈕時候讓按鈕消失,事實上他會觸發在他下面元素的click事件!

這個就是我們所謂“點透”問題,這個點透可以通過解決鬼點擊的方式解決,但是有一種點透卻不是那么簡單的!!!

 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: 50px; 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

這種情況下,我們點擊按鈕,按鈕消失,然后下面的input會獲取焦點的!這個問題無法避免,解決方案依舊是阻止瀏覽器本身事件

這樣在ios下面就沒有問題了,當然現在我們input不能獲得焦點了,但是該問題比較簡單,我們暫時不管他,說下我們android下的問題

現在我們在android下,那個input非要獲得焦點,這就是我們最痛恨的“點透”現象之一

該種場景比較常見:我們點擊按鈕出現一個彈出層,我們點擊彈出層關閉按鈕,正好下面有個input標簽,尼瑪就談了一個鍵盤出來......

android問題

最后研究得出了驚人的結果,這個勞什子android里面moveover事件偶然比尼瑪touchstart還快!!!

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

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

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

而事實上,我們input獲取焦點,就是通過mousedown觸發的,ios也是

至此,我們主要問題討論的差不多了,暫時到這里吧。

結語

今天我們一起溫故了一次javascript事件相關的知識,記錄了最近我遇到的一些問題供以后查詢,如果這些知識對你有用就善莫大焉了


免責聲明!

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



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