JavaScript 編程精解 中文第三版 十五、處理事件

十五、處理事件

原文:Handling Events

譯者:飛龍

協議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

部分參考了《JavaScript 編程精解(第 2 版)》

你對你的大腦擁有控制權,而不是外部事件。認識到這一點,你就找到了力量。

馬可·奧勒留,《沉思錄》

https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/15-0.jpg

有些程序處理用戶的直接輸入,比如鼠標和鍵盤動作。這種輸入方式不是組織整齊的數據結構 - 它是一次一個地,實時地出現的,并且期望程序在發生時作出響應。

事件處理器

想象一下,有一個接口,若想知道鍵盤上是否有一個鍵是否被按下,唯一的方法是讀取那個按鍵的當前狀態。為了能夠響應按鍵動作,你需要不斷讀取鍵盤狀態,以在按鍵被釋放之前捕捉到按下狀態。這種方法在執行時間密集計算時非常危險,因為你可能錯過按鍵事件。

一些原始機器可以像那樣處理輸入。有一種更進一步的方法,硬件或操作系統發現按鍵時間并將其放入隊列中。程序可以周期性地檢查隊列,等待新事件并在發現事件時進行響應。

當然,程序必須記得監視隊列,并經常做這種事,因為任何時候,按鍵被按下和程序發現事件之間都會使得軟件反應遲鈍。該方法被稱為輪詢。大多數程序員更希望避免這種方法。

一個更好的機制是,系統在發生事件時主動通知我們的代碼。瀏覽器實現了這種特性,支持我們將函數注冊為特定事件的處理器。

<p>Click this document to activate the handler.</p>
<script>
  window.addEventListener("click", () => {
    console.log("You knocked?");
  });
</script>

window綁定指向瀏覽器提供的內置對象。 它代表包含文檔的瀏覽器窗口。 調用它的addEventListener方法注冊第二個參數,以便在第一個參數描述的事件發生時調用它。

事件與 DOM 節點

每個瀏覽器事件處理器被注冊在上下文中。在為整個窗口注冊處理器之前,我們在window對象上調用了addEventListener。 這種方法也可以在 DOM 元素和一些其他類型的對象上找到。 僅當事件發生在其注冊對象的上下文中時,才調用事件監聽器。

<button>Click me</button>
<p>No handler here.</p>
<script>
  let button = document.querySelector("button");
  button.addEventListener("click", () => {
    console.log("Button clicked.");
  });
</script>

示例代碼中將處理器附加到按鈕節點上。因此,點擊按鈕時會觸發并執行處理器,而點擊文檔的其他部分則沒有反應。

向節點提供onclick屬性也有類似效果。這適用于大多數類型的事件 - 您可以為屬性附加處理器,屬性名稱為前面帶有on的事件名稱。

但是一個節點只能有一個onclick屬性,所以你只能用這種方式為每個節點注冊一個處理器。 addEventListener方法允許您添加任意數量的處理器,因此即使元素上已經存在另一個處理器,添加處理器也是安全的。

removeEventListener方法將刪除一個處理器,使用類似于addEventListener的參數調用。

<button>Act-once button</button>
<script>
  let button = document.querySelector("button");
  function once() {
    console.log("Done.");
    button.removeEventListener("click", once);
  }
  button.addEventListener("click", once);
</script>

賦予removeEventListener的函數必須是賦予addEventListener的完全相同的函數值。 因此,要注銷一個處理其,您需要為該函數提供一個名稱(在本例中為once),以便能夠將相同的函數值傳遞給這兩個方法。

事件對象

雖然目前為止我們忽略了它,事件處理器函數作為對象傳遞:事件(Event)對象。這個對象持有事件的額外信息。例如,如果我們想知道哪個鼠標按鍵被按下,我們可以查看事件對象的which屬性。

<button>Click me any way you want</button>
<script>
  let button = document.querySelector("button");
  button.addEventListener("mousedown", event => {
    if (event.button == 0) {
      console.log("Left button");
    } else if (event.button == 1) {
      console.log("Middle button");
    } else if (event.button == 2) {
      console.log("Right button");
    }
  });
</script>

存儲在各種類型事件對象中的信息是有差別的。隨后本章將會討論許多類型的事件。對象的type屬性一般持有一個字符串,表示事件(例如"click""mousedown")。

