【移動端相容問題研究】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相當受歡迎,至於滑鼠慢,他究竟有多慢,我們來看看:
現在我們在手機上同時觸發兩者事件看看區別:
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 = {})
② 提供全域性的事件註冊點
View Code
③ 提供全域性的事件登出點
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庫:
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:標題好像洩露了我的行蹤......
點透問題
其實上面的問題是導致點透的因素之一,所謂點透就是:
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下面的問題還得從其它層面抓起
事件捕獲解決鬼點選
現在回到我們最初的知識點:
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事件機制描述較為詳細,希望對各位有幫助。
本文轉自葉小釵部落格園部落格,原文連結:http://www.cnblogs.com/yexiaochai/p/3462657.html,如需轉載請自行聯絡原作者
相關文章
- 移動端常見相容性問題解決方案
- 移動端相容性問題解決方案
- 移動端開發的相容問題(自我總結篇)
- 移動端相容性問題解決方案(一)
- vue:移動端判斷鍵盤事件,相容安卓iosVue事件安卓iOS
- PC端/移動端常見的相容性問題總結
- ReactJs移動端相容問題彙總ReactJS
- 移動開發相容問題整理筆記移動開發筆記
- JavaScript事件機制相容性解決方案JavaScript事件
- 記一次移動端使用 rem 的相容性問題REM
- 移動端Android跟ios相容性問題,反人類!!!AndroidiOS
- 移動端事件touchstart、touchmove、touchend詳解事件
- 移動端頁面寬度相容處理
- 淺談前端和移動端的事件機制前端事件
- 移動端audio音訊播放相容方案薦音訊
- 移動端ios:active偽類無效的相容解決方案iOS
- react vue 在移動端的相容性問題和一些小細節ReactVue
- 移動端滾動穿透問題解決方案穿透
- 解決移動端複製問題
- 移動端 touch事件事件
- 移動端touch事件事件
- javascript event事件物件相容性問題JavaScript事件物件
- 【移動端開發】移動端開發基礎問題
- 移動 web 開發幾個明顯的相容性問題Web
- 移動端web整理 移動端問題總結,移動web遇到的那些坑Web
- 移動端滾動穿透問題完美解決方案穿透
- 對移動端相容適配的分析
- 解決vue移動端適配問題Vue
- 移動端適配問題解決方案
- 移動端點透事件--阻止滾動事件事件
- fastclick.js解決移動端點選事件反應慢問題ASTJS事件
- 移動端點透問題及其解決方案
- Vue移動端問題記錄Vue
- 移動端瀏覽器問題瀏覽器
- web移動端常問面試題Web面試題
- 相容移動端的 Web 檔案館視覺化管理系統Web視覺化
- Swipe 移動端滑動外掛使用詳解
- 移動端開發viewport用法詳解View