原文出處: Christian Heilmann 譯文出處:Chajn Science javascript
每當猴子們問我JavaScript和DOM裏啥東西最牛逼時,我都會一巴掌打回去:臥槽還用問麼固然是事件響應了啊!沒它你能有時間和我討論這個?你早去工地搬磚去了好麼!瀏覽器沒有事件響應就沒有行爲交互,那簡直就是一晚上回到解放前的感受啊。此外,以事件驅動使得功能解耦也是個至關高端大氣的技巧了,嘛,以此爲主的Node.js 如今但是風生水起的說。css
那如今咱們就再摳摳事件監聽的相關基礎,讓你們在心情愉悅的狀態下得到更多的姿♂勢。不過那些常常寫<a href=」javascript:void(0)」>和在標籤上寫onclick=」foo()」的猴子們請自動迴避,當心你看不懂又想不開,老衲徒增罪孽呀(偶八年前就解釋了內聯事件處理是自尋死路)。html
再嘮叨兩句:本文的代碼內容只涉及到原生JavaScript,像JQuery,YUI或Dojo什麼的所提供的事件處理這裏就不加以贅述了。我但願你們可以明白,使用這些庫只是爲了方便,但咱們卻不能徹底依賴它。瞭解基礎與理解本質是很是重要的,這樣你就能夠在不能使用傻瓜庫的狀況下,依舊能夠提供一個牛逼的解決方案。html5
進化主義聲明:這裏咱們使用的事件語法是「DOM Level 3 Events」規範定義的「addEventListener()。除了IE9如下版本之外的現代瀏覽器都支持。固然,咱們可使用JQuery,它會幫我解決這些瀏覽器兼容性的問題。但若是你還想讓互聯網能夠良好發展和進化,你就應該馬上中止爲兼容低級瀏覽器而再去寫一坨屎同樣的傻逼兼容代碼。這條路雖然艱辛,但卻無比正確。能夠試着給你的產品進行功能降級,檢測到是低級瀏覽器就不執行腳本,好比addEventListener()的DOMContentLoaded事件就能確保你的代碼不在低級瀏覽器中運行,而頁面能夠將主體內容正常顯示就OK的。java
在咱們進入事件的細節以前先看幾個牛逼的演示,它利用onscroll事件獲得了一個很nice的效果:node
以上全部都是基於瀏覽器的事件監聽和處理功能,因此,讓咱們細細品味一下原汁原味的事件機制吧。git
1
2
3
4
5
6
7
|
var
log = document.getElementById(
'log'
),
i =
''
,
out = [];
for
(i
in
window) {
if
( /^on/.test(i)) { out[out.length] = i; }
}
log.innerHTML = out.join(
', '
);
|
在瀏覽器中運行如上代碼,親們能夠獲得以下:github
onmouseenter, onmouseleave, onafterprint, onbeforeprint, onbeforeunload, onhashchange, onmessage, onoffline, ononline, onpopstate, onpagehide, onpageshow, onresize, onunload, ondevicemotion, ondeviceorientation, onabort, onblur, oncanplay, oncanplaythrough, onchange, onclick, oncontextmenu, ondblclick, ondrag, ondragend, ondragenter, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, onerror, onfocus, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmousemove, onmouseout, onmouseover, onmouseup, onmozfullscreenchange, onmozfullscreenerror, onpause, onplay, onplaying, onprogress, onratechange, onreset, onscroll, onseeked, onseeking, onselect, onshow, onstalled, onsubmit, onsuspend, ontimeupdate, onvolumechange, onwaiting, oncopy, oncut, onpaste, onbeforescriptexecute, onafterscriptexecuteweb
一大坨事件就夠你吃幾天的了,用addEventListener()方法能夠進行事件監聽:ajax
1
|
element.addEventListener(event, handler, useCapture);
|
舉個例子來講:
1
2
|
var
a = document.querySelector(
'a'
);
// grab the first link in the document
a.addEventListener(
'click'
, ajaxloader,
false
);
|
咱們在一個element上加了個事件監聽,就好像是在命令她,「你被客人摸了你就給我喊起來!」 The ajaxloader()是監聽事件的回調方法,就好像是,「你就在這兒給我盯着,妞要是喊了,你就過去給客人道歉!」 將第三個參數useCapture設置爲false是爲了表示此次是在事件冒泡階段進行觸發,而不是在事件捕獲階段。咳咳,這是一個漫長而艱鉅的課題,你也能夠看看Dev.Opera對capture的解釋。哎呀反正不用管那麼多啦,99.7434%的狀況下設置useCapture爲false準沒錯!其實它默認就是false,因此按理來講是能夠不用填寫的,但Opera這逗比例外…
在事件被觸發之時,回調方法會接收到一個事件對象。請試着在恰當的環境中運行以下代碼,也能夠直接點擊這裏測試這個例子,對象內的屬性又夠吃一盆的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
var
log = document.getElementById(
'log'
),
out =
''
;
document.addEventListener(
'click'
, logeventinfo,
false
);
document.addEventListener(
'keypress'
, logeventinfo,
false
);
function
logeventinfo (ev) {
log.innerHTML =
''
;
out =
'<ul>'
;
for
(
var
i
in
ev) {
if
(
typeof
ev[i] ===
'function'
|| i === i.toUpperCase()) {
continue
;
}
out +=
'<li><span>'
+i+
'</span>: '
+ev[i]+
'</li>'
;
}
log.innerHTML += out +
'</ul>'
;
}
|
你能夠對同一事件進行多重監聽,也能夠對多個事件使用同一方法處理(如本例)。
參數ev是傳回來的事件對象,下面是它所帶的所有屬性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
originalTarget: [object HTMLHtmlElement]
type: click
target: [object HTMLHtmlElement]
currentTarget: [object HTMLDocument]
eventPhase: 3
bubbles:
true
cancelable:
true
timeStamp: 574553210
defaultPrevented:
false
which: 1
rangeParent: [object Text]
rangeOffset: 23
pageX: 182
pageY: 111
isChar:
false
screenX: 1016
screenY: 572
clientX: 182
clientY: 111
ctrlKey:
false
shiftKey:
false
altKey:
false
metaKey:
false
button: 0
relatedTarget:
null
mozPressure: 0
mozInputSource: 1
view: [object Window]
detail: 1
layerX: 182
layerY: 111
cancelBubble:
false
explicitOriginalTarget: [object HTMLHtmlElement]
isTrusted:
true
originalTarget: [object HTMLHeadingElement]
type: click
target: [object HTMLHeadingElement]
currentTarget: [object HTMLDocument]
eventPhase: 3
bubbles:
true
cancelable:
true
timeStamp: 574554192
defaultPrevented:
false
which: 1
rangeParent: [object Text]
rangeOffset: 0
pageX: 1
pageY: 18
isChar:
false
screenX: 835
screenY: 479
clientX: 1
clientY: 18
ctrlKey:
false
shiftKey:
false
altKey:
false
metaKey:
false
button: 0
relatedTarget:
null
mozPressure: 0
mozInputSource: 1
view: [object Window]
detail: 1
layerX: 1
layerY: 18
cancelBubble:
false
explicitOriginalTarget: [object Text]
isTrusted:
true
|
試一下本例中點擊鼠標和按鍵盤,不一樣的事件觸發傳回來的結果是不一樣的。能夠看看完整的標準事件屬性列表。
咱們須要瞭解瀏覽器中關於事件對象有兩個很重要的功能。阻止瀏覽器執行事件默認行爲的ev.preventDefault(),和能夠得到事件目標元素的ev.target.
好比說一個連接被點擊了,但由於業務須要,咱們並不想讓瀏覽器打開新頁面。這時候能夠給這個連接加個點擊事件監聽,而後在回調函數中調用 preventDefault()方法便可。昂,就以下面這個例子,請看HTML:
1
2
|
<a class=
"prevent"
href=
"http://smashingmagazine.com"
>Smashing, my dear!</a>
<a class=
"normal"
href=
"http://smashingmagazine.com"
>Smashing, my dear!</a>
|
還有JavaScript:
1
2
3
4
5
6
7
8
9
10
11
|
var
normal = document.querySelector(
'.normal'
),
prevent = document.querySelector(
'.prevent'
);
prevent.addEventListener(
'click'
,
function
(ev) {
alert(
'fabulous, really!'
);
ev.preventDefault();
},
false
);
normal.addEventListener(
'click'
,
function
(ev) {
alert(
'fabulous, really!'
);
},
false
);
|
注意: document.querySelector() 是合理獲取DOM元素的一種方式。和jQuery的 $() 差很少。 能夠讀讀 W3C’s specification 和MDN的 explanatory code snippets 去了解。
若是點擊.prevent連接,會彈出個對話框,點「肯定」後啥事都沒發生,呵~呵~,由於處理中有執行ev.preventDefault(),因此不會跳到 http://smashingmagazine.com。沒有 preventDefault()的就會在彈對話框,且跳連接咯。不信你能夠試一下嘛。
一般狀況下,處理事件的方法想要訪問觸發元素只能經過變量和this什麼的去關聯,雖然看似簡單方便,但addEventListener()給了咱們更好的選擇:事件目標(target),不過它可能被其餘的一些東西混淆,因此用ev.currentTarget更保險些。一般想要在點擊、懸停或輸入事件的回調方法中訪問觸發元素都是用變量 this 。雖然方便,但這個關鍵字你懂得…因而 addEventListener() 提供給咱們一個更好的獲取方式:event.target。 不過它可能會被混淆( this 被綁到奇怪的東西上的時候),因此用 ev.currentTarget 更保險些。
用事件對象的 target 屬性,你能夠獲得觸發事件的元素。
事件被激活後,會像猴子同樣沿着DOM樹從監聽節點下滑到觸發節點,而後再上爬回監聽節點。也就是說,若是你監聽了一個DOM節點,那也就等於你監聽了其全部的後代節點。代理的意思就是隻監聽父節點的事件觸發,以來代理對其後代節點的監聽,而你須要作的只是經過 target 屬性獲得觸發元素並做出迴應。來看我下面的例子:
1
2
3
4
5
6
7
|
<ul id=
"resources"
>
<li><a href=
"http://developer.mozilla.org"
>MDN</a></li>
<li><a href=
"http://html5doctor.com"
>HTML5 Doctor</a></li>
<li><a href=
"http://html5rocks.com"
>HTML5 Rocks</a></li>
<li><a href=
"http://beta.theexpressiveweb.com/"
>Expressive Web</a></li>
<li><a href=
"http://creativeJS.com/"
>CreativeJS</a></li>
</ul>
|
這個例子中的HTML結構是個無序列表,當鼠標懸停會顯示相應的標籤信息。下面是它JS代碼,你將看到它只用到了一個事件監聽,而後在處理函數中獲得target,以它的 tagName 來進行區分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
var
resources = document.querySelector(
'#resources'
),
log = document.querySelector(
'#log'
);
resources.addEventListener(
'mouseover'
, showtarget,
false
);
function
showtarget(ev) {
var
target = ev.target;
if
(target.tagName ===
'A'
) {
log.innerHTML =
'A link, with the href:'
+ target.href;
}
if
(target.tagName ===
'LI'
) {
log.innerHTML =
'A list item'
;
}
if
(target.tagName ===
'UL'
) {
log.innerHTML =
'The list itself'
;
}
}
|
我費事吧啦解釋了半天,乃們懂得這麼作意味着什麼麼?這意味着你能夠節省大量重複的事件監聽,以減小瀏覽器資源消耗。大多數人可能會用jQuery的$(‘a’).click(…) 啥啥啥的(雖然 jQuery的 on 方法優化的還OK啦不過偶仍是蠻鄙視他的),這麼作看似一句話蠻帶感的,可其實它是把獲取到的全部A元素一個一個的註冊監聽!而後在某個時刻,充斥着無數事件監聽的頁面終於覺累不愛,自爆以鳴冤屈。
她還有一個好處就是讓HTML獨立起來,好比以後還有要加子元素的需求,也不須要再爲其單獨加事件監聽了。
之前咱們會用mouseover|mouseout事件來暗挫挫的實現hover效果,而如今用CSS僞選擇器的:hover和:focus什麼的就直接搞定了。想到這裏,心裏止不住的傷感啊……魔法師們堅持住!咳咳,固然,CSS也並非萬能的,有些事情仍是要跟事件配合完成,好比下面這個例子,對鼠標指針進行定位。這是至關簡單的了是不,咱們先搞個絕對定位的小球元素,下面是它的HTML:
1
|
<div class=
"plot"
></div>
|
這是它的CSS:
1
2
3
4
5
6
7
8
9
10
|
.plot {
position:absolute;
background:rgb(175,50,50);
width: 20px;
height: 20px;
border-radius: 20px;
display: block;
top:0;
left:0;
}
|
咱們監聽並處理doucment的click事件,利用PageX和pageY對小球進行定位。注意啊這裏,咱們須要減去球的半徑,以讓球的中心在鼠標指針上:
1
2
3
4
5
6
|
var
plot = document.querySelector(
'.plot'
),
offset = plot.offsetWidth / 2;
document.addEventListener(
'click'
,
function
(ev) {
plot.style.left = (ev.pageX - offset) +
'px'
;
plot.style.top = (ev.pageY - offset) +
'px'
;
},
false
);
|
隨便點擊屏幕的任意位置,小球都會隨之閃現到那。不過它並非平滑過去的,但若是你勾選這個示例的複選框,你會發現小球就會很圓潤的滑過來了。這個效果呢,過去的話可能只能用JS庫來完成,但如今啊,時代不一樣了……咱們只須要用CSS寫個過渡效果的類,而剩下的事情就讓瀏覽器去處理。爲至於此,咱們寫個類名爲smooth的樣式,在複選框被選中以後,將其應用到小球上:
1
2
3
4
5
6
7
|
.smooth {
-webkit-transition: 0.5s;
-moz-transition: 0.5s;
-ms-transition: 0.5s;
-o-transition: 0.5s;
transition: 0.5s;
}
|
添加JavaScript:
1
2
3
4
|
var
cb = document.querySelector(
'input[type=checkbox]'
);
cb.addEventListener(
'click'
,
function
(ev) {
plot.classList.toggle(
'smooth'
);
},
false
);
|
隨着新世界的來臨,CSS和JavaScript雙劍合璧,誰與爭鋒!嘛順便說下,在JS中也有跟CSS過渡和動畫效果有關的事件噢。
正如你以前在可用事件列表中看到的,咱們也能夠監聽按鍵輸入事件。不過很遺憾的是,瀏覽器對鍵盤事件處理的並非很到位,你能夠看看Jan Wolter對此的詳細解釋接下來讓咱們看一個keytime的例子,它會輸出用戶按鍵的毫秒間隔。代碼並不難:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
var
resources = document.querySelector(
'#resources'
),
log = document.querySelector(
'#log'
),
time = 0;
document.addEventListener(
'keydown'
, keydown,
false
);
document.addEventListener(
'keyup'
, keyup,
false
);
function
keydown(ev) {
if
(time === 0) {
time = ev.timeStamp;
log.classList.add(
'animate'
);
}
}
function
keyup(ev) {
if
(time !== 0) {
log.innerHTML = ev.timeStamp - time;
time = 0;
log.classList.remove(
'animate'
);
}
}
|
先定義咱們想要操縱的元素並設置time爲0。而後咱們在document上監聽兩個鍵盤輸入事件 keydown和keyup。
在keydown事件處理中,咱們檢查變量time是否爲0,若是是則把事件對象的timeStamp賦值給time。再加個CSS動畫類animate給log節點,讓它向滾動條同樣動起來
在keyup事件處理中,若是time仍是爲0則忽略(在按着鍵盤的期間keydown事件是接二連三被觸發的),若是不是則經過二者時間戳相減去計算一次按鍵操做通過多長時間。最後讓time爲0並移除log節點的animate類
當瀏覽器運行CSS過渡效果是會在JavaScript中觸發一個獨立事件,叫transitionend。這個事件對象會有兩個屬性:被其所影響到的屬性名propertyName,和其過渡所經歷的時間elapsedTime。
能夠查看這個demo感覺一下,代碼很簡單,下面是它的CSS:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
.plot {
background:rgb(175,50,50);
width: 20px;
height: 20px;
border-radius: 20px;
display: block;
-webkit-transition: 0.5s;
-moz-transition: 0.5s;
-ms-transition: 0.5s;
-o-transition: 0.5s;
transition: 0.5s;
}
.plot:hover {
width: 50px;
height: 50px;
border-radius: 100px;
background: blue;
}
|
這是它的JavaScript:
1
2
3
|
plot.addEventListener(
'transitionend'
,
function
(ev) {
log.innerHTML += ev.propertyName +
':'
+ ev.elapsedTime +
's '
;
},
false
);
|
但由於Fire/Chrome/Safari/Opera等這些瀏覽器廠商各自爲政,也是由於這些事件還不成熟,因此這些事件名一般都會被加上前綴,那在使用時你就不得不判斷下瀏覽器兼容性。能夠看看這個David Calhoun’s gist。
CSS動畫事件和上面演示的過渡事件基本一個意思,它有三個事件:animationstart,animationend和animationiteration。能夠看MDN的demo。
事件咱們是監聽到了,但若是想讓它更加屌炸天,咱們就須要再來點有深度的,好比在用戶在拖拽元素時,給元素來個計算角度、距離和速度差什麼的——示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
var
plot = document.querySelector(
'.plot'
),
log = document.querySelector(
'output'
),
offset = plot.offsetWidth / 2,
pressed =
false
,
start = 0, x = 0, y = 0, end = 0, ex = 0, ey = 0, mx = 0, my = 0,
duration = 0, dist = 0, angle = 0;
document.addEventListener(
'mousedown'
, onmousedown,
false
);
document.addEventListener(
'mouseup'
, onmouseup,
false
);
document.addEventListener(
'mousemove'
, onmousemove,
false
);
function
onmousedown(ev) {
if
(start === 0 && x === 0 && y === 0) {
start = ev.timeStamp;
x = ev.clientX;
y = ev.clientY;
moveplot(x, y);
pressed =
true
;
}
}
function
onmouseup(ev) {
end = ev.timeStamp;
duration = end - start;
ex = ev.clientX;
ey = ev.clientY;
mx = ex - x;
my = ey - y;
dist = Math.sqrt(mx * mx + my * my);
start = x = y = 0;
pressed =
false
;
angle = Math.atan2( my, mx ) * 180 / Math.PI;
log.innerHTML =
'<strong>'
+ (dist>>0) +
'</strong> pixels in <strong>'
+
duration +
'</strong> ms ( <strong>'
+
twofloat(dist/duration) +
'</strong> pixels/ms)'
+
' at <strong>'
+ twofloat(angle) +
'</strong> degrees'
;
}
function
onmousemove (ev) {
if
(pressed) {
moveplot(ev.pageX, ev.pageY);
}
}
function
twofloat(val) {
return
Math.round((val*100))/100;
}
function
moveplot(x, y) {
plot.style.left = (x - offset) +
'px'
;
plot.style.top = (y - offset) +
'px'
;
}
|
好啦,好像作了不少事情的樣子,但事實上並無那麼複雜。監聽onmousedown和onmouseup兩個事件,咱們能獲得鼠標當前位置clientX和clientY,還有記錄按鍵時間的timeStamp。當鼠標移動時,檢查鼠標是否被按下了(經過在mousedown時設定的布爾值),若是按下了則讓小球跟着鼠標移動。
而後是幾何——Pythagoras(畢達哥拉斯定理)經過mousedown和mouseup的時間間隔和像素位移而得出它運動的速度。
咱們獲得了運動開始和結束的xy座標,相減獲得距離差,再平方相加,最後獲得和的平方根,即爲小球運動的位移。嘛咱們還經過計算三角形的反正切獲得了它運動先後的偏轉角度,高端吧!嘿嘿其實這些都是抄「A Quick Look Into the Math of Animations With JavaScript」的……你能夠看下這個在JSFiddle上的示例:
http://jsfiddle.net/codepo8/bAwUf/light/
video和audio這倆很潮的玩意也有一大堆事件供咱們使用。好比有趣的time事件,它能夠告訴咱們這首歌或電影的已播放時長。能夠看看MDN這個MGM-inspired dinosaur animation;我也沒事閒的錄製了一個six-minute screencast來玩弄了一下~。
想看全部的事件動做?去JPlayer看媒體事件的演示頁面吧。
咱們知道,瀏覽器提供了與鼠標鍵盤的交互,但這還遠遠不夠知足咱們更多的硬件交互需求。好比檢測手機或平板電腦傾斜度的Device orientation和 touch events。the Gamepad API讓咱們能夠在瀏覽器中作遊戲控制;postMessage讓咱們能夠在瀏覽器各窗口之間進行跨域消息傳遞;pageVisibility讓咱們能夠得知瀏覽器中當前標籤頁可見狀態。甚至當window的history對象有操做時也能監聽的到。查看window對象的事件列表,有的可能已經被實現了,還有更多的在謀劃中……
嘛,無論瀏覽器是否會支持,最終都是要支持的嘛,這些是剛需。咱們只要默默等待就能夠了,騷年,向着夕陽奔跑吧!=v=
基本上就這樣了,看,事件並不難。通常狀況下,你只須要註冊監聽他們,而後在事件處理函數中使用event對象就行了。若是到如今你還沒想到它能作些什麼有趣的事情,那這篇文章也只能幫你倒這裏了。別再管上文的那些例子了,請你用你那上鏽的腦殼好好思考下,玩憤怒小鳥時候,不就是監聽個觸摸事件的開始與結束,再處理下相應的方向和距離差所得出的射擊力量麼?最後嗖的一聲,小鳥就自由飛翔在遠方了~。因此,到底是什麼阻止了你的創意?該加了個油了,同窗。