傳播

對于大多數事件類型,在具有子節點的節點上注冊的處理器,也將接收發生在子節點中的事件。若點擊一個段落中的按鈕,段落的事件處理器也會收到點擊事件。

但若段落和按鈕都有事件處理器,則先執行最特殊的事件處理器(按鈕的事件處理器)。也就是說事件向外傳播,從觸發事件的節點到其父節點,最后直到文檔根節點。最后,當某個特定節點上注冊的所有事件處理器按其順序全部執行完畢后,窗口對象的事件處理器才有機會響應事件。

事件處理器任何時候都可以調用事件對象的stopPropagation方法,阻止事件進一步傳播。該方法有時很實用,例如,你將一個按鈕放在另一個可點擊元素中,但你不希望點擊該按鈕會激活外部元素的點擊行為。

下面的示例代碼將mousedown處理器注冊到按鈕和其外部的段落節點上。在按鈕上點擊鼠標右鍵,按鈕的處理器會調用stopPropagation,調度段落上的事件處理器執行。當點擊鼠標其他鍵時,兩個處理器都會執行。

<p>A paragraph with a <button>button</button>.</p>
<script>
  let para = document.querySelector("p");
  let button = document.querySelector("button");
  para.addEventListener("mousedown", () => {
    console.log("Handler for paragraph.");
  });
  button.addEventListener("mousedown", event => {
    console.log("Handler for button.");
    if (event.button == 2) event.stopPropagation();
  });
</script>

大多數事件對象都有target屬性,指的是事件來源節點。你可以根據該屬性防止無意中處理了傳播自其他節點的事件。

我們也可以使用target屬性來創建出特定類型事件的處理網絡。例如,如果一個節點中包含了很長的按鈕列表,比較方便的處理方式是在外部節點上注冊一個點擊事件處理器,并根據事件的target屬性來區分用戶按下了哪個按鈕,而不是為每個按鈕都注冊獨立的事件處理器。

<button>A</button>
<button>B</button>
<button>C</button>
<script>
  document.body.addEventListener("click", event => {
    if (event.target.nodeName == "BUTTON") {
      console.log("Clicked", event.target.textContent);
    }
  });
</script>

默認動作

大多數事件都有與其關聯的默認動作。若點擊鏈接,就會跳轉到鏈接目標。若點擊向下的箭頭,瀏覽器會向下翻頁。若右擊鼠標,可以得到一個上下文菜單等。

對于大多數類型的事件,JavaScript 事件處理器會在默認行為發生之前調用。若事件處理器不希望執行默認行為(通常是因為已經處理了該事件),會調用preventDefault事件對象的方法。

你可以實現你自己的鍵盤快捷鍵或交互式菜單。你也可以干擾用戶期望的行為。例如,這里實現一個無法跳轉的鏈接。

<a >MDN</a>
<script>
  let link = document.querySelector("a");
  link.addEventListener("click", event => {
    console.log("Nope.");
    event.preventDefault();
  });
</script>

除非你有非常充足的理由,否則不要這樣做。當預期的行為被打破時,使用你的頁面的人會感到不快。

在有些瀏覽器中,你完全無法攔截某些事件。比如在 Chrome 中,關閉鍵盤快捷鍵(CTRL-WCOMMAND-W)無法由 JavaScript 處理。

按鍵事件

當按下鍵盤上的按鍵時,瀏覽器會觸發"keydown"事件。當松開按鍵時,會觸發"keyup"事件。

<p>This page turns violet when you hold the V key.</p>
<script>
  window.addEventListener("keydown", event => {
    if (event.key == "v") {
      document.body.style.background = "violet";
    }
  });
  window.addEventListener("keyup", event => {
    if (event.key == "v") {
      document.body.style.background = "";
    }
  });
</script>

盡管從keydown這個事件名上看應該是物理按鍵按下時觸發,但當持續按下某個按鍵時,會循環觸發該事件。有時,你想謹慎對待它。例如,如果您在按下某個按鍵時向 DOM 添加按鈕,并且在釋放按鍵時再次將其刪除,則可能會在按住某個按鍵的時間過長時,意外添加數百個按鈕。

