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

十五、處理事件

原文:Handling Events

譯者:飛龍

協(xié)議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

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

你對(duì)你的大腦擁有控制權(quán)崩侠,而不是外部事件傀缩。認(rèn)識(shí)到這一點(diǎn)那先,你就找到了力量。

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

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

有些程序處理用戶的直接輸入售淡,比如鼠標(biāo)和鍵盤動(dòng)作。這種輸入方式不是組織整齊的數(shù)據(jù)結(jié)構(gòu) - 它是一次一個(gè)地慷垮,實(shí)時(shí)地出現(xiàn)的揖闸,并且期望程序在發(fā)生時(shí)作出響應(yīng)。

事件處理器

想象一下料身,有一個(gè)接口汤纸,若想知道鍵盤上是否有一個(gè)鍵是否被按下,唯一的方法是讀取那個(gè)按鍵的當(dāng)前狀態(tài)芹血。為了能夠響應(yīng)按鍵動(dòng)作贮泞,你需要不斷讀取鍵盤狀態(tài),以在按鍵被釋放之前捕捉到按下狀態(tài)幔烛。這種方法在執(zhí)行時(shí)間密集計(jì)算時(shí)非常危險(xiǎn)啃擦,因?yàn)槟憧赡苠e(cuò)過按鍵事件。

一些原始機(jī)器可以像那樣處理輸入饿悬。有一種更進(jìn)一步的方法令蛉,硬件或操作系統(tǒng)發(fā)現(xiàn)按鍵時(shí)間并將其放入隊(duì)列中。程序可以周期性地檢查隊(duì)列狡恬,等待新事件并在發(fā)現(xiàn)事件時(shí)進(jìn)行響應(yīng)珠叔。

當(dāng)然,程序必須記得監(jiān)視隊(duì)列傲宜,并經(jīng)常做這種事运杭,因?yàn)槿魏螘r(shí)候,按鍵被按下和程序發(fā)現(xiàn)事件之間都會(huì)使得軟件反應(yīng)遲鈍函卒。該方法被稱為輪詢辆憔。大多數(shù)程序員更希望避免這種方法。

一個(gè)更好的機(jī)制是报嵌,系統(tǒng)在發(fā)生事件時(shí)主動(dòng)通知我們的代碼虱咧。瀏覽器實(shí)現(xiàn)了這種特性,支持我們將函數(shù)注冊為特定事件的處理器锚国。

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

window綁定指向?yàn)g覽器提供的內(nèi)置對(duì)象腕巡。 它代表包含文檔的瀏覽器窗口。 調(diào)用它的addEventListener方法注冊第二個(gè)參數(shù)血筑,以便在第一個(gè)參數(shù)描述的事件發(fā)生時(shí)調(diào)用它绘沉。

事件與 DOM 節(jié)點(diǎn)

每個(gè)瀏覽器事件處理器被注冊在上下文中煎楣。在為整個(gè)窗口注冊處理器之前,我們在window對(duì)象上調(diào)用了addEventListener车伞。 這種方法也可以在 DOM 元素和一些其他類型的對(duì)象上找到择懂。 僅當(dāng)事件發(fā)生在其注冊對(duì)象的上下文中時(shí),才調(diào)用事件監(jiān)聽器另玖。

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

示例代碼中將處理器附加到按鈕節(jié)點(diǎn)上困曙。因此,點(diǎn)擊按鈕時(shí)會(huì)觸發(fā)并執(zhí)行處理器谦去,而點(diǎn)擊文檔的其他部分則沒有反應(yīng)慷丽。

向節(jié)點(diǎn)提供onclick屬性也有類似效果。這適用于大多數(shù)類型的事件 - 您可以為屬性附加處理器鳄哭,屬性名稱為前面帶有on的事件名稱要糊。

但是一個(gè)節(jié)點(diǎn)只能有一個(gè)onclick屬性,所以你只能用這種方式為每個(gè)節(jié)點(diǎn)注冊一個(gè)處理器窃诉。 addEventListener方法允許您添加任意數(shù)量的處理器杨耙,因此即使元素上已經(jīng)存在另一個(gè)處理器,添加處理器也是安全的飘痛。

removeEventListener方法將刪除一個(gè)處理器珊膜,使用類似于addEventListener的參數(shù)調(diào)用。

<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的函數(shù)必須是賦予addEventListener的完全相同的函數(shù)值宣脉。 因此车柠,要注銷一個(gè)處理其,您需要為該函數(shù)提供一個(gè)名稱(在本例中為once)塑猖,以便能夠?qū)⑾嗤暮瘮?shù)值傳遞給這兩個(gè)方法竹祷。

