# JS30 DAY1 JavaScript Drum Kit ![](https://hackmd.io/_uploads/Bk-xaTlrle.jpg) ## 今天學到什麼? 1. `<kbd></kbd>`標示鍵盤按鍵 2. `<audio></audio>`嵌入音訊元素 3. 使用querySelector取得data-* attribute自訂屬性 4. 監聽按下鍵盤事件(keydown)、動畫結束事件(transitionend) 5. e.propertyName 6. e.keyCode 7. audio.currentTime、audio.play() 8. Element.classList.add()及Element.classList.remove()操作CSS類別 9. this ## 核心技術 ### 1. 鍵盤事件處理和keyCode使用 按下鍵盤時取得對應的keyCode,識別按鍵的動作 - addEventListener('keydown',...):按下鍵盤事件 - keyCode鍵碼:表示每個按鍵的數字代碼 ```javascript window.addEventListener('keydown', function(e) { console.log(e.keyCode); }); ``` ### 2. 控制Audio音檔 - currentTime:指定當前的播放時間點(以秒為單位) ```javascript audio.currentTime = 0; //重置播放時間 audio.play(); //播放音效 ``` ### 3. DOM 選取data-* 和 classList 操作 - data-*:當屬性有特殊意義時使用自定義的屬性比起id、class更易讀。 (此處data-key值為數字,若用id、class難以判斷出是指鍵碼) - 選取data-*:屬性選擇器`[attribute="value"]`搭配樣版字面值 ```javascript // 選取所有符合「key class中有data-key="${e.keyCode}"」的元素 const key = document.querySelector(`.key[data-key="${e.keyCode}"]`); // 選取所有符合「<audio>標籤中有data-key="${e.keyCode}"」的元素 const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`); ``` - classList.add():增加class - classList.remove():移除class ```javascript // 以此方式控制CSS的效果 key.classList.add('playing'); key.classList.remove('playing'); ``` ### 4. 監聽轉場動畫結束後才移除class - addEventListener('transitioned',...):轉場結束事件 這個屬性可以在e.propertyName中取得,當CSS轉場結束才會觸發 ```javascript const keys = document.querySelectorAll('.key'); // keys 是一個 NodeList,不是DOM元素,所以不能直接使用addEventListener方法 keys.forEach(key => key.addEventListener('transitionend', removeTransition)); function removeTransition(e) { // 如果不是transform,直接跳過 if (e.propertyName !== 'transform') return; // 當transition結束時(transitionend),移除playing this.classList.remove('playing'); } ``` ## 我遇到的問題 ### 問題1:用 setTimeout 移除動畫效果的時機不正確 - 原因: 1. 時間不準確:setTimeout 設定的時間是固定的,可能與CSS轉場動畫的時長不一樣,且JS計時器與CSS動畫是獨立運行的,不能保證同步。 2. 快速按鍵時有衝突:如果在設定的時間內重複按同一鍵,會導致第一次的還沒執行,第二次又設定新的 setTimeout - 解決方法:監聽 transitionend 事件,可以確保在轉場結束後才執行函式 ```javascript ...key.addEventListener('transitionend', removeTransition)... ``` ### 問題2:連續按鍵時,音檔會不同步 - 預期:每按一下都重播一次 - 原因:音檔預設要完整播完後才會停止,所以連續按時還沒播完前一次的就沒反應 - 解決方法:用currentTime指定時間0,每次按鍵時會先回到0秒才開始播 ```javascript audio.currentTime = 0; //先重置才播放 audio.play(); ``` ### 問題3:this指哪? ```javascript! const keys = document.querySelectorAll('.key'); // keys 是一個 NodeList,不是DOM元素,所以不能直接使用addEventListener方法 keys.forEach(key => key.addEventListener('transitionend', removeTransition)); function removeTransition(e) { // 如果不是transform,直接跳過 if (e.propertyName !== 'transform') return; // 當transition結束時(transitionend),移除playing this.classList.remove('playing'); } ``` 此處this,指的是keys.forEach中被觸發的DOM元素 = `key.classList.remove('playing');` = `e.target.classList.remove('playing');` - 箭頭函式不能用this ```javascript! // 一般函數 key.addEventListener('transitionend', function(e) { console.log(this); // 指向觸發事件的 DOM 元素 this.classList.remove('playing'); // ✅ 正確 }); // 箭頭函數 key.addEventListener('transitionend', (e) => { console.log(this); // 指向外層作用域的 this (通常是 window) this.classList.remove('playing'); // ❌ 錯誤!this 不是 DOM 元素 }); ``` > 相關文章: [this作用域](https://ithelp.ithome.com.tw/m/articles/10208958) [解釋 JavaScript 中 this 的值?](https://www.explainthis.io/zh-hant/swe/what-is-this)