該示例查看了事件對象的key屬性,來查看事件關于哪個鍵。 該屬性包含一個字符串,對于大多數鍵,它對應于按下該鍵時將鍵入的內容。 對于像Enter這樣的特殊鍵,它包含一個用于命名鍵的字符串(在本例中為"Enter")。 如果你按住一個鍵的同時按住Shift鍵,這也可能影響鍵的名稱 - "v"變為"V""1"可能變成"!",這是按下Shift-1鍵 在鍵盤上產生的東西。

諸如shiftctrlaltmeta(Mac 上的command)之類的修飾按鍵會像普通按鍵一樣產生事件。但在查找組合鍵時,你也可以查看鍵盤和鼠標事件的shiftKeyctrlKeyaltKeymetaKey屬性來判斷這些鍵是否被按下。

<p>Press Ctrl-Space to continue.</p>
<script>
  window.addEventListener("keydown", event => {
    if (event.key == " " && event.ctrlKey) {
      console.log("Continuing!");
    }
  });
</script>

按鍵事件發生的 DOM 節點取決于按下按鍵時具有焦點的元素。 大多數節點不能擁有焦點,除非你給他們一個tabindex屬性,但像鏈接,按鈕和表單字段可以。 我們將在第 18 章中回顧表單字段。 當沒有特別的焦點時,document.body充當按鍵事件的目標節點。

當用戶鍵入文本時,使用按鍵事件來確定正在鍵入的內容是有問題的。 某些平臺,尤其是 Android 手機上的虛擬鍵盤,不會觸發按鍵事件。 但即使你有一個老式鍵盤,某些類型的文本輸入也不能直接匹配按鍵,例如其腳本不適合鍵盤的人所使用的 IME(“輸入法編輯器”)軟件 ,其中組合多個熱鍵來創建字符。

要注意什么時候輸入了內容,每當用戶更改其內容時,可以鍵入的元素(例如<input><textarea>標簽)觸發"input"事件。為了獲得輸入的實際內容,最好直接從焦點字段中讀取它。 第 18 章將展示如何實現。

指針事件

目前有兩種廣泛使用的方式,用于指向屏幕上的東西:鼠標(包括類似鼠標的設備,如觸摸板和軌跡球)和觸摸屏。 它們產生不同類型的事件。

鼠標點擊

點擊鼠標按鍵會觸發一系列事件。"mousedown"事件和"mouseup"事件類似于"keydown""keyup"事件,當鼠標按鈕按下或釋放時觸發。當事件發生時,由鼠標指針下方的 DOM 節點觸發事件。

mouseup事件后,包含鼠標按下與釋放的特定節點會觸發"click"事件。例如,如果我在一個段落上按下鼠標,移動到另一個段落上釋放鼠標,"click"事件會發生在包含這兩個段落的元素上。

若兩次點擊事件觸發時機接近,則在第二次點擊事件之后,也會觸發"dbclick"(雙擊,double-click)事件。

為了獲得鼠標事件觸發的精確信息,你可以查看事件中的clientXclientY屬性,包含了事件相對于窗口左上角的坐標(以像素為單位)。或pageXpageY,它們相對于整個文檔的左上角(當窗口被滾動時可能不同)。

下面的代碼實現了簡單的繪圖程序。每次點擊文檔時,會在鼠標指針下添加一個點。還有一個稍微優化的繪圖程序,請參見第 19 章。

<style>
  body {
    height: 200px;
    background: beige;
  }
  .dot {
    height: 8px; width: 8px;
    border-radius: 4px; /* rounds corners */
    background: blue;
    position: absolute;
  }
</style>
<script>
  window.addEventListener("click", event => {
    let dot = document.createElement("div");
    dot.className = "dot";
    dot.style.left = (event.pageX - 4) + "px";
    dot.style.top = (event.pageY - 4) + "px";
    document.body.appendChild(dot);
  });
</script>

鼠標移動

每次鼠標移動時都會觸發"mousemove"事件。該事件可用于跟蹤鼠標位置。當實現某些形式的鼠標拖拽功能時,該事件非常有用。

舉一個例子,下面的程序展示一條欄,并設置一個事件處理器,當向左拖動這個欄時,會使其變窄,若向右拖動則變寬。