事件對(duì)象

雖然目前為止我們忽略了它,事件處理器函數(shù)作為對(duì)象傳遞:事件(Event)對(duì)象羊苟。這個(gè)對(duì)象持有事件的額外信息塑陵。例如,如果我們想知道哪個(gè)鼠標(biāo)按鍵被按下蜡励,我們可以查看事件對(duì)象的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>

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

傳播

對(duì)于大多數(shù)事件類型稽寒,在具有子節(jié)點(diǎn)的節(jié)點(diǎn)上注冊的處理器扮碧,也將接收發(fā)生在子節(jié)點(diǎn)中的事件。若點(diǎn)擊一個(gè)段落中的按鈕,段落的事件處理器也會(huì)收到點(diǎn)擊事件慎王。

但若段落和按鈕都有事件處理器蚓土,則先執(zhí)行最特殊的事件處理器(按鈕的事件處理器)。也就是說事件向外傳播柬祠,從觸發(fā)事件的節(jié)點(diǎn)到其父節(jié)點(diǎn)北戏,最后直到文檔根節(jié)點(diǎn)。最后漫蛔,當(dāng)某個(gè)特定節(jié)點(diǎn)上注冊的所有事件處理器按其順序全部執(zhí)行完畢后,窗口對(duì)象的事件處理器才有機(jī)會(huì)響應(yīng)事件旧蛾。

事件處理器任何時(shí)候都可以調(diào)用事件對(duì)象的stopPropagation方法莽龟,阻止事件進(jìn)一步傳播。該方法有時(shí)很實(shí)用锨天,例如毯盈,你將一個(gè)按鈕放在另一個(gè)可點(diǎn)擊元素中,但你不希望點(diǎn)擊該按鈕會(huì)激活外部元素的點(diǎn)擊行為病袄。

下面的示例代碼將mousedown處理器注冊到按鈕和其外部的段落節(jié)點(diǎn)上搂赋。在按鈕上點(diǎn)擊鼠標(biāo)右鍵,按鈕的處理器會(huì)調(diào)用stopPropagation益缠,調(diào)度段落上的事件處理器執(zhí)行脑奠。當(dāng)點(diǎn)擊鼠標(biāo)其他鍵時(shí),兩個(gè)處理器都會(huì)執(zhí)行幅慌。

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

大多數(shù)事件對(duì)象都有target屬性宋欺,指的是事件來源節(jié)點(diǎn)。你可以根據(jù)該屬性防止無意中處理了傳播自其他節(jié)點(diǎn)的事件胰伍。

我們也可以使用target屬性來創(chuàng)建出特定類型事件的處理網(wǎng)絡(luò)齿诞。例如,如果一個(gè)節(jié)點(diǎn)中包含了很長的按鈕列表骂租,比較方便的處理方式是在外部節(jié)點(diǎn)上注冊一個(gè)點(diǎn)擊事件處理器祷杈,并根據(jù)事件的target屬性來區(qū)分用戶按下了哪個(gè)按鈕,而不是為每個(gè)按鈕都注冊獨(dú)立的事件處理器渗饮。

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

默認(rèn)動(dòng)作

大多數(shù)事件都有與其關(guān)聯(lián)的默認(rèn)動(dòng)作但汞。若點(diǎn)擊鏈接,就會(huì)跳轉(zhuǎn)到鏈接目標(biāo)抽米。若點(diǎn)擊向下的箭頭特占,瀏覽器會(huì)向下翻頁。若右擊鼠標(biāo)云茸,可以得到一個(gè)上下文菜單等是目。

對(duì)于大多數(shù)類型的事件,JavaScript 事件處理器會(huì)在默認(rèn)行為發(fā)生之前調(diào)用标捺。若事件處理器不希望執(zhí)行默認(rèn)行為(通常是因?yàn)橐呀?jīng)處理了該事件)懊纳,會(huì)調(diào)用preventDefault事件對(duì)象的方法揉抵。

你可以實(shí)現(xiàn)你自己的鍵盤快捷鍵或交互式菜單。你也可以干擾用戶期望的行為嗤疯。例如冤今,這里實(shí)現(xiàn)一個(gè)無法跳轉(zhuǎn)的鏈接。

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

除非你有非常充足的理由茂缚,否則不要這樣做戏罢。當(dāng)預(yù)期的行為被打破時(shí),使用你的頁面的人會(huì)感到不快脚囊。

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

按鍵事件

