Javascript 瀏覽器事件簡介

2023-02-17 10:54 更新

事件 是某事發(fā)生的信號。所有的 DOM 節(jié)點都生成這樣的信號(但事件不僅限于 DOM)。

這是最有用的 DOM 事件的列表,你可以瀏覽一下:

鼠標事件:

  • ?click? —— 當鼠標點擊一個元素時(觸摸屏設(shè)備會在點擊時生成)。
  • ?contextmenu? —— 當鼠標右鍵點擊一個元素時。
  • ?mouseover? / ?mouseout? —— 當鼠標指針移入/離開一個元素時。
  • ?mousedown? / ?mouseup? —— 當在元素上按下/釋放鼠標按鈕時。
  • ?mousemove? —— 當鼠標移動時。

鍵盤事件

  • ?keydown? 和 ?keyup? —— 當按下和松開一個按鍵時。

表單(form)元素事件

  • ?submit? —— 當訪問者提交了一個 ?<form>? 時。
  • ?focus? —— 當訪問者聚焦于一個元素時,例如聚焦于一個 ?<input>?。

Document 事件

  • ?DOMContentLoaded? —— 當 HTML 的加載和處理均完成,DOM 被完全構(gòu)建完成時。

CSS 事件

  • ?transitionend? —— 當一個 CSS 動畫完成時。

還有很多其他事件。我們將在下一章中詳細介紹具體事件。

事件處理程序

為了對事件作出響應(yīng),我們可以分配一個 處理程序(handler)—— 一個在事件發(fā)生時運行的函數(shù)。

處理程序是在發(fā)生用戶行為(action)時運行 JavaScript 代碼的一種方式。

有幾種分配處理程序的方法。讓我們來看看,從最簡單的開始。

HTML 特性

處理程序可以設(shè)置在 HTML 中名為 on<event> 的特性(attribute)中。

例如,要為一個 input 分配一個 click 處理程序,我們可以使用 onclick,像這樣;

<input value="Click me" onclick="alert('Click!')" type="button">

在鼠標點擊時,onclick 中的代碼就會運行。

請注意,在 onclick 中,我們使用單引號,因為特性本身使用的是雙引號。如果我們忘記了代碼是在特性中的,而使用了雙引號,像這樣:onclick="alert("Click!")",那么它就無法正確運行。

HTML 特性不是編寫大量代碼的好位置,因此我們最好創(chuàng)建一個 JavaScript 函數(shù),然后在 HTML 特性中調(diào)用這個函數(shù)。

我們知道,HTML 特性名是大小寫不敏感的,所以 ONCLICK 和 onClick 以及 onCLICK 都一樣可以運行。但是特性通常是小寫的:onclick。

DOM 屬性

我們可以使用 DOM 屬性(property)on<event> 來分配處理程序。

例如 elem.onclick

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you');
  };
</script>

如果一個處理程序是通過 HTML 特性(attribute)分配的,那么隨后瀏覽器讀取它,并從特性的內(nèi)容創(chuàng)建一個新函數(shù),并將這個函數(shù)寫入 DOM 屬性(property)。

因此,這種方法實際上與前一種方法相同。

這兩段代碼工作相同:

  1. 只有 HTML:
  2. <input type="button" onclick="alert('Click!')" value="Button">
    
  3. HTML + JS:
  4. <input type="button" id="button" value="Button">
    <script>
      button.onclick = function() {
        alert('Click!');
      };
    </script>

在第一個例子中,button.onclick 是通過 HTML 特性(attribute)初始化的,而在第二個例子中是通過腳本初始化的。這是它們唯一的不同之處。

因為這里只有一個 onclick 屬性,所以我們無法分配更多事件處理程序。

在下面這個示例中,我們使用 JavaScript 添加了一個處理程序,覆蓋了現(xiàn)有的處理程序:

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // 覆蓋了現(xiàn)有的處理程序
    alert('After'); // 只會顯示此內(nèi)容
  };
</script>

要移除一個處理程序 —— 賦值 elem.onclick = null。

訪問元素:this

處理程序中的 this 的值是對應(yīng)的元素。就是處理程序所在的那個元素。

下面這行代碼中的 button 使用 this.innerHTML 來顯示它的內(nèi)容:

<button onclick="alert(this.innerHTML)">Click me</button>