<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
  let lastX; // Tracks the last observed mouse X position
  let bar = document.querySelector("div");
  bar.addEventListener("mousedown", event => {
    if (event.button == 0) {
      lastX = event.clientX;
      window.addEventListener("mousemove", moved);
      event.preventDefault(); // Prevent selection
    }
  });


  function moved(event) {
    if (event.buttons == 0) {
      window.removeEventListener("mousemove", moved);
    } else {
      let dist = event.clientX - lastX;
      let newWidth = Math.max(10, bar.offsetWidth + dist);
      bar.style.width = newWidth + "px";
      lastX = event.clientX;
    }
  }
</script>

請注意,mousemove處理器注冊在窗口對象上。即使鼠標在改變窗口尺寸時在欄外側移動,只要按住按鈕,我們仍然想要更新其大小。

釋放鼠標按鍵時,我們必須停止調整欄的大小。 為此,我們可以使用buttons屬性(注意復數形式),它告訴我們當前按下的按鍵。 當它為零時,沒有按下按鍵。 當按鍵被按住時,其值是這些按鍵的代碼總和 - 左鍵代碼為 1,右鍵為 2,中鍵為 4。 這樣,您可以通過獲取buttons的剩余值及其代碼,來檢查是否按下了給定按鍵。

請注意,這些代碼的順序與button使用的順序不同,中鍵位于右鍵之前。 如前所述,一致性并不是瀏覽器編程接口的強項。

觸摸事件

我們使用的圖形瀏覽器的風格,是考慮到鼠標界面的情況下而設計的,那個時候觸摸屏非常罕見。 為了使網絡在早期的觸摸屏手機上“工作”,在某種程度上,這些設備的瀏覽器假裝觸摸事件是鼠標事件。 如果你點擊你的屏幕,你會得到'mousedown''mouseup''click'事件。

但是這種錯覺不是很健壯。 觸摸屏與鼠標的工作方式不同:它沒有多個按鈕,當手指不在屏幕上時不能跟蹤手指(來模擬"mousemove"),并且允許多個手指同時在屏幕上。

鼠標事件只涵蓋了簡單情況下的觸摸交互 - 如果您為按鈕添加"click"處理器,觸摸用戶仍然可以使用它。 但是像上一個示例中的可調整大小的欄在觸摸屏上不起作用。

觸摸交互觸發了特定的事件類型。 當手指開始觸摸屏幕時,您會看到'touchstart'事件。 當它在觸摸中移動時,觸發"touchmove"事件。 最后,當它停止觸摸屏幕時,您會看到"touchend"事件。

由于許多觸摸屏可以同時檢測多個手指,這些事件沒有與其關聯的一組坐標。 相反,它們的事件對象擁有touches屬性,它擁有一個類數組對象,每個對象都有自己的clientXclientYpageXpageY屬性。

你可以這樣,在每個觸摸手指周圍顯示紅色圓圈。

<style>
  dot { position: absolute; display: block;
        border: 2px solid red; border-radius: 50px;
        height: 100px; width: 100px; }
</style>
<p>Touch this page</p>
<script>
  function update(event) {
    for (let dot; dot = document.querySelector("dot");) {
      dot.remove();
    }
    for (let i = 0; i < event.touches.length; i++) {
      let {pageX, pageY} = event.touches[i];
      let dot = document.createElement("dot");
      dot.style.left = (pageX - 50) + "px";
      dot.style.top = (pageY - 50) + "px";
      document.body.appendChild(dot);
    }
  }
  window.addEventListener("touchstart", update);
  window.addEventListener("touchmove", update);
  window.addEventListener("touchend", update);
</script>

您經常希望在觸摸事件處理器中調用preventDefault,來覆蓋瀏覽器的默認行為(可能包括在滑動時滾動頁面),并防止觸發鼠標事件,您也可能擁有它的處理器。

滾動事件

每當元素滾動時,會觸發scroll事件。該事件用處極多,比如知道用戶當前查看的元素(禁用用戶視線以外的動畫,或向邪惡的指揮部發送監視報告),或展示一些滾動的跡象(通過高亮表格的部分內容,或顯示頁碼)。

以下示例在文檔上方繪制一個進度條,并在您向下滾動時更新它來填充:

<style>
  #progress {
    border-bottom: 2px solid blue;
    width: 0;
    position: fixed;
    top: 0; left: 0;
  }