當(dāng)按下鍵盤上的按鍵時(shí),瀏覽器會(huì)觸發(fā)"keydown"事件衬以。當(dāng)松開按鍵時(shí)缓艳,會(huì)觸發(fā)"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這個(gè)事件名上看應(yīng)該是物理按鍵按下時(shí)觸發(fā)看峻,但當(dāng)持續(xù)按下某個(gè)按鍵時(shí)阶淘,會(huì)循環(huán)觸發(fā)該事件。有時(shí)备籽,你想謹(jǐn)慎對(duì)待它舶治。例如,如果您在按下某個(gè)按鍵時(shí)向 DOM 添加按鈕车猬,并且在釋放按鍵時(shí)再次將其刪除霉猛,則可能會(huì)在按住某個(gè)按鍵的時(shí)間過長時(shí),意外添加數(shù)百個(gè)按鈕珠闰。

該示例查看了事件對(duì)象的key屬性惜浅,來查看事件關(guān)于哪個(gè)鍵。 該屬性包含一個(gè)字符串伏嗜,對(duì)于大多數(shù)鍵坛悉,它對(duì)應(yīng)于按下該鍵時(shí)將鍵入的內(nèi)容。 對(duì)于像Enter這樣的特殊鍵承绸,它包含一個(gè)用于命名鍵的字符串(在本例中為"Enter")裸影。 如果你按住一個(gè)鍵的同時(shí)按住Shift鍵,這也可能影響鍵的名稱 - "v"變?yōu)?code>"V"军熏,"1"可能變成"!"轩猩,這是按下Shift-1鍵 在鍵盤上產(chǎn)生的東西。

諸如shiftctrl均践、altmeta(Mac 上的command)之類的修飾按鍵會(huì)像普通按鍵一樣產(chǎn)生事件晤锹。但在查找組合鍵時(shí),你也可以查看鍵盤和鼠標(biāo)事件的shiftKey彤委、ctrlKey鞭铆、altKeymetaKey屬性來判斷這些鍵是否被按下。

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

按鍵事件發(fā)生的 DOM 節(jié)點(diǎn)取決于按下按鍵時(shí)具有焦點(diǎn)的元素焦影。 大多數(shù)節(jié)點(diǎn)不能擁有焦點(diǎn)车遂,除非你給他們一個(gè)tabindex屬性,但像鏈接斯辰,按鈕和表單字段可以艰额。 我們將在第 18 章中回顧表單字段。 當(dāng)沒有特別的焦點(diǎn)時(shí)椒涯,document.body充當(dāng)按鍵事件的目標(biāo)節(jié)點(diǎn)。

當(dāng)用戶鍵入文本時(shí)回梧,使用按鍵事件來確定正在鍵入的內(nèi)容是有問題的废岂。 某些平臺(tái),尤其是 Android 手機(jī)上的虛擬鍵盤狱意,不會(huì)觸發(fā)按鍵事件湖苞。 但即使你有一個(gè)老式鍵盤,某些類型的文本輸入也不能直接匹配按鍵详囤,例如其腳本不適合鍵盤的人所使用的 IME(“輸入法編輯器”)軟件 财骨,其中組合多個(gè)熱鍵來創(chuàng)建字符。

要注意什么時(shí)候輸入了內(nèi)容藏姐,每當(dāng)用戶更改其內(nèi)容時(shí)隆箩,可以鍵入的元素(例如<input><textarea>標(biāo)簽)觸發(fā)"input"事件。為了獲得輸入的實(shí)際內(nèi)容羔杨,最好直接從焦點(diǎn)字段中讀取它捌臊。 第 18 章將展示如何實(shí)現(xiàn)。

指針事件

目前有兩種廣泛使用的方式兜材,用于指向屏幕上的東西:鼠標(biāo)(包括類似鼠標(biāo)的設(shè)備理澎,如觸摸板和軌跡球)和觸摸屏。 它們產(chǎn)生不同類型的事件曙寡。

鼠標(biāo)點(diǎn)擊

點(diǎn)擊鼠標(biāo)按鍵會(huì)觸發(fā)一系列事件糠爬。"mousedown"事件和"mouseup"事件類似于"keydown""keyup"事件,當(dāng)鼠標(biāo)按鈕按下或釋放時(shí)觸發(fā)举庶。當(dāng)事件發(fā)生時(shí)皂林,由鼠標(biāo)指針下方的 DOM 節(jié)點(diǎn)觸發(fā)事件。

mouseup事件后油昂,包含鼠標(biāo)按下與釋放的特定節(jié)點(diǎn)會(huì)觸發(fā)"click"事件。例如捅膘,如果我在一個(gè)段落上按下鼠標(biāo),移動(dòng)到另一個(gè)段落上釋放鼠標(biāo)滚粟,"click"事件會(huì)發(fā)生在包含這兩個(gè)段落的元素上寻仗。