可能出現(xiàn)的錯誤

如果你剛開始寫事件 —— 請注意一些細微之處。

我們可以將一個現(xiàn)存的函數(shù)用作處理程序:

function sayThanks() {
  alert('Thanks!');
}

elem.onclick = sayThanks;

但要注意:函數(shù)應(yīng)該是以 sayThanks 的形式進行賦值,而不是 sayThanks()

// 正確
button.onclick = sayThanks;

// 錯誤
button.onclick = sayThanks();

如果我們添加了括號,那么 sayThanks() 就變成了一個函數(shù)調(diào)用。所以,最后一行代碼實際上獲得的是函數(shù)執(zhí)行的 結(jié)果,即 undefined(因為這個函數(shù)沒有返回值)。此代碼不會工作。

……但在標記(markup)中,我們確實需要括號:

<input type="button" id="button" onclick="sayThanks()">

這個區(qū)別很容易解釋。當瀏覽器讀取 HTML 特性(attribute)時,瀏覽器將會使用 特性中的內(nèi)容 創(chuàng)建一個處理程序。

所以,標記(markup)會生成下面這個屬性:

button.onclick = function() {
  sayThanks(); // <-- 特性(attribute)中的內(nèi)容變到了這里
};

不要對處理程序使用 setAttribute。

這樣的調(diào)用會失效:

// 點擊 <body> 將產(chǎn)生 error,
// 因為特性總是字符串的,函數(shù)變成了一個字符串
document.body.setAttribute('onclick', function() { alert(1) });

DOM 屬性是大小寫敏感的。

將處理程序分配給 elem.onclick,而不是 elem.ONCLICK,因為 DOM 屬性是大小寫敏感的。

addEventListener

上述分配處理程序的方式的根本問題是 —— 我們不能為一個事件分配多個處理程序。

假設(shè),在我們點擊了一個按鈕時,我們代碼中的一部分想要高亮顯示這個按鈕,另一部分則想要顯示一條消息。

我們想為此事件分配兩個處理程序。但是,新的 DOM 屬性將覆蓋現(xiàn)有的 DOM 屬性:

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // 替換了前一個處理程序

Web 標準的開發(fā)者很早就了解到了這一點,并提出了一種使用特殊方法 addEventListener 和 removeEventListener 來管理處理程序的替代方法。它們沒有這樣的問題。

添加處理程序的語法:

element.addEventListener(event, handler[, options]);

?event ?

事件名,例如:?"click"?。

?handler ?

處理程序。

?options ?

具有以下屬性的附加可選對象:

  • ?once?:如果為 ?true?,那么會在被觸發(fā)后自動刪除監(jiān)聽器。
  • ?capture?:事件處理的階段,我們稍后將在 冒泡和捕獲 一章中介紹。由于歷史原因,?options? 也可以是 ?false/true?,它與 ?{capture: false/true}? 相同。
  • ?passive?:如果為 ?true?,那么處理程序?qū)⒉粫{(diào)用 ?preventDefault()?,我們稍后將在 瀏覽器默認行為 一章中介紹。

要移除處理程序,可以使用 removeEventListener

element.removeEventListener(event, handler[, options]);

移除需要相同的函數(shù)

要移除處理程序,我們需要傳入與分配的函數(shù)完全相同的函數(shù)。

這不起作用:

elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));

處理程序不會被移除,因為 removeEventListener 獲取了另一個函數(shù) —— 使用相同的代碼,但這并不起作用,因為它是一個不同的函數(shù)對象。

下面是正確方法:

function handler() {
  alert( 'Thanks!' );
}

input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);

請注意 —— 如果我們不將函數(shù)存儲在一個變量中,那么我們就無法移除它。由 addEventListener 分配的處理程序?qū)o法被“讀回”。

多次調(diào)用 addEventListener 允許添加多個處理程序,如下所示:

<input id="elem" type="button" value="Click me"/>

<script>
  function handler1() {
    alert('Thanks!');
  };

  function handler2() {
    alert('Thanks again!');
  }

  elem.onclick = () => alert("Hello");
  elem.addEventListener("click", handler1); // Thanks!
  elem.addEventListener("click", handler2); // Thanks again!
</script>

正如我們在上面這個例子中所看到的,我們可以 同時 使用 DOM 屬性和 addEventListener 來設(shè)置處理程序。但通常我們只使用其中一種方式。