</style>
<div id="progress"></div>
<script>
  // Create some content
  document.body.appendChild(document.createTextNode(
    "supercalifragilisticexpialidocious ".repeat(1000)));

  let bar = document.querySelector("#progress");
  window.addEventListener("scroll", () => {
    let max = document.body.scrollHeight - innerHeight;
    bar.style.width = `${(pageYOffset / max) * 100}%`;
  });
</script>

將元素的position屬性指定為fixed時,其行為和absolute很像,但可以防止在文檔滾動時期跟著文檔一起滾動。其效果是讓我們的進度條呆在最頂上。 改變其寬度來指示當前進度。 在設置寬度時,我們使用%而不是px作為單位,使元素的大小相對于頁面寬度。

innerHeight全局綁定是窗口高度,我們必須要減去滾動條的高度。你點擊文檔底部的時候是無法繼續滾動的。對于窗口高度來說,也存在innerWidth。使用pageYOffset(當前滾動位置)除以最大滾動位置,并乘以 100,就可以得到進度條長度。

調用滾動事件的preventDefault無法阻止滾動。實際上,事件處理器是在進行滾動之后才觸發的。

焦點事件

當元素獲得焦點時,瀏覽器會觸發其上的focus事件。當失去焦點時,元素會獲得blur事件。

與前文討論的事件不同,這兩個事件不會傳播。子元素獲得或失去焦點時,不會激活父元素的處理器。

下面的示例中,文本域在擁有焦點時會顯示幫助文本。

<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Your age in years"></p>
<p id="help"></p>

<script>
  let help = document.querySelector("#help");
  let fields = document.querySelectorAll("input");
  for (let field of Array.from(fields)) {
    field.addEventListener("focus", event => {
      let text = event.target.getAttribute("data-help");
      help.textContent = text;
    });
    field.addEventListener("blur", event => {
      help.textContent = "";
    });
  }
</script>

當用戶從瀏覽器標簽或窗口移開時,窗口對象會收到focus事件,當移動到標簽或窗口上時,則收到blur事件。

加載事件

當界面結束裝載時,會觸發窗口對象和文檔body對象的"load"事件。該事件通常用于在當整個文檔構建完成時,進行初始化。請記住<script>標簽的內容是一遇到就執行的。這可能太早了,比如有時腳本需要處理在<script>標簽后出現的內容。

諸如imagescript這類會裝載外部文件的標簽都有load事件,指示其引用文件裝載完畢。類似于焦點事件,裝載事件是不會傳播的。

當頁面關閉或跳轉(比如跳轉到一個鏈接)時,會觸發beforeunload事件。該事件用于防止用戶突然關閉文檔而丟失工作結果。你無法使用preventDefault方法阻止頁面卸載。它通過從處理器返回非空值來完成。當你這樣做時,瀏覽器會通過顯示一個對話框,詢問用戶是否關閉頁面的對話框中。該機制確保用戶可以離開,即使在那些想要留住用戶,強制用戶看廣告的惡意頁面上,也是這樣。

事件和事件循環

在事件循環的上下文中,如第 11 章中所述,瀏覽器事件處理器的行為,類似于其他異步通知。 它們是在事件發生時調度的,但在它們有機會運行之前,必須等待其他正在運行的腳本完成。

僅當沒有別的事情正在運行時,才能處理事件,這個事實意味著,如果事件循環與其他工作捆綁在一起,任何頁面交互(通過事件發生)都將延遲,直到有時間處理它為止。 因此,如果您安排了太多工作,無論是長時間運行的事件處理器還是大量短時間運行的工作,該頁面都會變得緩慢且麻煩。

如果您想在背后做一些耗時的事情而不會凍結頁面,瀏覽器會提供一些名為 Web Worker 的東西。 Web Worker 是一個 JavaScript 過程,與主腳本一起在自己的時間線上運行。

想象一下,計算一個數字的平方運算是一個重量級的,長期運行的計算,我們希望在一個單獨的線程中執行。 我們可以編寫一個名為code/squareworker.js的文件,通過計算平方并發回消息來響應消息:

addEventListener("message", event => {
  postMessage(event.data * event.data);
});

為了避免多線程觸及相同數據的問題,Web Worker 不會將其全局作用域或任何其他數據與主腳本的環境共享。 相反,你必須通過來回發送消息與他們溝通。