若兩次點(diǎn)擊事件觸發(fā)時(shí)機(jī)接近,則在第二次點(diǎn)擊事件之后凡壤,也會(huì)觸發(fā)"dbclick"(雙擊署尤,double-click)事件。

為了獲得鼠標(biāo)事件觸發(fā)的精確信息亚侠,你可以查看事件中的clientXclientY屬性曹体,包含了事件相對(duì)于窗口左上角的坐標(biāo)(以像素為單位)∠趵茫或pageXpageY箕别,它們相對(duì)于整個(gè)文檔的左上角(當(dāng)窗口被滾動(dòng)時(shí)可能不同)。

下面的代碼實(shí)現(xiàn)了簡單的繪圖程序滞谢。每次點(diǎn)擊文檔時(shí)串稀,會(huì)在鼠標(biāo)指針下添加一個(gè)點(diǎn)。還有一個(gè)稍微優(yōu)化的繪圖程序狮杨,請(qǐng)參見第 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>

鼠標(biāo)移動(dòng)

每次鼠標(biāo)移動(dòng)時(shí)都會(huì)觸發(fā)"mousemove"事件。該事件可用于跟蹤鼠標(biāo)位置橄教。當(dāng)實(shí)現(xiàn)某些形式的鼠標(biāo)拖拽功能時(shí)清寇,該事件非常有用。

舉一個(gè)例子护蝶,下面的程序展示一條欄华烟,并設(shè)置一個(gè)事件處理器,當(dāng)向左拖動(dòng)這個(gè)欄時(shí)滓走,會(huì)使其變窄垦江,若向右拖動(dòng)則變寬。

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

請(qǐng)注意搅方,mousemove處理器注冊在窗口對(duì)象上比吭。即使鼠標(biāo)在改變窗口尺寸時(shí)在欄外側(cè)移動(dòng),只要按住按鈕姨涡,我們?nèi)匀幌胍缕浯笮 ?/p>

釋放鼠標(biāo)按鍵時(shí)衩藤,我們必須停止調(diào)整欄的大小。 為此涛漂,我們可以使用buttons屬性(注意復(fù)數(shù)形式)赏表,它告訴我們當(dāng)前按下的按鍵检诗。 當(dāng)它為零時(shí),沒有按下按鍵瓢剿。 當(dāng)按鍵被按住時(shí)逢慌,其值是這些按鍵的代碼總和 - 左鍵代碼為 1,右鍵為 2间狂,中鍵為 4攻泼。 這樣,您可以通過獲取buttons的剩余值及其代碼鉴象,來檢查是否按下了給定按鍵忙菠。

請(qǐng)注意,這些代碼的順序與button使用的順序不同纺弊,中鍵位于右鍵之前牛欢。 如前所述,一致性并不是瀏覽器編程接口的強(qiáng)項(xiàng)淆游。

觸摸事件

我們使用的圖形瀏覽器的風(fēng)格傍睹,是考慮到鼠標(biāo)界面的情況下而設(shè)計(jì)的,那個(gè)時(shí)候觸摸屏非常罕見犹菱。 為了使網(wǎng)絡(luò)在早期的觸摸屏手機(jī)上“工作”焰望,在某種程度上,這些設(shè)備的瀏覽器假裝觸摸事件是鼠標(biāo)事件已亥。 如果你點(diǎn)擊你的屏幕,你會(huì)得到'mousedown'来屠,'mouseup''click'事件虑椎。

但是這種錯(cuò)覺不是很健壯。 觸摸屏與鼠標(biāo)的工作方式不同:它沒有多個(gè)按鈕俱笛,當(dāng)手指不在屏幕上時(shí)不能跟蹤手指(來模擬"mousemove")捆姜,并且允許多個(gè)手指同時(shí)在屏幕上。

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

觸摸交互觸發(fā)了特定的事件類型磕仅。 當(dāng)手指開始觸摸屏幕時(shí)珊豹,您會(huì)看到'touchstart'事件。 當(dāng)它在觸摸中移動(dòng)時(shí)榕订,觸發(fā)"touchmove"事件店茶。 最后,當(dāng)它停止觸摸屏幕時(shí)劫恒,您會(huì)看到"touchend"事件贩幻。

由于許多觸摸屏可以同時(shí)檢測多個(gè)手指轿腺,這些事件沒有與其關(guān)聯(lián)的一組坐標(biāo)。 相反丛楚,它們的事件對(duì)象擁有touches屬性族壳,它擁有一個(gè)類數(shù)組對(duì)象,每個(gè)對(duì)象都有自己的clientX趣些,clientY仿荆,pageXpageY屬性。

