你若觸發 我就處理——淺談JavaScript的事件響應

chajn.org發表於2013-11-22

  每當猴子們問我JavaScript和DOM裡啥東西最牛逼時,我都會一巴掌打回去:臥槽還用問麼當然是事件響應了啊!沒它你能有時間和我討論這個?你早去工地搬磚去了好麼!瀏覽器沒有事件響應就沒有行為互動,那簡直就是一夜回到解放前的感覺啊。此外,以事件驅動使得功能解耦也是個相當高階大氣的技巧了,嘛,以此為主的Node.js 現在可是風生水起的說。

 

  那現在我們就再摳摳事件監聽的相關基礎,讓大家在心情愉悅的狀態下獲得更多的姿♂勢。不過那些經常寫<a href=”javascript:void(0)”>和在標籤上寫onclick=”foo()”的猴子們請自動迴避,小心你看不懂又想不開,老衲徒增罪孽呀(偶八年前就解釋了內聯事件處理是自尋死路)。

  再嘮叨兩句:本文的程式碼內容只涉及到原生JavaScript,像JQuery,YUI或Dojo什麼的所提供的事件處理這裡就不加以贅述了。我希望大家能夠明白,使用這些庫只是為了方便,但我們卻不能完全依賴它。瞭解基礎與理解本質是非常重要的,這樣你就可以在不能使用傻瓜庫的情況下,依舊可以提供一個牛逼的解決方案。

  進化主義宣告:這裡我們使用的事件語法是“DOM Level 3 Events”規範定義的“addEventListener()。除了IE9以下版本以外的現代瀏覽器都支援。當然,我們可以使用JQuery,它會幫我解決這些瀏覽器相容性的問題。但如果你還想讓網際網路可以良好發展和進化,你就應該立刻停止為相容低階瀏覽器而再去寫一坨屎一樣的傻逼相容程式碼。這條路雖然艱辛,但卻無比正確。可以試著給你的產品進行功能降級,檢測到是低階瀏覽器就不執行指令碼,比如addEventListener()的DOMContentLoaded事件就能確保你的程式碼不在低階瀏覽器中執行,而頁面可以將主體內容正常顯示就OK的。

  在我們進入事件的細節之前先看幾個牛逼的演示,它利用onscroll事件得到了一個很nice的效果:

  • 因為要招設計師,Wealthfront的工程師們開發了Z軸滾動平移效果。這也是Beercamp 2011 website的一部分。Wealthfront的部落格有細節介紹。
  • Stroll.js用的也是類似的手法,使用者可以在滾動列表時看到很多種炫酷展現。
  • jQuery Scroll Path是一個JQ外掛,它的功能是當使用者在頁面內能夠跟隨著一條路徑去動態瀏覽內容。

  以上所有都是基於瀏覽器的事件監聽和處理功能,所以,讓我們細細品味一下原汁原味的事件機制吧。

  基礎問題:啥是事件?

var log = document.getElementById('log'),
    i = '', 
    out = [];
for (i in window) {
  if ( /^on/.test(i)) { out[out.length] = i; }
}
log.innerHTML = out.join(', ');

  在瀏覽器中執行如上程式碼,親們可以得到如下:

  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, onafterscriptexecute

  一大坨事件就夠你吃幾天的了,用addEventListener()方法可以進行事件監聽:

element.addEventListener(event, handler, useCapture);

  舉個例子來說:

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這逗比例外…

  在事件被觸發之時,回撥方法會接收到一個事件物件。請試著在恰當的環境中執行如下程式碼,也可以直接點選這裡測試這個例子,物件內的屬性又夠吃一盆的:

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是傳回來的事件物件,下面是它所帶的全部屬性:

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:

<a class="prevent" href="http://smashingmagazine.com">Smashing, my dear!</a>
<a class="normal" href="http://smashingmagazine.com">Smashing, my dear!</a>

  還有JavaScript:

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 屬性得到觸發元素並作出回應。來看我下面的例子

<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 來進行區分。

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獨立起來,比如之後還有要加子元素的需求,也不需要再為其單獨加事件監聽了。

  事件監測,順便說一下超級牛逼的CSS平滑效果

  以前我們會用mouseover|mouseout事件來暗挫挫的實現hover效果,而現在用CSS偽選擇器的:hover和:focus什麼的就直接搞定了。想到這裡,內心止不住的傷感啊……魔法師們堅持住!咳咳,當然,CSS也並不是萬能的,有些事情還是要跟事件配合完成,比如下面這個例子,對滑鼠指標進行定位。這是相當簡單的了是不,我們先搞個絕對定位的小球元素,下面是它的HTML:

<div class="plot"></div>

  這是它的CSS:

.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對小球進行定位。注意啊這裡,我們需要減去球的半徑,以讓球的中心在滑鼠指標上:

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的樣式,在核取方塊被選中之後,將其應用到小球上:

.smooth {
  -webkit-transition: 0.5s;
     -moz-transition: 0.5s;
      -ms-transition: 0.5s;
       -o-transition: 0.5s;
          transition: 0.5s;
}

  新增JavaScript:

var cb = document.querySelector('input[type=checkbox]');
cb.addEventListener('click', function(ev) {
  plot.classList.toggle('smooth');
}, false);

  隨著新世界的來臨,CSS和JavaScript雙劍合璧,誰與爭鋒!嘛順便說下,在JS中也有跟CSS過渡和動畫效果有關的事件噢。

  擼一個鍵需要多長時間?

  正如你之前在可用事件列表中看到的,我們也可以監聽按鍵輸入事件。不過很遺憾的是,瀏覽器對鍵盤事件處理的並不是很到位,你可以看看Jan Wolter對此的詳細解釋接下來讓我們看一個keytime的例子,它會輸出使用者按鍵的毫秒間隔。程式碼並不難:

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過渡(動畫)效果

  當瀏覽器執行CSS過渡效果是會在JavaScript中觸發一個獨立事件,叫transitionend。這個事件物件會有兩個屬性:被其所影響到的屬性名propertyName,和其過渡所經歷的時間elapsedTime。

  可以檢視這個demo感受一下,程式碼很簡單,下面是它的CSS:

.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:

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

  速度,距離和角度,沒有問題!

  事件我們是監聽到了,但如果想讓它更加屌炸天,我們就需要再來點有深度的,比如在使用者在拖拽元素時,給元素來個計算角度、距離和速度差什麼的——示例

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 eventsthe Gamepad API讓我們可以在瀏覽器中做遊戲控制;postMessage讓我們可以在瀏覽器各視窗之間進行跨域訊息傳遞;pageVisibility讓我們可以得知瀏覽器中當前標籤頁可見狀態。甚至當window的history物件有操作時也能監聽的到。檢視window物件的事件列表,有的可能已經被實現了,還有更多的在謀劃中……

  嘛,不管瀏覽器是否會支援,最終都是要支援的嘛,這些是剛需。我們只要默默等待就可以了,騷年,向著夕陽奔跑吧!=v=

  玩出個未來

  基本上就這樣了,看,事件並不難。一般情況下,你只需要註冊監聽他們,然後在事件處理函式中使用event物件就好了。如果到現在你還沒想到它能做些什麼有趣的事情,那這篇文章也只能幫你倒這裡了。別再管上文的那些例子了,請你用你那上鏽的腦袋好好思考下,玩憤怒小鳥時候,不就是監聽個觸控事件的開始與結束,再處理下相應的方向和距離差所得出的射擊力量麼?最後嗖的一聲,小鳥就自由飛翔在遠方了~。所以,究竟是什麼阻止了你的創意?該加了個油了,同學。

  原文出處: javascript-events-responding-user

相關文章