此代碼會生成一個運行該腳本的 Web Worker,向其發送幾條消息并輸出響應。

let squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", event => {
  console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);

函數postMessage會發送一條消息,觸發接收方的message事件。創建工作單元的腳本通過Worker對象收發消息,而worker則直接向其全局作用域發送消息,或監聽其消息。只有可以表示為 JSON 的值可以作為消息發送 - 另一方將接收它們的副本,而不是值本身。

定時器

我們在第 11 章中看到了setTimeout函數。 它會在給定的毫秒數之后,調度另一個函數在稍后調用。

有時讀者需要取消調度的函數。可以存儲setTimeout的返回值,并將作為參數調用clearTimeout

let bombTimer = setTimeout(() => {
  console.log("BOOM!");
}, 500);

if (Math.random() < 0.5) { // 50% chance
  console.log("Defused.");
  clearTimeout(bombTimer);
}

函數cancelAnimationFrame作用與clearTimeout相同,使用requestAnimationFrame的返回值調用該函數,可以取消幀(假定函數還沒有被調用)。

還有setIntervalclearInterval這種相似的函數,用于設置計時器,每隔一定毫秒數重復執行一次。

let ticks = 0;
let clock = setInterval(() => {
  console.log("tick", ticks++);
  if (ticks == 10) {
    clearInterval(clock);
    console.log("stop.");
  }
}, 200);

降頻

某些類型的事件可能會連續、迅速觸發多次(例如mousemovescroll事件)。處理這類事件時,你必須小心謹慎,防止處理任務耗時過長,否則處理器會占據過多事件,導致用戶與文檔交互變得非常慢。

若你需要在這類處理器中編寫一些重要任務,可以使用setTimeout來確保不會頻繁進行這些任務。我們通常稱之為“事件降頻(Debounce)”。有許多方法可以完成該任務。

在第一個示例中,當用戶輸入某些字符時,我們想要有所反應,但我們不想在每個按鍵事件中立即處理該任務。當用戶輸入過快時,我們希望暫停一下然后進行處理。我們不是立即在事件處理器中執行動作,而是設置一個定時器。我們也會清除上一次的定時器(如果有),因此當兩個事件觸發間隔過短(比定時器延時短),就會取消上一次事件設置的定時器。

<textarea>Type something here...</textarea>
<script>
  let textarea = document.querySelector("textarea");
  let timeout;
  textarea.addEventListener("input", () => {
    clearTimeout(timeout);
    timeout = setTimeout(() => console.log("Typed!"), 500);
  });
</script>

undefined傳遞給clearTimeout或在一個已結束的定時器上調用clearTimeout是沒有效果的。因此,我們不需要關心何時調用該方法,只需要每個事件中都這樣做即可。

如果我們想要保證每次響應之間至少間隔一段時間,但不希望每次事件發生時都重置定時器,而是在一連串事件連續發生時能夠定時觸發響應,那么我們可以使用一個略有區別的方法來解決問題。例如,我們想要響應"mousemove"事件來顯示當前鼠標坐標,但頻率只有 250ms。

<script>
  let scheduled = null;
  window.addEventListener("mousemove", event => {
    if (!scheduled) {
      setTimeout(() => {
        document.body.textContent =
          `Mouse at ${scheduled.pageX}, ${scheduled.pageY}`;
        scheduled = null;
      }, 250);
    }
    scheduled = event;
  });
</script>

本章小結

事件處理器可以檢測并響應發生在我們的 Web 頁面上的事件。addEventListener方法用于注冊處理器。

每個事件都有標識事件的類型(keydownfocus等)。大多數方法都會在特定 DOM 元素上調用,接著向其父節點傳播,允許每個父元素的處理器都能處理這些事件。

JavaScript 調用事件處理器時,會傳遞一個包含事件額外信息的事件對象。該對象也有方法支持停止進一步傳播(stopPropagation),也支持阻止瀏覽器執行事件的默認處理器(preventDefault)。

按下鍵盤按鍵時會觸發keydownkeyup事件。按下鼠標按鈕時,會觸發mousedownmouseupclick事件。移動鼠標會觸發mousemove事件。觸摸屏交互會導致"touchstart""touchmove""touchend"事件。