你可以這樣喧务,在每個(gè)觸摸手指周圍顯示紅色圓圈赖歌。

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

您經(jīng)常希望在觸摸事件處理器中調(diào)用preventDefault,來覆蓋瀏覽器的默認(rèn)行為(可能包括在滑動(dòng)時(shí)滾動(dòng)頁面)功茴,并防止觸發(fā)鼠標(biāo)事件庐冯,您也可能擁有它的處理器。

滾動(dòng)事件

每當(dāng)元素滾動(dòng)時(shí)坎穿,會(huì)觸發(fā)scroll事件展父。該事件用處極多,比如知道用戶當(dāng)前查看的元素(禁用用戶視線以外的動(dòng)畫玲昧,或向邪惡的指揮部發(fā)送監(jiān)視報(bào)告)栖茉,或展示一些滾動(dòng)的跡象(通過高亮表格的部分內(nèi)容,或顯示頁碼)孵延。

以下示例在文檔上方繪制一個(gè)進(jìn)度條吕漂,并在您向下滾動(dòng)時(shí)更新它來填充:

<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時(shí),其行為和absolute很像尘应,但可以防止在文檔滾動(dòng)時(shí)期跟著文檔一起滾動(dòng)惶凝。其效果是讓我們的進(jìn)度條呆在最頂上。 改變其寬度來指示當(dāng)前進(jìn)度犬钢。 在設(shè)置寬度時(shí)苍鲜,我們使用%而不是px作為單位,使元素的大小相對(duì)于頁面寬度玷犹。

innerHeight全局綁定是窗口高度混滔,我們必須要減去滾動(dòng)條的高度。你點(diǎn)擊文檔底部的時(shí)候是無法繼續(xù)滾動(dòng)的歹颓。對(duì)于窗口高度來說坯屿,也存在innerWidth。使用pageYOffset(當(dāng)前滾動(dòng)位置)除以最大滾動(dòng)位置巍扛,并乘以 100愿伴,就可以得到進(jìn)度條長度。

調(diào)用滾動(dòng)事件的preventDefault無法阻止?jié)L動(dòng)电湘。實(shí)際上隔节,事件處理器是在進(jìn)行滾動(dòng)之后才觸發(fā)的鹅经。

焦點(diǎn)事件

當(dāng)元素獲得焦點(diǎn)時(shí),瀏覽器會(huì)觸發(fā)其上的focus事件怎诫。當(dāng)失去焦點(diǎn)時(shí)瘾晃,元素會(huì)獲得blur事件。

與前文討論的事件不同幻妓,這兩個(gè)事件不會(huì)傳播蹦误。子元素獲得或失去焦點(diǎn)時(shí),不會(huì)激活父元素的處理器肉津。

下面的示例中强胰,文本域在擁有焦點(diǎn)時(shí)會(huì)顯示幫助文本。

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

當(dāng)用戶從瀏覽器標(biāo)簽或窗口移開時(shí)妹沙,窗口對(duì)象會(huì)收到focus事件偶洋,當(dāng)移動(dòng)到標(biāo)簽或窗口上時(shí),則收到blur事件距糖。

加載事件

當(dāng)界面結(jié)束裝載時(shí)玄窝,會(huì)觸發(fā)窗口對(duì)象和文檔body對(duì)象的"load"事件。該事件通常用于在當(dāng)整個(gè)文檔構(gòu)建完成時(shí)悍引,進(jìn)行初始化恩脂。請(qǐng)記住<script>標(biāo)簽的內(nèi)容是一遇到就執(zhí)行的。這可能太早了趣斤,比如有時(shí)腳本需要處理在<script>標(biāo)簽后出現(xiàn)的內(nèi)容俩块。

諸如imagescript這類會(huì)裝載外部文件的標(biāo)簽都有load事件,指示其引用文件裝載完畢浓领。類似于焦點(diǎn)事件典阵,裝載事件是不會(huì)傳播的。

當(dāng)頁面關(guān)閉或跳轉(zhuǎn)(比如跳轉(zhuǎn)到一個(gè)鏈接)時(shí)镊逝,會(huì)觸發(fā)beforeunload事件。該事件用于防止用戶突然關(guān)閉文檔而丟失工作結(jié)果嫉鲸。你無法使用preventDefault方法阻止頁面卸載撑蒜。它通過從處理器返回非空值來完成。當(dāng)你這樣做時(shí)玄渗,瀏覽器會(huì)通過顯示一個(gè)對(duì)話框座菠,詢問用戶是否關(guān)閉頁面的對(duì)話框中。該機(jī)制確保用戶可以離開藤树,即使在那些想要留住用戶浴滴,強(qiáng)制用戶看廣告的惡意頁面上,也是這樣岁钓。