對于某些事件,只能通過 ?addEventListener? 設(shè)置處理程序

有些事件無法通過 DOM 屬性進行分配。只能使用 addEventListener。

例如,DOMContentLoaded 事件,該事件在文檔加載完成并且 DOM 構(gòu)建完成時觸發(fā)。

// 永遠不會運行
document.onDOMContentLoaded = function() {
  alert("DOM built");
};
// 這種方式可以運行
document.addEventListener("DOMContentLoaded", function() {
  alert("DOM built");
});

所以 addEventListener 更通用。雖然這樣的事件是特例而不是規(guī)則。

事件對象

為了正確處理事件,我們需要更深入地了解發(fā)生了什么。不僅僅是 “click” 或 “keydown”,還包括鼠標指針的坐標是什么?按下了哪個鍵?等等。

當事件發(fā)生時,瀏覽器會創(chuàng)建一個 event 對象,將詳細信息放入其中,并將其作為參數(shù)傳遞給處理程序。

下面是一個從 event 對象獲取鼠標指針的坐標的示例:

<input type="button" value="Click me" id="elem">

<script>
  elem.onclick = function(event) {
    // 顯示事件類型、元素和點擊的坐標
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
</script>

event 對象的一些屬性:

?event.type ?

事件類型,這里是 ?"click"?。

?event.currentTarget ?

處理事件的元素。這與 ?this? 相同,除非處理程序是一個箭頭函數(shù),或者它的 ?this? 被綁定到了其他東西上,之后我們就可以從 ?event.currentTarget? 獲取元素了。

?event.clientX? / ?event.clientY ?

指針事件(pointer event)的指針的窗口相對坐標。

還有很多屬性。其中很多都取決于事件類型:鍵盤事件具有一組屬性,指針事件具有另一組屬性,稍后我們將詳細討論不同事件,那時我們再對其進行詳細研究。

?event? 對象在 HTML 處理程序中也可用

如果我們在 HTML 中分配了一個處理程序,那么我們也可以使用 event 對象,像這樣:

<input type="button" onclick="alert(event.type)" value="Event type">

這是可能的,因為當瀏覽器讀取特性(attribute)時,它會創(chuàng)建像這樣的處理程序:function(event) { alert(event.type) }。也就是說:它的第一個參數(shù)是 "event",而主體取自于該特性(attribute)。

對象處理程序:handleEvent

我們不僅可以分配函數(shù),還可以使用 addEventListener 將一個對象分配為事件處理程序。當事件發(fā)生時,就會調(diào)用該對象的 handleEvent 方法。

例如:

<button id="elem">Click me</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  };

  elem.addEventListener('click', obj);
</script>

正如我們所看到的,當 addEventListener 接收一個對象作為處理程序時,在事件發(fā)生時,它就會調(diào)用 obj.handleEvent(event) 來處理事件。

我們也可以對此使用一個類:

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      switch(event.type) {
        case 'mousedown':
          elem.innerHTML = "Mouse button pressed";
          break;
        case 'mouseup':
          elem.innerHTML += "...and released.";
          break;
      }
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

這里,同一個對象處理兩個事件。請注意,我們需要使用 addEventListener 來顯式設(shè)置事件,以指明要監(jiān)聽的事件。這里的 menu 對象只監(jiān)聽 mousedown 和 mouseup,而沒有任何其他類型的事件。

handleEvent 方法不必通過自身完成所有的工作。它可以調(diào)用其他特定于事件的方法,例如:

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

現(xiàn)在事件處理程序已經(jīng)明確地分離了出來,這樣更容易進行代碼編寫和后續(xù)維護。

總結(jié)

這里有 3 種分配事件處理程序的方式:

  1. HTML 特性(attribute):?onclick="..."?。
  2. DOM 屬性(property):?elem.onclick = function?。
  3. 方法(method):?elem.addEventListener(event, handler[, phase])? 用于添加,?removeEventListener? 用于移除。

HTML 特性很少使用,因為 HTML 標簽中的 JavaScript 看起來有些奇怪且陌生。而且也不能在里面寫太多代碼。

DOM 屬性用起來還可以,但我們無法為特定事件分配多個處理程序。在許多場景中,這種限制并不嚴重。