我們可以通過scroll事件監測滾動行為,可以通過focusblur事件監控焦點改變。當文檔完成加載后,會觸發窗口的load事件。

習題

氣球

編寫一個顯示氣球的頁面(使用氣球 emoji,\ud83c\udf88)。 當你按下上箭頭時,它應該變大(膨脹)10%,而當你按下下箭頭時,它應該縮小(放氣)10%。

您可以通過在其父元素上設置font-size CSS 屬性(style.fontSize)來控制文本大小(emoji 是文本)。 請記住在該值中包含一個單位,例如像素(10px)。

箭頭鍵的鍵名是"ArrowUp""ArrowDown"。確保按鍵只更改氣球,而不滾動頁面。

實現了之后,添加一個功能,如果你將氣球吹過一定的尺寸,它就會爆炸。 在這種情況下,爆炸意味著將其替換為“爆炸 emoji,\ud83d\udca5”,并且移除事件處理器(以便您不能使爆炸變大變小)。

<p>&#x1f4a5;</p>
<script>
  // Your code here
</script>

鼠標軌跡

在 JavaScript 早期,有許多主頁都會在頁面上使用大量的動畫,人們想出了許多該語言的創造性用法。

其中一種是“鼠標蹤跡”,也就是一系列的元素,隨著你在頁面上移動鼠標,它會跟著你的鼠標指針。

在本習題中實現鼠標軌跡的功能。使用絕對定位、固定尺寸的<div>元素,背景為黑色(請參考鼠標點擊一節中的示例)。創建一系列此類元素,當鼠標移動時,伴隨鼠標指針顯示它們。

有許多方案可以實現我們所需的功能。你可以根據你的需要實現簡單的或復雜的方法。簡單的解決方案是保存固定鼠標的軌跡元素并循環使用它們,每次mousemove事件觸發時將下一個元素移動到鼠標當前位置。

<style>
  .trail { /* className for the trail elements */
    position: absolute;
    height: 6px; width: 6px;
    border-radius: 3px;
    background: teal;
  }
  body {
    height: 300px;
  }
</style>

<script>
  // Your code here.
</script>

選項卡

選項卡面板廣泛用于用戶界面。它支持用戶通過選擇元素上方的很多突出的選項卡來選擇一個面板。

本習題中,你必須實現一個簡單的選項卡界面。編寫asTabs函數,接受一個 DOM 節點并創建選項卡界面來展現該節點的子元素。該函數應該在頂層節點中插入大量<button>元素,與每個子元素一一對應,按鈕文本從子節點的data-tabname中獲取。除了顯示一個初始子節點,其他子節點都應該隱藏(將display樣式設置成none),并通過點擊按鈕來選擇當前顯示的節點。

當它生效時將其擴展,為當前選中的選項卡,將按鈕的樣式設為不同的,以便明確選擇了哪個選項卡。

<tab-panel>
  <div data-tabname="one">Tab one</div>
  <div data-tabname="two">Tab two</div>
  <div data-tabname="three">Tab three</div>
</tab-panel>
<script>
  function asTabs(node) {
    // Your code here.
  }
  asTabs(document.querySelector("tab-panel"));
</script>
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容

  • ??JavaScript 與 HTML 之間的交互是通過事件實現的。 ??事件,就是文檔或瀏覽器窗口中發生的一些特...
    霜天曉閱讀 3,523評論 1 11
  • 本節介紹各種常見的瀏覽器事件。 鼠標事件 鼠標事件指與鼠標相關的事件,主要有以下一些。 click 事件,dblc...
    許先生__閱讀 2,474評論 0 4
  • 事件類型 Web 瀏覽器中可能發生的事件有很多類型UI事件:當用戶與界面上的元素交互時觸發。焦點事件:當元素獲得或...
    shanruopeng閱讀 927評論 0 0
  • 13.1 事件流 “DOM2級事件”規定事件流包括3個階段:事件捕獲階段,處于目標階段,事件冒泡階段。事件捕獲表示...
    Elevens_regret閱讀 440評論 0 0
  • JavaScript 與 HTML 間通過事件實現交互。事件——文檔或瀏覽器窗口中發生的一些特定的交互瞬間,即用戶...
    sylvia_yue閱讀 485評論 0 0