事件和事件循環(huán)

在事件循環(huán)的上下文中升略,如第 11 章中所述微王,瀏覽器事件處理器的行為,類似于其他異步通知品嚣。 它們是在事件發(fā)生時(shí)調(diào)度的炕倘,但在它們有機(jī)會(huì)運(yùn)行之前,必須等待其他正在運(yùn)行的腳本完成翰撑。

僅當(dāng)沒有別的事情正在運(yùn)行時(shí)罩旋,才能處理事件,這個(gè)事實(shí)意味著眶诈,如果事件循環(huán)與其他工作捆綁在一起涨醋,任何頁面交互(通過事件發(fā)生)都將延遲,直到有時(shí)間處理它為止逝撬。 因此浴骂,如果您安排了太多工作,無論是長時(shí)間運(yùn)行的事件處理器還是大量短時(shí)間運(yùn)行的工作球拦,該頁面都會(huì)變得緩慢且麻煩靠闭。

如果您想在背后做一些耗時(shí)的事情而不會(huì)凍結(jié)頁面,瀏覽器會(huì)提供一些名為 Web Worker 的東西坎炼。 Web Worker 是一個(gè) JavaScript 過程愧膀,與主腳本一起在自己的時(shí)間線上運(yùn)行。

想象一下谣光,計(jì)算一個(gè)數(shù)字的平方運(yùn)算是一個(gè)重量級(jí)的檩淋,長期運(yùn)行的計(jì)算,我們希望在一個(gè)單獨(dú)的線程中執(zhí)行萄金。 我們可以編寫一個(gè)名為code/squareworker.js的文件蟀悦,通過計(jì)算平方并發(fā)回消息來響應(yīng)消息:

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

為了避免多線程觸及相同數(shù)據(jù)的問題,Web Worker 不會(huì)將其全局作用域或任何其他數(shù)據(jù)與主腳本的環(huán)境共享氧敢。 相反日戈,你必須通過來回發(fā)送消息與他們溝通。

此代碼會(huì)生成一個(gè)運(yùn)行該腳本的 Web Worker孙乖,向其發(fā)送幾條消息并輸出響應(yīng)浙炼。

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

函數(shù)postMessage會(huì)發(fā)送一條消息,觸發(fā)接收方的message事件唯袄。創(chuàng)建工作單元的腳本通過Worker對(duì)象收發(fā)消息弯屈,而worker則直接向其全局作用域發(fā)送消息,或監(jiān)聽其消息恋拷。只有可以表示為 JSON 的值可以作為消息發(fā)送 - 另一方將接收它們的副本资厉,而不是值本身。

定時(shí)器

我們在第 11 章中看到了setTimeout函數(shù)蔬顾。 它會(huì)在給定的毫秒數(shù)之后宴偿,調(diào)度另一個(gè)函數(shù)在稍后調(diào)用湘捎。

有時(shí)讀者需要取消調(diào)度的函數(shù)±椅遥可以存儲(chǔ)setTimeout的返回值消痛,并將作為參數(shù)調(diào)用clearTimeout

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

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

函數(shù)cancelAnimationFrame作用與clearTimeout相同都哭,使用requestAnimationFrame的返回值調(diào)用該函數(shù)秩伞,可以取消幀(假定函數(shù)還沒有被調(diào)用)。

還有setIntervalclearInterval這種相似的函數(shù)欺矫,用于設(shè)置計(jì)時(shí)器纱新,每隔一定毫秒數(shù)重復(fù)執(zhí)行一次。

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

降頻

某些類型的事件可能會(huì)連續(xù)穆趴、迅速觸發(fā)多次(例如mousemovescroll事件)脸爱。處理這類事件時(shí),你必須小心謹(jǐn)慎未妹,防止處理任務(wù)耗時(shí)過長簿废,否則處理器會(huì)占據(jù)過多事件,導(dǎo)致用戶與文檔交互變得非常慢络它。

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

在第一個(gè)示例中,當(dāng)用戶輸入某些字符時(shí)点楼,我們想要有所反應(yīng)扫尖,但我們不想在每個(gè)按鍵事件中立即處理該任務(wù)。當(dāng)用戶輸入過快時(shí)掠廓,我們希望暫停一下然后進(jìn)行處理换怖。我們不是立即在事件處理器中執(zhí)行動(dòng)作,而是設(shè)置一個(gè)定時(shí)器蟀瞧。我們也會(huì)清除上一次的定時(shí)器(如果有)沉颂,因此當(dāng)兩個(gè)事件觸發(fā)間隔過短(比定時(shí)器延時(shí)短),就會(huì)取消上一次事件設(shè)置的定時(shí)器黄橘。