最后一種方式是最靈活的,但也是寫起來最長的。有少數(shù)事件只能使用這種方式。例如 transtionend 和 DOMContentLoaded(上文中講到了)。addEventListener 也支持對象作為事件處理程序。在這種情況下,如果發(fā)生事件,則會調(diào)用 handleEvent 方法。

無論你如何分類處理程序 —— 它都會將獲得一個事件對象作為第一個參數(shù)。該對象包含有關(guān)所發(fā)生事件的詳細信息。

在下一章中,我們將學習更多關(guān)于一般事件和不同類型事件的內(nèi)容。

任務(wù)


點擊隱藏

重要程度: 5

為 button 添加 JavaScript 代碼,使得 <div id="text"> 在我們點擊該按鈕時消失。

打開一個任務(wù)沙箱。


解決方案

使用沙箱打開解決方案。


隱藏自己

重要程度: 5

創(chuàng)建一個按鈕,在被點擊時,隱藏自己。


解決方案

可以在處理程序中使用 this 來引用“元素自身”:

<input type="button" onclick="this.hidden=true" value="Click to hide">

哪個處理程序會運行?

重要程度: 5

在變量中有一個按鈕。它上面沒有處理程序。

執(zhí)行以下代碼之后,哪些處理程序會在按鈕被點擊時運行?會顯示哪些 alert?

button.addEventListener("click", () => alert("1"));

button.removeEventListener("click", () => alert("1"));

button.onclick = () => alert(2);

解決方案

答案:1 和 2

第一個處理程序會觸發(fā),因為它沒有被 removeEventListener 移除。要移除處理程序,我們需要傳遞正確的所分配的函數(shù)。在代碼中,傳遞了一個新的函數(shù),該函數(shù)看起來相同,但仍然是另一個函數(shù)。

要移除一個函數(shù)對象,我們需要存儲對它的引用,像這樣:

function handler() {
  alert(1);
}

button.addEventListener("click", handler);
button.removeEventListener("click", handler);

無論 addEventListener 怎樣,button.onclick 處理程序都會觸發(fā)。


讓球在球場中移動

重要程度: 5

點擊球場中任意一點,讓球在球場中移動。

要求:

  • 球的中心應(yīng)該恰好在點擊時鼠標指針位置的下方(如果在球不越過球場邊緣的情況下,能實現(xiàn)的話)。
  • 最好添加一些 CSS 動畫。
  • 球不能越過場地邊界。
  • 頁面滾動時,布局不能被破壞。

注意:

  • 代碼還應(yīng)該適用于不同大小的球和球場,而不應(yīng)該綁定到任何固定值。
  • 使用 ?event.clientX/event.clientY? 屬性來獲取點擊坐標。

打開一個任務(wù)沙箱。


解決方案

首先,我們需要選擇一種定位球的方法。

我們不能使用 position:fixed,因為滾動頁面會造成球被移出球場。

所以我們應(yīng)該使用 position:absolute,并且要使定位真正可靠,應(yīng)該使 field 自身具有 position:absolute。

然后,球?qū)⑾鄬τ谇驁龆ㄎ唬?

#field {
  width: 200px;
  height: 150px;
  position: relative;
}

#ball {
  position: absolute;
  left: 0; /* 相對于最接近的祖先(field) */
  top: 0;
  transition: 1s all; /* left/top 的 CSS 動畫,使球飛起來 */
}

接下來我們需要指定正確的 ball.style.left/top。它們現(xiàn)在包含相對于球場的坐標。

這是示意圖:


我們有 event.clientX/clientY —— 點擊位置的窗口相對坐標。

要獲取點擊位置的球場相對坐標 left,我們可以減去球場左邊緣和邊框的寬度:

let left = event.clientX - fieldCoords.left - field.clientLeft;

通常情況下,ball.style.left 表示“元素的左邊緣”(球)。因此,如果我們將其指定為 left,那么球的邊緣而非球的中心將位于鼠標光標下方。

我們需要將球向左移動球?qū)挾鹊囊话?,向上移動球高度的一半,以使其居中?nbsp;   

所以,最后的 left 將是:

let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2;

使用相同的邏輯來計算垂直坐標。

請注意,球的寬度/高度必須在我們訪問 ball.offsetWidth 時就已知。應(yīng)該在 HTML 或 CSS 中指定。

使用沙箱打開解決方案。


