javascript事件基礎(chǔ)
我們的網(wǎng)頁之所以豐富多彩并具有交互功能,是因為我們的javascript腳本語言,而javascript與HTML之間的交互又是通過事件機制實現(xiàn)的所以,事件是javascript一大核心,深入了解事件機制在我們遇到較困難問題時候十分有幫助。</br>所謂事件,就是網(wǎng)頁發(fā)生的一些瞬間(比如點擊、滑動),在這些瞬間我們使用事件監(jiān)聽器(回調(diào)函數(shù))去訂閱事件,在事件發(fā)生時候我們的回調(diào)函數(shù)就會觸發(fā)。</br>觀察者模式的javascript事件機制的基石,這種異步事件編程模型,就是用戶產(chǎn)生特定的操作,瀏覽器就會產(chǎn)生特定的事件,我們?nèi)羰怯嗛喠耸录卣{(diào)就會觸發(fā)。</br>好了,我們下面就來研究下javascript事件機制的幾個關(guān)鍵點。
事件捕獲/冒泡
網(wǎng)頁上的布局很復雜,我們對頁面的單一操作有可能產(chǎn)生預計以外的影響:
比如我點擊一個span,我可能就想點擊一個span,試試上他是先點擊document,然后點擊事件傳遞到span的,而且并不會在span停下,span有子元素就會繼續(xù)往下,最后會依次回傳至document,我們這里偷一張圖:
事件冒泡即由最具體的元素(文檔嵌套最深節(jié)點)接收,然后逐步上傳至document.</br>事件捕獲會由最先接收到事件的元素然后傳向最里邊<strong>(我們可以將元素想象成一個盒子裝一個盒子,而不是一個積木堆積)</strong>
這里我們進入dom事件流,這里我們詳細看看javascript事件的傳遞方式
DOM事件流
DOM2級事件規(guī)定事件包括三個階段:
- 事件捕獲階段
- 處于目標階段
- 事件冒泡階段
舉個栗子:
<pre>
`<style type="text/css">
p { width: 300px; height: 300px; padding: 10px; border: 1px solid black; }
c { width: 100px; height: 100px; border: 1px solid red; }
</style>
<div id="p">
parent
<div id="c">
child
</div>
</div>
<script type="text/javascript">
var p = document.getElementById('p');
var c = document.getElementById('c');
c.addEventListener('click', function ()
{alert('子節(jié)點捕獲')}, true);
c.addEventListener('click', function () {
alert('子節(jié)點冒泡') }, false);
</script>`
</pre>
這個代碼比較簡單,我們主要點擊child即可,這里要證明的就是點擊事件是先捕獲再冒泡,所以我們這里來一個復雜點的關(guān)系:
<pre>
` <style type="text/css">
#p { width: 300px; height: 300px; padding: 10px; border: 1px solid black; }
#c { width: 100px; height: 100px; border: 1px solid red; }
</style>
<div id="p">
parent
<div id="c">
child
</div>
</div>
<script type="text/javascript">
var p = document.getElementById('p'),
c = document.getElementById('c');
c.addEventListener('click', function () {
alert('子節(jié)點捕獲')
}, true);
c.addEventListener('click', function () {
alert('子節(jié)點冒泡')
}, false);
p.addEventListener('click', function () {
alert('父節(jié)點捕獲')
}, true);
p.addEventListener('click', function () {
alert('父節(jié)點冒泡')
}, false);
</script>
`</pre>
現(xiàn)在這個家伙就比較實在了,不注意就容易暈的,我們來稍微理一理:
① 點擊parent,事件首先在document上然后parent捕獲到事件,處于目標階段然后event.target也等于parent,所以觸發(fā)捕獲事件
<strong>由于target與currentTarget相等了,所以認為到底了,開始冒泡,執(zhí)行冒泡事件</strong>
② 點擊child情況有所不同,事件由document傳向parent執(zhí)行事件,然后傳向child最后開始冒泡,所以執(zhí)行順序各位一定要清晰
至此,我們事件傳輸結(jié)束,下面開始研究事件參數(shù) .
事件對象
所謂事件對象,是與特定對象相關(guān),并且包含該事件詳細信息的對象。
事件對象作為參數(shù)傳遞給事件處理程序(IE8之前通過window.event獲得),所有事件對象都有事件類型type與事件目標target(IE8之前的srcElement我們不關(guān)注了)
各個事件的事件參數(shù)不一樣,比如鼠標事件就會有相關(guān)坐標,包含和創(chuàng)建他的特定事件有關(guān)的屬性和方法,觸發(fā)的事件不一樣,參數(shù)也不一樣(比如鼠標事件就會有坐標信息),我們這里題幾個較重要的
PS:以下的兄弟全部是只讀的,所以不要妄想去隨意更改,IE之前的問題我們就不關(guān)注了
bubbles
表明事件是否冒泡
cancelable
表明是否可以取消事件的默認行為
currentTarget
某事件處理程序當前正在處理的那個元素
defaultPrevented
為true表明已經(jīng)調(diào)用了preventDefault(DOM3新增)
eventPhase
調(diào)用事件處理程序的階段:1 捕獲;2 處于階段;3 冒泡階段
這個屬性的變化需要在斷點中查看,不然你看到的總是0
target
事件目標(綁定事件那個dom)
trusted
true表明是系統(tǒng)的,false為開發(fā)人員自定義的(DOM3新增)
type
事件類型
view
與事件關(guān)聯(lián)的抽象視圖,發(fā)生事件的window對象
preventDefault
取消事件默認行為,cancelable是true時可以使用
stopPropagation
取消事件捕獲/冒泡,bubbles為true才能使用
stopImmediatePropagation
取消事件進一步冒泡,并且組織任何事件處理程序被調(diào)用(DOM3新增)
在我們的事件處理內(nèi)部,this與currentTarget相同
createEvent
可以在document對象上使用createEvent創(chuàng)建一個event對象
DOM3新增以下事件:
UIEvents
MouseEvents
MutationEvents,一般化dom變動
HTMLEvents一般dom事件
創(chuàng)建鼠標事件時需要創(chuàng)建的事件對象需要提供指定的信息(鼠標位置信息),我們這里提供以下參數(shù):
<pre>`
var type = 'click'; //要觸發(fā)的事件類型
var bubbles = true; //事件是否可以冒泡
var cancelable = true; //事件是否可以阻止瀏覽器默認事件
var view = document.defaultView; //與事件關(guān)聯(lián)的視圖,該屬性默認即可,不管
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時候用到,與事件相關(guān)的對象
var event = document.createEvent('MouseEvents');
event.initMouseEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY,
ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
`</pre>
如此,我們就自己創(chuàng)建了一個event對象,然后可以傳給我們自己創(chuàng)建的事件,這個知識點,我們下面再說
PS:值得注意的是,我們自己創(chuàng)建的event對象可以有一點不一樣的東西,比如我們的事件對象可能多了一個這種屬性:
event.flag = '葉小釵'
事件模擬
事件模擬是javascript事件機制中相當有用的功能,理解事件模擬與善用事件模擬是判別一個前端的重要依據(jù),所以各位一定要深入理解(我理解較水)
事件一般是由用戶操作觸發(fā),其實javascript也是可以觸發(fā)的,比較重要的是,javascript的觸發(fā)事件還會冒泡哦!!!
意思就是,javascript觸發(fā)的事件與瀏覽器本身觸發(fā)其實是一樣的(并不完全一致)
如此,我們這里來通過鍵盤事件觸發(fā)剛剛的點擊事件吧,我們這里點擊鍵盤便觸發(fā)child的點擊,看看他的表現(xiàn)如何
PS:由于是鍵盤觸發(fā),便不具有相關(guān)參數(shù)了,我們可以捕捉event參數(shù),這對我們隊事件傳輸?shù)睦斫庥心蟮膸椭?/p>
我們這里先創(chuàng)建事件參數(shù),然后給鍵盤注冊事件,在點擊鍵盤時候便觸發(fā)child的點擊事件,各位試試看:
PS:這個可能需要打開網(wǎng)頁點擊空格測試了
<pre>
<style type="text/css">
#p { width: 300px; height: 300px; padding: 10px; border: 1px solid black; }
#c { width: 100px; height: 100px; border: 1px solid red; }
</style>
<div id="p">
parent
<div id="c">
child
</div>
</div>
<script type="text/javascript">
alert = function (msg) {
console.log(msg);
}
var p = document.getElementById('p'),
c = document.getElementById('c');
c.addEventListener('click', function (e) {
console.log(e);
alert('子節(jié)點捕獲')
}, true);
c.addEventListener('click', function (e) {
console.log(e);
alert('子節(jié)點冒泡')
}, false);
p.addEventListener('click', function (e) {
console.log(e);
alert('父節(jié)點捕獲')
}, true);
p.addEventListener('click', function (e) {
console.log(e);
alert('父節(jié)點冒泡')
}, false);
document.addEventListener('keydown', function (e) {
if (e.keyCode == '32') {
var type = 'click'; //要觸發(fā)的事件類型
var bubbles = true; //事件是否可以冒泡
var cancelable = true; //事件是否可以阻止瀏覽器默認事件
var view = document.defaultView; //與事件關(guān)聯(lián)的視圖,該屬性默認即可,不管
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時候用到,與事件相關(guān)的對象
var event = document.createEvent('Events');
event.myFlag = '葉小釵';
event.initEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY,ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
console.log(event);
c.dispatchEvent(event);
}
}, false);
</script>
</pre>
各位,這里看到了與之前的相同或者不同嗎???這些都是很關(guān)鍵的哦,其實主要不同就是我們的事件參數(shù)沒了鼠標位置,多了一個屬性:
這里有兩點容易讓各位造成錯覺:
① firefox并不會將myFlag顯示到console下面
② chrome如果使用原生alert會阻止第一次父元素捕獲,所以各位一定要注意
然后這里還有一個小小知識點:
使用dom.dispatchEvent(event)觸發(fā)模擬事件
移動端響應(yīng)速度
有了以上知識點,其實對PC端來說基本夠用了,如果再稍微研究下jquery源碼就善莫大焉了,但是在移動端卻有所不同,我們這里還得來理一理
PS:我這里主要針對點擊事件
首先,在移動端mouse事件好像就有點不那么適用了,倒不是說touch事件要比mouse事件好,其實他們底層原理相距不大,主要不同點就是:
移動端會多點觸屏
多點觸屏就帶來了事件對象參數(shù)的差異,比如說:
changedTouches/touches/targetTouches
touches:為屏幕上所有手指的信息
PS:因為手機屏幕支持多點觸屏,所以這里的參數(shù)就與手機有所不同
targetTouches:手指在目標區(qū)域的手指信息
changedTouches:最近一次觸發(fā)該事件的手指信息
比如兩個手指同時觸發(fā)事件,2個手指都在區(qū)域內(nèi),則容量為2,如果是先后離開的的話,就會先觸發(fā)一次再觸發(fā)一次,這里的length就是1,只統(tǒng)計最新的
PS:一般changedTouches的length都是1
touchend時,touches與targetTouches信息會被刪除,changedTouches保存的最后一次的信息,最好用于計算手指信息
這里要使用哪個數(shù)據(jù)各位自己看著辦吧,我也不是十分清晰(我這里還是使用changedTouches吧)
以上就是mouse與touch主要不同點,但這些并不是太影響我們的操作,因為到現(xiàn)在為止,我們一般還是使用的是單擊
小貼士
國內(nèi)SPA網(wǎng)站模式較少,目前為止還是以單個網(wǎng)頁為主,spa模式對javascript技術(shù)要求較高不說,首次加載量大也是不可避免的問題
加之移動端設(shè)備今年才普及,而且各自爭奪領(lǐng)地、爭奪入口,還有其他原因,反正現(xiàn)況是有時做移動端的兼容比做IE的兼容還難
就拿簡單的CSS3動畫來說,在ios下就有閃動現(xiàn)象,而且還是iPhone4s,就現(xiàn)今更新?lián)Q代來說,此種情況并不會得到明顯好轉(zhuǎn),而且CSS3動畫狀態(tài)保存問題亦是一大難題
另外,網(wǎng)頁想要檢測手機是否安裝APP也是有很大缺陷,移動端的fixed更不要說,這些問題都需要我們乃至開發(fā)商解決
PS:這里扯得有點遠,我們繼續(xù)下面的話題
touch與click響應(yīng)速度問題
click本身在移動端響應(yīng)是沒有問題的,但是我們點擊下來300ms 的延遲卻是事實,這種事實造成的原因就是
手機需要知道你是不是想雙擊放大網(wǎng)頁內(nèi)容
所以click點擊響應(yīng)慢,而touch卻不會有這樣的限制,于是移動端的touch相當受歡迎,至于鼠標慢,他究竟有多慢,我們來看看:
現(xiàn)在我們在手機上同時觸發(fā)兩者事件看看區(qū)別:
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script id="others_zepto_10rc1" type="text/javascript" class="library" src="http://sandbox.runjs.cn/js/sandbox/other/zepto.min.js"></script>
</head>
<body>
<div id="d" style="width: 100px; height: 100px; border: 1px solid black;">
</div>
</body>
<script type="text/javascript">
var startTime;
var log = function (msg) {
var div = $('<div></div>');
div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg)
$('body').append(div);
};
var touchStart = function () {
startTime = new Date().getTime();
log('touchStart');
};
var touchEnd = function () {
log('touchEnd');
};
var mouseDown = function () {
log('mouseDown');
};
var mouseClick = function () {
log('mouseClick');
};
var mouseUp = function () {
log('mouseUp');
};
var d = $('#d');
d.bind('mousedown', mouseDown);
d.bind('click', mouseClick);
d.bind('mouseup', mouseUp);
d.bind('touchstart', touchStart);
d.bind('touchend', touchEnd);
</script>
</html>
</pre>
測試地址:(使用手機)
http://sandbox.runjs.cn/show/ey54cgqf
此處手機與電腦有非常大的區(qū)別!!!
結(jié)論
不要同時給document綁定鼠標與touch事件
<pre>
document.addEventListener('mousedown', mouseDown);
document.addEventListener('click', mouseClick);
document.addEventListener('mouseup', mouseUp);
document.addEventListener('touchstart', touchStart);
document.addEventListener('touchend', touchEnd);
</pre>
這個樣子,在手機上不會觸發(fā)click事件,click事件要綁定到具體元素
PS:此處在ios與android上有不一樣的表現(xiàn),我們后面會涉及
手機上mousedown響應(yīng)慢
經(jīng)過測試,電腦上touch與click事件的差距不大,但是手機上,當我們手觸碰屏幕時,要過300ms左右才會觸發(fā)mousedown事件
所以click事件在手機上響應(yīng)就是慢一拍,我們前面說過為什么click慢了
數(shù)據(jù)說明:
可以看到,在手機上使用click事件其實對用戶體驗并不好,所以我們可能會逐步使用touch事件,但是真正操作時候你就會知道click的好
好了,此處內(nèi)容暫時到這,我們先看看zepto的事件機制,下面會提到如何使用touch提升click的響應(yīng)速度.
zepto事件機制
zepto是以輕巧的dom庫,這家伙可以說是jquery的html5版本,而且在移動端有媲美jqueryPC端的趨勢,如果jquery不予以回擊,可能移動端的份額就不行了
我們這里不討論zepto的其他地方了,我們單獨講他的事件相關(guān)提出來看看
注冊/注銷事件
事件注冊是我們項目開發(fā)中用得最多的一塊,我們一般會使用以下幾種方式綁定事件:
<pre>
`el.on(type, function () {});//力推
el.bind(function(){});
el.click(function() {});//不推薦
dom.onclick = function() {};//需要淘汰`
</pre>
以上幾種方式用得較多,最后一種在真實的項目中基本不用,單數(shù)第二也極少使用,那么zepto內(nèi)部是怎么實現(xiàn)的呢?
PS:這里,我就不詳細說明zepto事件機制了,這里點一下即可
zepto事件機制其實比較簡單,他具體流程如下:
① 事件注冊時在全局保存事件句柄(handlers = {})
② 提供全局的事件注冊點
<pre>
//給元素綁定監(jiān)聽事件,可同時綁定多個事件類型,如['click','mouseover','mouseout'],也可以是'click mouseover mouseout'
function add(element, events, fn, selector, getDelegate, capture) {
var id = zid(element),
set = (handlers[id] || (handlers[id] = [])) //元素上已經(jīng)綁定的所有事件處理函數(shù)
eachEvent(events, fn, function (event, fn) {
var handler = parse(event)
//保存fn,下面為了處理mouseenter, mouseleave時,對fn進行了修改
handler.fn = fn
handler.sel = selector
// 模仿 mouseenter, mouseleave
if (handler.e in hover) fn = function (e) {
/*
relatedTarget為事件相關(guān)對象,只有在mouseover和mouseout事件時才有值
mouseover時表示的是鼠標移出的那個對象,mouseout時表示的是鼠標移入的那個對象
當related不存在,表示事件不是mouseover或者mouseout,mouseover時!$.contains(this, related)當相關(guān)對象不在事件對象內(nèi)
且related !== this相關(guān)對象不是事件對象時,表示鼠標已經(jīng)從事件對象外部移入到了對象本身,這個時間是要執(zhí)行處理函數(shù)的
當鼠標從事件對象上移入到子節(jié)點的時候related就等于this了,且!$.contains(this, related)也不成立,這個時間是不需要執(zhí)行處理函數(shù)的
*/
var related = e.relatedTarget
if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments)
}
//事件委托
handler.del = getDelegate && getDelegate(fn, event)
var callback = handler.del || fn
handler.proxy = function (e) {
var result = callback.apply(element, [e].concat(e.data))
//當事件處理函數(shù)返回false時,阻止默認操作和冒泡
if (result === false) e.preventDefault(), e.stopPropagation()
return result
}
//設(shè)置處理函數(shù)的在函數(shù)集中的位置
handler.i = set.length
//將函數(shù)存入函數(shù)集中
set.push(handler)
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
}
</pre>
③ 提供全局的事件注銷點
<pre>
`$.fn.off = function (event, selector, callback) {
return !selector || $.isFunction(selector) ? this.unbind(event, selector || callback) : this.undelegate(selector, event, callback)
}
$.fn.unbind = function (event, callback) {
return this.each(function () {
remove(this, event, callback)
})
}
function remove(element, events, fn, selector, capture) {
var id = zid(element)
eachEvent(events || '', fn, function (event, fn) {
findHandlers(element, event, fn, selector).forEach(function (handler) {
delete handlers[id][handler.i]
element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
})
}</pre> ④ 提供簡便寫法 <pre>
$.fn.click = function (fn) {
this.bind('click', callback)
}
`
</pre>
如果需要詳細了解的朋友請看此篇博客:
http://www.cnblogs.com/yexiaochai/p/3448500.html
我這里就不詳細說明了,這里需要說明的是,zepto提供了兩個語法糖:
創(chuàng)建事件參數(shù)/觸發(fā)事件
這兩個方法,完全是我們上面代碼的縮寫,當然他更加健壯,我們后面就可以使用他了
<pre>
$.fn.trigger = function (event, data) { if (typeof event == 'string' || $.isPlainObject(event)) event = $.Event(event) fix(event) event.data = data return this.each(function () { // items in the collection might not be DOM elements // (todo: possibly support events on plain old objects) if ('dispatchEvent' in this) this.dispatchEvent(event) }) }
</pre>
<pre>
`specialEvents = {}
specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'
//根據(jù)參數(shù)創(chuàng)建一個event對象
$.Event = function (type, props) {
//當type是個對象時
if (typeof type != 'string') props = type, type = props.type
//創(chuàng)建一個event對象,如果是click,mouseover,mouseout時,創(chuàng)建的是MouseEvent,bubbles為是否冒泡
var event = document.createEvent(specialEvents[type] || 'Events'),
bubbles = true
//確保bubbles的值為true或false,并將props參數(shù)的屬性擴展到新創(chuàng)建的event對象上
if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
//初始化event對象,type為事件類型,如click,bubbles為是否冒泡,第三個參數(shù)表示是否可以用preventDefault方法來取消默認操作
event.initEvent(type, bubbles, true, null, null, null, null, null, null, null, null, null, null, null, null)
//添加isDefaultPrevented方法,event.defaultPrevented返回一個布爾值,表明當前事件的默認動作是否被取消,也就是是否執(zhí)行了 event.preventDefault()方法.
event.isDefaultPrevented = function () {
return this.defaultPrevented
}
return event
}`
</pre>
zepto模擬tap事件
前面,我們提到過,我們移動端的點擊響應(yīng)很慢,但是touch不會有這種限制,所以zepto為我們封裝了一個touch庫:
<pre>
`
(function ($) {
var touch = {},
touchTimeout, tapTimeout, swipeTimeout,
longTapDelay = 750, longTapTimeout
function parentIfText(node) {
return 'tagName' in node ? node : node.parentNode
}
function swipeDirection(x1, x2, y1, y2) {
var xDelta = Math.abs(x1 - x2), yDelta = Math.abs(y1 - y2)
return xDelta >= yDelta ? (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 = {}
}
$(document).ready(function () {
var now, delta
$(document.body)
.bind('touchstart', function (e) {
now = Date.now()
delta = now - (touch.last || now)
touch.el = $(parentIfText(e.touches[0].target))
touchTimeout && clearTimeout(touchTimeout)
touch.x1 = e.touches[0].pageX
touch.y1 = e.touches[0].pageY
if (delta > 0 && delta <= 250) touch.isDoubleTap = true
touch.last = now
longTapTimeout = setTimeout(longTap, longTapDelay)
})
.bind('touchmove', function (e) {
cancelLongTap()
touch.x2 = e.touches[0].pageX
touch.y2 = e.touches[0].pageY
if (Math.abs(touch.x1 - touch.x2) > 10)
e.preventDefault()
})
.bind('touchend', function (e) {
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)
// 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) {
touch.el.trigger('doubleTap')
touch = {}
}
// trigger single tap after 250ms of inactivity
else {
touchTimeout = setTimeout(function () {
touchTimeout = null
touch.el.trigger('singleTap')
touch = {}
}, 250)
}
}, 0)
})
.bind('touchcancel', cancelAll)
$(window).bind('scroll', cancelAll)
})
; ['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function (m) {
$.fn[m] = function (callback) { return this.bind(m, callback) }
})
})(Zepto)
`
</pre>
這個touch庫個人覺得寫得不行,雖然我寫不出來......
拋開其他東西,我們將其中點擊的核心給剝離出來
<pre>
`
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) {
touch.el.trigger('doubleTap')
touch = {}
}
// trigger single tap after 250ms of inactivity
else {
touchTimeout = setTimeout(function () {
touchTimeout = null
touch.el.trigger('singleTap')
touch = {}
}, 250)
}
}, 0)
`
</pre>
拋開其他問題,這里5-7行就是觸發(fā)TAP事件的核心,我們這里簡單說下流程:
① 我們在程序過程中為dom(包裝過的)tap事件(使用addEventListener方式注冊/zepto使用bind即可)
② 點擊目標元素,觸發(fā)document的touchstart與touchend,在end時候判斷是否為一次點擊事件(是否touchmove過多)
③ 如果是便觸發(fā)tap事件,于是我們的事件監(jiān)聽器便會觸發(fā)了
以程序邏輯來說,他這個是沒問題的,他甚至考慮了雙擊與滑動事件,結(jié)合前面的知識點,這里應(yīng)該很好理解
但就是這段代碼卻帶來了這樣那樣的問題,這些問題就是移動端兼容的血淚史,且聽我一一道來
tap事件的問題一覽
body區(qū)域外點擊無效
我們看看我們的touch事件的綁定點
<pre>
$(document.body).bind(......)
</pre>
這段代碼本身沒什么問題,在PC端毫無問題,但就是這樣的代碼在手機端(多個手機/多個瀏覽器)下產(chǎn)生了一些區(qū)域不可點擊的現(xiàn)象
這其實不完全是兼容問題,是因為我們在手機端時候往往喜歡將body設(shè)置為height: 100%,于是這樣會產(chǎn)生一個問題
如果我們的view長度過程那么body區(qū)域事實上不會增加,所以我們點擊下面區(qū)域時候手機就不認為我們點擊的是body了......
這個BUG只能說無語,但是min-height雖然可以解決點擊BUG卻會帶來全局布局的問題,所以這個問題依然糾結(jié)
好在后面zepto意識到了這個問題將事件綁定改成了這個:
<pre>$(document).bind(......)
</pre>
于是修復了這個問題
e.preventDefault失效(settimeout小貼士)
如果說第一個問題導致點是我們自己的布局的話,第二個問題的引發(fā)點我就覺得是開發(fā)人員的問題了
PS:zepto多數(shù)是抄寫jquery,touch是自己寫的,就是這個touch就搞了很多問題出來......
這里我們先不忙看tap代碼本身帶來的問題,我這里出一個題各位試試:
<pre>
var sum1 = 0, sum2 = 0, sum3 = 0; len = 2; var arr = []; for (var i = 0; i < len; i++) { arr.push(i) } for (var i = 0; i < len; i++) { setTimeout(function () { sum1 += arr[i]; }, 0); } $.each(arr, function (i, v) { setTimeout(function () { sum2 += v; }, 0); }); for (var i = 0; i < len; i++) { sum3++; } //sum3不管,答出len=2與len=200000時,sum1,sum2的值 console.log(sum1); console.log(sum2); console.log(sum3);
</pre>
各位仔細觀察這個題,會有不一樣的感覺,在sum很大的時候第三個循環(huán)肯定會耗費超過一秒的時間
按道理說這里的sum1/sum2會進行相關(guān)計算,事實卻是:
settimeout將優(yōu)先級降到了最低,他會在主干流程執(zhí)行結(jié)束后才會執(zhí)行
于是我們這里引出了一個非常有趣的問題,且看zepto源碼:
<pre>
1 tapTimeout = setTimeout(function () { 2 var event = $.Event('tap') 3 event.cancelTouch = cancelAll 4 touch.el.trigger(event) 5 }, 0)
</pre>
各位小伙伴,你認為我們在第四行后執(zhí)行e.preventDefault()等操作會有效么???
或者說,我們在觸發(fā)tap事件后,會執(zhí)行我們的回調(diào)函數(shù)我們在我們的回調(diào)函數(shù)中執(zhí)行e.preventDefault()等操作會有效么???
各位小伙伴可以去試試,我這里就不做說明了
PS:標題好像泄露了我的行蹤......
點透問題
其實上面的問題是導致點透的因素之一,所謂點透就是:
<pre>
`
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
#list { border: 1px solid black; position: absolute; top: 0px; left: 10px; width: 200px; height: 100px; }
#d { border: 1px solid black; height: 300px; width: 100%; }
</style>
</head>
<body>
<div id="d">
<input type="text" id="input" style="width: 80px; height: 200px;" />
<div id="list">
</div>
</div>
</body>
<script src="res/libs/zepto.js" type="text/javascript"></script>
<script type="text/javascript">
window.log = function (msg) {
console.log(msg);
var div = $('#myMsg');
if (!div[0]) div = $('<div id="myMsg"></div>')
$('#d').append(div);
div.click(function () {
div.html('');
});
div.append($('<div>' + msg + '</div>'));
}
var list = $('#list');
var d = $('#d');
var input = $('#input');
input.tap(function (e) {
input.val(new Date().getTime());
});
list.tap(function (e) {
list.hide();
setTimeout(function () {
list.show();
}, 1000);
});
d.tap(function () {
log('div tap');
});
</script>
</html>
`
</pre>
這個頁面有三個元素
① 父容器div,我們?yōu)樗壎艘粋€tap事件,會打印文字
② 在上的div,我們?yōu)槠浣壎艘粋€tap事件,點擊便消失
③ input,主要用于測試focus問題
現(xiàn)在開啟touch事件的情況下,我們點擊上面的div,他會消失,于是:
div消失會觸發(fā)div(list)的tap事件
div消失會觸發(fā)input獲取焦點事件
提示層一閃而過
表單提交頁,用戶提交時如果信息有誤,會彈出一個提示,并且為蒙版添加click的關(guān)閉事件
但是有tap在的情況效果就不一樣了,我們極有可能點擊提交,彈出提示層,觸發(fā)蒙版點擊事件,蒙版關(guān)閉!!!
input獲取焦點彈出鍵盤
我們可能遇到這種情況,我們在彈出層上做了一些操作后,點擊彈出層關(guān)閉彈出層,但是下面有一個input(div有事件也行)
于是觸發(fā)了div事件,于是input獲取了焦點,某明奇妙的彈出來鍵盤!!!
以上都屬于touch事件導致的點透現(xiàn)象,有問題就有解決方案,于是我們就來說針對zepto如何解決點透現(xiàn)象
神奇菊花解決點透
此方案只針對zepto的tap事件
其實并不是所有的tap事件都會產(chǎn)生點透,只不過在頁面切換/有彈出層時候容易出現(xiàn)這個問題
根據(jù)zepto事件注冊機制我這里做了一點修改便可以解決zepto點透問題:于是這里便引進一個新的事件lazyTap
lazyTap只是名字變了,其實他還是tap,首先我們說事件注冊:
<pre>
el.on('lazyTap', function () { });
</pre>
如此我們就注冊了一個lazyTap事件,但是我們的zepto并不會因此而買賬,而且我也說了他就是tap事件,于是我們進入事件注冊入口:
<pre>
function add(element, events, fn, selector, getDelegate, capture) { var id = zid(element), set = (handlers[id] || (handlers[id] = [])) //元素上已經(jīng)綁定的所有事件處理函數(shù) eachEvent(events, fn, function (event, fn) { if (event == 'lazyTap') event = 'tap'; element.lazyTap = true; var handler = parse(event) //保存fn,下面為了處理mouseenter, mouseleave時,對fn進行了修改 handler.fn = fn handler.sel = selector // 模仿 mouseenter, mouseleave if (handler.e in hover) fn = function (e) { /* relatedTarget為事件相關(guān)對象,只有在mouseover和mouseout事件時才有值 mouseover時表示的是鼠標移出的那個對象,mouseout時表示的是鼠標移入的那個對象 當related不存在,表示事件不是mouseover或者mouseout,mouseover時!$.contains(this, related)當相關(guān)對象不在事件對象內(nèi) 且related !== this相關(guān)對象不是事件對象時,表示鼠標已經(jīng)從事件對象外部移入到了對象本身,這個時間是要執(zhí)行處理函數(shù)的 當鼠標從事件對象上移入到子節(jié)點的時候related就等于this了,且!$.contains(this, related)也不成立,這個時間是不需要執(zhí)行處理函數(shù)的 */ var related = e.relatedTarget if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments) } //事件委托 handler.del = getDelegate && getDelegate(fn, event) var callback = handler.del || fn handler.proxy = function (e) { var result = callback.apply(element, [e].concat(e.data)) //當事件處理函數(shù)返回false時,阻止默認操作和冒泡 if (result === false) e.preventDefault(), e.stopPropagation() return result } //設(shè)置處理函數(shù)的在函數(shù)集中的位置 handler.i = set.length //將函數(shù)存入函數(shù)集中 set.push(handler) element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) }) }
</pre>
這里5、6行,我們對我們傳入的事件類型進行了出來,將它改成了tap事件,并且在dom上打了一個標記
PS:zepto記錄事件句柄的zid也是記錄至dom屬性的
于是我們在觸發(fā)的時候可以這樣干:
<pre>
`
$.showLazyTap = function (e) {
var forTap = $('#forTap');
if (!forTap[0]) {
forTap = $('<div id="forTap" style="background: black;color: White; display: none; border-radius: 60px; position: absolute;
z-index: 99999; width: 60px; height: 60px"></div>');
$('body').append(forTap);
}
forTap.css({
top: (e.changedTouches[0].pageY - 30) + 'px',
left: (e.changedTouches[0].pageX - 30) + 'px'
})
forTap.show();
setTimeout(function () {
forTap.hide();
}, 350);
}
tapTimeout = setTimeout(function () {
var event = $.Event('tap')
event.cancelTouch = cancelAll
touch.el.trigger(event)
if (touch.el.lazyTap) {
$.showLazyTap(e);
}
}, 0)
`
</pre>
如此一來,在我們tap事件執(zhí)行后,我們會彈出一朵菊花,阻止我們與下面的元素觸碰,然后350ms后消失
這里去掉菊花的背景就完全沒有影響了,然后我們就解決了tap事件帶來的點透問題
放棄tap
最后我們開始評估,評估后的結(jié)果是放棄tap事件,放棄他主要有以下原因:
① 兼容問題,使用tap事件在電腦上操作不便,自動化測試無法進行
② 兼容問題,IE內(nèi)核的手機會完蛋
③ 點透解決方案不完美,蒙版形式不是所有人能接受,并且憑空多出一個lazyTap事件更是不該
所以我們放棄了這一方案,開始從根本上追尋問題,這正是我們最初的知識點的交合了
fastclick思想提升點擊響應(yīng)
程序界是一個神奇的地方,每當方案不夠完美時便會更加靠近真相,但當你真的想對真相著手時候,卻發(fā)現(xiàn)已經(jīng)有人干了!
前面已經(jīng)說過tap的種種弊端,所以原生的click事件依舊是最優(yōu)方案,于是我們可以在click上面打主意了
實現(xiàn)原理
依舊使用touch事件模擬點擊,卻在tap觸發(fā)那一步自己創(chuàng)建一個click的Event對象觸發(fā)之:
PS:這里需要手機測試了
<pre>
`
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style type="text/css">
#p { width: 300px; height: 300px; padding: 10px; border: 1px solid black; }
#c { width: 100px; height: 100px; border: 1px solid red; }
</style>
</head>
<body>
<input id="tap1" type="button" value="我是tap" /><br />
<input id="click1" type="button" value="我是click" />
<script type="text/javascript">
var tap1 = document.getElementById('tap1');
var click1 = document.getElementById('click1');
var t = 0, el;
document.addEventListener('touchstart', function (e) {
t = e.timeStamp;
el = e.target;
});
//注意,此處鼠標信息我沒有管他
function createEvent(type) {
var bubbles = true; //事件是否可以冒泡
var cancelable = true; //事件是否可以阻止瀏覽器默認事件
var view = document.defaultView; //與事件關(guān)聯(lián)的視圖,該屬性默認即可,不管
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時候用到,與事件相關(guān)的對象
var event = document.createEvent('MouseEvents');
event.initMouseEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
return event;
}
document.addEventListener('touchend', function (e) {
t = e.timeStamp;
var event = createEvent('tap')
//觸發(fā)tap事件
el.dispatchEvent(event);
//觸發(fā)click
var cEvent = createEvent('click');
el.dispatchEvent(cEvent);
});
function fnDom(el, msg, e) {
el.value = msg + '(' + (e.timeStamp - t) + ')';
}
tap1.addEventListener('tap', function (e) {
fnDom(this, '我是tap,我響應(yīng)時間:', e);
});
click1.addEventListener('click', function (e) {
fnDom(this, '我是click,我響應(yīng)時間:', e);
});
</script>
</body>
</html>
`
</pre>
http://sandbox.runjs.cn/show/8ruv88rb
這里我們點擊按鈕后就明顯看到了按鈕開始響應(yīng)時間是80左右,馬上變成了300多ms,因為click事件被執(zhí)行了兩次
一次是touchend我們手動執(zhí)行,一次是系統(tǒng)自建的click,這就是傳說中的鬼點擊,于是我們接下來說一說這個鬼點擊
鬼點擊
所謂鬼點擊,就是一次點擊執(zhí)行了兩次,以程序來說,他這個是正常的現(xiàn)象,沒有問題的,但是我們的業(yè)務(wù)邏輯不允許這個事情存在
初步解決鬼點擊是比較容易的,直接在touchend處阻止瀏覽器默認事件即可:
<pre>
document.addEventListener('touchend', function (e) { t = e.timeStamp; var event = createEvent('tap') //觸發(fā)tap事件 el.dispatchEvent(event); //觸發(fā)click var cEvent = createEvent('click'); el.dispatchEvent(cEvent); e.preventDefault(); });
</pre>
按道理來說,這個代碼是沒有問題的(而且同時可以解決我們的點透問題),但是在android上情況有所不同
我們的click依舊執(zhí)行了兩次!!!!!由此又引入了下一話題,android與ios鼠標事件差異
ios與android鼠標事件差異
PS:此點還要做詳細研究,今天淺淺的說幾點
在android上獲得的結(jié)果是驚人的,這個勞什子android里面moveover事件偶然比尼瑪touchstart還快!!!
而ios壓根就不理睬mouseover事件,這是主要問題產(chǎn)生原因!!!
而android在movedown時候,開開心心觸發(fā)了input的focus事件,然后鍵盤就彈起來了!!!
所以針對android,我們還得將mousedown干掉才行!!!!
而事實上,我們input獲取焦點,就是通過mousedown觸發(fā)的,ios也是,所以要解決android下面的問題還得從其它層面抓起
事件捕獲解決鬼點擊
<pre>
`
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style type="text/css">
.bt { position: absolute; top: 250px; display: block; height: 50px; }
</style>
</head>
<body>
<input type="button" class="bt" value="我是快速點擊事件" id="fastclick" />
<input type="text" style="width: 150px; height: 200px;" />
<div id="div" style="width: 200px; height: 200px; border: 1px solid black">
</div>
</body>
<script type="text/javascript">
var fastclick = document.getElementById('fastclick');
var div = document.getElementById('div');
var touch = {};
var t = new Date().getTime();
window.log = function (msg) {
var d = document.createElement('div');
d.innerHTML = msg;
div.appendChild(d);
console.log(msg);
};
document.addEventListener('click', function (event) {
if (event.myclick == true) {
return true;
}
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
} else {
event.propagationStopped = true;
}
event.stopPropagation();
event.preventDefault();
return true;
}, true);
document.addEventListener('touchstart', function (e) {
touch.startTime = e.timeStamp;
touch.el = e.target;
t = e.timeStamp;
});
document.addEventListener('touchmove', function (e) { });
document.addEventListener('touchend', function (e) {
touch.last = e.timeStamp;
var event = document.createEvent('Events');
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);
event.myclick = true;
touch.el && touch.el.dispatchEvent(event);
return true;
});
function fnDom(el, msg, e) {
el.value = msg + '(' + (e.timeStamp - t) + ')';
el.style.display = 'none';
setTimeout(function () {
el.style.display = '';
}, 1000)
}
fastclick.addEventListener('click', function (e) {
fnDom(this, '我是快速點擊事件', e);
log('快速點擊');
});
div.addEventListener('click', function (e) {
this.innerHTML += 'div<br/>'
});
</script>
</html>
`
</pre>
http://sandbox.runjs.cn/show/muk6q2br
最后追尋很久找到一個解決方案,該方案將上述知識點全部聯(lián)系起來了:
① 我們程序邏輯時先觸發(fā)touch事件,在touchend時候模擬click事件
② 這時我們給click事件對象一個屬性:
<pre>
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);
</pre>
③ 然后按照我們基礎(chǔ)篇的邏輯,我們事實上會先執(zhí)行document上的click事件
我們這里做了一個操作,判斷是否包含myclick屬性,有就直接跳出(事件會向下傳遞),如果沒有就阻止傳遞
到此,我們就解決了鬼點擊問題,當然,不夠完善
結(jié)語
此文有點過長,但是對javascript事件機制描述較為詳細,希望對各位有幫助。
原文地址 http://www.cnblogs.com/yexiaochai/p/3462657.html
博主敬上,由于簡書的markdown為文庫類.md,所以,原博主部分iframe均無法實現(xiàn),已圖片與連接方式實現(xiàn),望各位看官海涵