<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或在一個(gè)已結(jié)束的定時(shí)器上調(diào)用clearTimeout是沒有效果的。因此屈溉,我們不需要關(guān)心何時(shí)調(diào)用該方法塞关,只需要每個(gè)事件中都這樣做即可。

如果我們想要保證每次響應(yīng)之間至少間隔一段時(shí)間子巾,但不希望每次事件發(fā)生時(shí)都重置定時(shí)器帆赢,而是在一連串事件連續(xù)發(fā)生時(shí)能夠定時(shí)觸發(fā)響應(yīng)小压,那么我們可以使用一個(gè)略有區(qū)別的方法來解決問題。例如椰于,我們想要響應(yīng)"mousemove"事件來顯示當(dāng)前鼠標(biāo)坐標(biāo)怠益,但頻率只有 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>

本章小結(jié)

事件處理器可以檢測并響應(yīng)發(fā)生在我們的 Web 頁面上的事件瘾婿。addEventListener方法用于注冊處理器蜻牢。

每個(gè)事件都有標(biāo)識(shí)事件的類型(keydownfocus等)偏陪。大多數(shù)方法都會(huì)在特定 DOM 元素上調(diào)用抢呆,接著向其父節(jié)點(diǎn)傳播,允許每個(gè)父元素的處理器都能處理這些事件笛谦。

JavaScript 調(diào)用事件處理器時(shí)抱虐,會(huì)傳遞一個(gè)包含事件額外信息的事件對(duì)象。該對(duì)象也有方法支持停止進(jìn)一步傳播(stopPropagation)饥脑,也支持阻止瀏覽器執(zhí)行事件的默認(rèn)處理器(preventDefault)恳邀。

按下鍵盤按鍵時(shí)會(huì)觸發(fā)keydownkeyup事件。按下鼠標(biāo)按鈕時(shí)灶轰,會(huì)觸發(fā)mousedown谣沸、mouseupclick事件。移動(dòng)鼠標(biāo)會(huì)觸發(fā)mousemove事件框往。觸摸屏交互會(huì)導(dǎo)致"touchstart"鳄抒,"touchmove""touchend"事件。

我們可以通過scroll事件監(jiān)測滾動(dòng)行為椰弊,可以通過focusblur事件監(jiān)控焦點(diǎn)改變许溅。當(dāng)文檔完成加載后,會(huì)觸發(fā)窗口的load事件秉版。

習(xí)題

氣球

編寫一個(gè)顯示氣球的頁面(使用氣球 emoji贤重,\ud83c\udf88)。 當(dāng)你按下上箭頭時(shí)清焕,它應(yīng)該變大(膨脹)10%并蝗,而當(dāng)你按下下箭頭時(shí),它應(yīng)該縮薪胀住(放氣)10%滚停。

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

箭頭鍵的鍵名是"ArrowUp""ArrowDown"。確保按鍵只更改氣球起惕,而不滾動(dòng)頁面涡贱。

實(shí)現(xiàn)了之后,添加一個(gè)功能惹想,如果你將氣球吹過一定的尺寸问词,它就會(huì)爆炸。 在這種情況下嘀粱,爆炸意味著將其替換為“爆炸 emoji激挪,\ud83d\udca5”,并且移除事件處理器(以便您不能使爆炸變大變胁菽隆)灌灾。

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

鼠標(biāo)軌跡

在 JavaScript 早期,有許多主頁都會(huì)在頁面上使用大量的動(dòng)畫悲柱,人們想出了許多該語言的創(chuàng)造性用法锋喜。

其中一種是“鼠標(biāo)蹤跡”,也就是一系列的元素豌鸡,隨著你在頁面上移動(dòng)鼠標(biāo)嘿般,它會(huì)跟著你的鼠標(biāo)指針。

在本習(xí)題中實(shí)現(xiàn)鼠標(biāo)軌跡的功能涯冠。使用絕對(duì)定位炉奴、固定尺寸的<div>元素,背景為黑色(請(qǐng)參考鼠標(biāo)點(diǎn)擊一節(jié)中的示例)蛇更。創(chuàng)建一系列此類元素瞻赶,當(dāng)鼠標(biāo)移動(dòng)時(shí),伴隨鼠標(biāo)指針顯示它們派任。