創(chuàng)建滑動菜單

重要程度: 5

創(chuàng)建一個在點擊時打開/折疊的菜單:

P.S. 源文檔的 HTML/CSS 將被修改。

打開一個任務(wù)沙箱。


解決方案

  • HTML/CSS
  • 首先,讓我們創(chuàng)建 HTML/CSS。

    菜單是頁面上的一個獨立圖形組件,所以最好把它放入一個單獨的 DOM 元素中。

    菜單項的列表可以被作為列表 ul/li 列出。

    下面是示例結(jié)構(gòu):

    <div class="menu">
      <span class="title">Sweeties (click me)!</span>
      <ul>
        <li>Cake</li>
        <li>Donut</li>
        <li>Honey</li>
      </ul>
    </div>

    我們對標題使用 <span>,因為 <div> 有一個隱式的 display:block,它會占據(jù) 100% 的水平寬度。

    就像這樣:

    <div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div>
    

    因此,如果我們在它上面設(shè)置 onclick,那么它也會捕獲文本右側(cè)的點擊。

    ……由于 <span> 有一個隱式的 display: inline,它恰好占據(jù)了足以容納所有文本的位置:

    <span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span>
    
  • 切換菜單
  • 切換菜單應(yīng)更改箭頭并顯示/隱藏菜單列表。

    所有這些更改都可以通過 CSS 完美處理。在 JavaScript 中,我們應(yīng)該通過添加/移除 .open 類來標記菜單的當前狀態(tài)。

    沒有它,菜單就會被關(guān)閉:

    .menu ul {
      margin: 0;
      list-style: none;
      padding-left: 20px;
      display: none;
    }
    
    .menu .title::before {
      content: '? ';
      font-size: 80%;
      color: green;
    }

    ……有 .open 后,箭頭會改變,列表會出現(xiàn):

    .menu.open .title::before {
      content: '▼ ';
    }
    
    .menu.open ul {
      display: block;
    }

    使用沙箱打開解決方案。


添加關(guān)閉按鈕

重要程度: 5

有一個消息列表。

使用 JavaScript 在每條消息的右上角添加一個關(guān)閉按鈕。

結(jié)果應(yīng)該如下所示:


打開一個任務(wù)沙箱。


解決方案

我們可以使用 position:absolute(并使窗格 position:relative)或者 float:right 來添加按鈕。float:right 的好處是按鈕永遠都不會與文本重疊,但是 position:absolute 則提供了更大的自由度。選擇權(quán)在你自己手上。

然后,對于每個窗格(pane),代碼可以像這樣:

pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>');

然后 <button> 變成了 pane.firstChild,因此我們可以像這樣為它添加處理程序:

pane.firstChild.onclick = () => pane.remove();

使用沙箱打開解決方案。


輪播圖

重要程度: 4

創(chuàng)建一個“輪播圖(carousel)” —— 一條可以通過點擊箭頭來滾動圖像的圖像帶。


之后,我們可以為其添加更多功能:無限滾動,動態(tài)加載等。

P.S. 對于這個任務(wù),HTML/CSS 結(jié)構(gòu)實際上占解決方案的 90%。

打開一個任務(wù)沙箱。


解決方案

圖像帶可以表示為圖像 <img> 的 ul/li 列表。

通常,這樣的圖像帶是很寬的,但我們在其周圍放置了一個固定大小的 <div> 來“剪切”它,因此,只有圖像帶的一部分是可見的:


為了使列表水平顯示,我們需要為 <li> 應(yīng)用正確的 CSS 屬性,例如 display: inline-block

對于 <img> 來說,我們應(yīng)該調(diào)整 display,因為默認情況下它是 inline。在 inline 元素下方為 “l(fā)etter tails” 保留了額外的空間,因此,我們可以使用 display:block 來將其刪除。

我們可以移動 <ul> 來進行滾動。有很多方法可以實現(xiàn)這一點,例如,通過修改 margin-left 或者使用 transform: translateX()(性能更好):


外部的 <div> 具有固定的寬度,因此,會裁剪掉“多余”的圖像。

整個輪播圖是頁面上的一個獨立的“圖形組件”,因此我們最好將其包裝到一個單獨的 <div class="carousel"> 中,并在其中對其進行樣式設(shè)置。

使用沙箱打開解決方案。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號