有許多方案可以實(shí)現(xiàn)我們所需的功能砸逊。你可以根據(jù)你的需要實(shí)現(xiàn)簡單的或復(fù)雜的方法。簡單的解決方案是保存固定鼠標(biāo)的軌跡元素并循環(huán)使用它們掌逛,每次mousemove事件觸發(fā)時(shí)將下一個(gè)元素移動(dòng)到鼠標(biāo)當(dāng)前位置师逸。

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

選項(xiàng)卡

選項(xiàng)卡面板廣泛用于用戶界面。它支持用戶通過選擇元素上方的很多突出的選項(xiàng)卡來選擇一個(gè)面板豆混。

本習(xí)題中篓像,你必須實(shí)現(xiàn)一個(gè)簡單的選項(xiàng)卡界面耘婚。編寫asTabs函數(shù)明垢,接受一個(gè) DOM 節(jié)點(diǎn)并創(chuàng)建選項(xiàng)卡界面來展現(xiàn)該節(jié)點(diǎn)的子元素。該函數(shù)應(yīng)該在頂層節(jié)點(diǎn)中插入大量<button>元素逆日,與每個(gè)子元素一一對(duì)應(yīng)鸵鸥,按鈕文本從子節(jié)點(diǎn)的data-tabname中獲取奠滑。除了顯示一個(gè)初始子節(jié)點(diǎn),其他子節(jié)點(diǎn)都應(yīng)該隱藏(將display樣式設(shè)置成none),并通過點(diǎn)擊按鈕來選擇當(dāng)前顯示的節(jié)點(diǎn)养叛。

當(dāng)它生效時(shí)將其擴(kuò)展,為當(dāng)前選中的選項(xiàng)卡宰翅,將按鈕的樣式設(shè)為不同的弃甥,以便明確選擇了哪個(gè)選項(xiàng)卡。

<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>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末汁讼,一起剝皮案震驚了整個(gè)濱河市淆攻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嘿架,老刑警劉巖瓶珊,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耸彪,居然都是意外死亡伞芹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門蝉娜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唱较,“玉大人,你說我怎么就攤上這事召川∧匣海” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵荧呐,是天一觀的道長汉形。 經(jīng)常有香客問我,道長倍阐,這世上最難降的妖魔是什么概疆? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮收捣,結(jié)果婚禮上届案,老公的妹妹穿的比我還像新娘。我一直安慰自己罢艾,他們只是感情好楣颠,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著咐蚯,像睡著了一般童漩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上春锋,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天矫膨,我揣著相機(jī)與錄音,去河邊找鬼。 笑死侧馅,一個(gè)胖子當(dāng)著我的面吹牛危尿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播馁痴,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼谊娇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了罗晕?” 一聲冷哼從身側(cè)響起济欢,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎小渊,沒想到半個(gè)月后法褥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡酬屉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年半等,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呐萨。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酱鸭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出垛吗,到底是詐尸還是另有隱情凹髓,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布怯屉,位于F島的核電站蔚舀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏锨络。R本人自食惡果不足惜赌躺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望羡儿。 院中可真熱鬧礼患,春花似錦、人聲如沸掠归。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虏冻。三九已至肤粱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間厨相,已是汗流浹背领曼。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來泰國打工鸥鹉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人庶骄。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓毁渗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親单刁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子祝蝠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • ??JavaScript 與 HTML 之間的交互是通過事件實(shí)現(xiàn)的。 ??事件细溅,就是文檔或?yàn)g覽器窗口中發(fā)生的一些特...
    霜天曉閱讀 3,502評(píng)論 1 11
  • 本節(jié)介紹各種常見的瀏覽器事件褥傍。 鼠標(biāo)事件 鼠標(biāo)事件指與鼠標(biāo)相關(guān)的事件,主要有以下一些喇聊。 click 事件恍风,dblc...
    許先生__閱讀 2,446評(píng)論 0 4
  • 事件類型 Web 瀏覽器中可能發(fā)生的事件有很多類型UI事件:當(dāng)用戶與界面上的元素交互時(shí)觸發(fā)。焦點(diǎn)事件:當(dāng)元素獲得或...
    shanruopeng閱讀 913評(píng)論 0 0
  • 13.1 事件流 “DOM2級(jí)事件”規(guī)定事件流包括3個(gè)階段:事件捕獲階段,處于目標(biāo)階段窜骄,事件冒泡階段锦募。事件捕獲表示...
    Elevens_regret閱讀 430評(píng)論 0 0
  • JavaScript 與 HTML 間通過事件實(shí)現(xiàn)交互。事件——文檔或?yàn)g覽器窗口中發(fā)生的一些特定的交互瞬間邻遏,即用戶...
    sylvia_yue閱讀 473評(píng)論 0 0