[FE102] 前端必備:JavaScript (下)


Posted by s103071049 on 2021-05-26

JavaScript 網頁事件處理

一、eventListener 與 callback function

目標:學習怎麼對事件做出反應。事件很多,例如:捲動(scroll), 按按鍵(keyDown)

eventListener (事件監聽)

首先要先選到元素,我們這邊選 block,接著 element.addEventListener(監聽事件名稱, 函數)。最後重新整理,跑一次 => 點 yo hello~ => 跳出 click

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
  </head>
<body>
  <div id='block'>
    yo
    <a>hello~</a>
  </div>
  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click',onClick)

    function onClick() {
      alert('click!')
    }
  </script>
</body>
</html>

整段代碼:我在 element 上新增 EventListener 他叫 click (我想監聽的事件叫做 click),當有 click 這個事件發生時,瀏覽器幫我觸發 onClick 這個函式。這個函式我們叫他 callback function。在這個 function 中可以做任何想做的事情。

大多數會偷懶直接用匿名函式寫,匿名函式和 callback function 一點關係都沒有,他只是沒有名稱。

  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', function() {
      alert('click!')
    })
  </script>

結論:可以對一個元素進行監聽,監聽什麼樣的事件,一旦這個事件被觸發,就會執行到後面的 function。

二、詳細講解 callback function

callback function(回呼函式)

Recall:監聽事件要先建立事件監聽器,EventListener(),第一個傳要監聽的事件名稱,第二個傳一個 function。

當有人點按鈕時,幫我呼叫這個 function。也就是當有人觸發監聽事件,你再告訴我,我就不用一直等在那邊,我可以去做其他事情,一旦這個事件發生,就呼叫這個 function。

  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', function() {
      alert('click!')
    })
  </script>

為甚麼要這樣寫? 你不知道使用者甚麼時候要按按鈕,如果按照下列寫法,程式會一直卡在那邊等待事件觸發。

  <script>
    const element = document.querySelector('#block')
    const event = element.addEventListener('click')
    function onClick() {
      alert('click!')
    }
  </script>

三、event(e) 是什麼碗糕?

callback function 可以拿到更多資訊。瀏覽器在乎叫 function 會拿進參數,參數可以隨意命名,通常命名 event 或 e。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
  </head>
<body>
  <div id='block'>
    yo
    <a>hello~</a>
  </div>
  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', function(e) {
      console.log(e)
    })
  </script>
</body>
</html>

也可以是這樣的寫法

  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', onClick)

    function onClick(e) {
      console.log(e)
    }
  </script>

e.target 表示我點了哪個元素。所以,可以用 e.target 取得我到底點了哪個元素。

  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', function(e) {
      console.log(e.target)
    })
  </script>

以瀏覽器的角度,當我點了這個 element,瀏覽器會幫我呼叫這個 function,然後 e 會帶一些資訊進去。所以後面的 e 是瀏覽器帶給我的資訊。

<body>
  <div id='block'>
    yo
    <a>hello~</a>
  </div>
  <input/>
  <script>
    const element = document.querySelector('input')
    element.addEventListener('keydown', function (e) {
      console.log(e.key)
    })
  </script>
</body>

按按鈕切換背景顏色

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>
      .active {
        background: red;
      }
    </style>
  </head>
<body>
  <div id='block'>
    yo
    <a>hello~</a>
    <button class='change-btn'>change </button>
  </div>
  <script>
    const element = document.querySelector('.change-btn')
    element.addEventListener('click', function (e) {
      document.querySelector('body').classList.toggle('active')
    })
  </script>
</body>
</html>

e 是瀏覽器幫我傳過來的變數,裡面會有和我事件相關的資訊。

四、表單事件處理 onSubmit

可以在表單上加上 submit 這個事件,在表單送出以前就會觸發這個事件。可以對這個事件做一些處理,例如 : 表單不要送出,通常會用在表單驗證。

表單如果沒有指定,他預設的網頁會是同個網頁,method 會是 get。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>

    </style>
  </head>
<body>
  <form class='login-form' method="GET" action="">
    <div>
      username: <input name="username"/>
    </div>
    <div>
      password: <input name="password" type="password"/>
    </div>
    <div>
      password again: <input name="password2" type="password"/>
    </div>
    <input type="submit"/>      
  </form>
  <script>
    const element = document.querySelector('.login-form')
    element.addEventListener('submit', function (e) {
      alert("submit")
    })
  </script>
</body>
</html>

e.preventDefault() 表示阻止他預設的行為,表單的預設行為=submit,preventDefault() 是 e 裡面的一個函式。
所以現在就不送出了。

  <script>
    const element = document.querySelector('.login-form')
    element.addEventListener('submit', function(e) {
      e.preventDefault()
    })
  </script>

如何拿值,透過 input1.value

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>

    </style>
  </head>
<body>
  <form class='login-form' method="GET" action="">
    <div>
      username: <input name="username"/>
    </div>
    <div>
      password: <input name="password" type="password"/>
    </div>
    <div>
      password again: <input name="password2" type="password"/>
    </div>
    <input type="submit"/>      
  </form>
  <script>
    const element = document.querySelector('.login-form')
    element.addEventListener('submit', function(e) {
      const input1 = document.querySelector('input[name=password]')
      const input2 = document.querySelector('input[name=password2]')
      if (input1.value !== input2.value) {
        alert('密碼不同')
        e.preventDefault()
      }

    })
  </script>
</body>
</html>

五、阻止預設行為:preventDefault

阻止瀏覽器的預設行為 e.preventDefault,常見用法:超連結、表單驗證。

超連結的預設是按了之後點到某個地方,所以加了 e.preventDefault 不管怎麼點他都不會有反應。

    <a href="https://www.google.com/">link</a>      
  </form>
  <script>
    const element = document.querySelector('a')
    element.addEventListener('click', function(e) {
      e.preventDefault()
    })

  </script>

這樣寫,表示我沒辦法打 e 這個字。

<body>
  <form class='login-form' method="GET" action="">
    <div>
      username: <input name="username"/>
    </div>
    <div>
      password: <input name="password" type="password"/>
    </div>
    <div>
      password again: <input name="password2" type="password"/>
    </div>
    <input type="submit"/>      
  </form>
  <script>
    const element = document.querySelector('input[name=username]')
    element.addEventListener('keypress', function(e) {
      if (e.key === 'e') {
        e.preventDefault()
      }
    })

  </script>
</body>

六、事件傳遞機制

甚麼是事件傳遞機制

前言:
因為他們是有層層疊疊的關係,所以點綠色區塊,也會觸發到紅色區塊的東西。所以會先從點到最近的那個開始,然後慢慢擴散出去,像漣漪一樣。這個東西就是瀏覽器的事件傳遞機制。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>
      .outer {
        width: 500px;
        height: 200px;
        background: red;
      }

      .inner {
        width: 300px;
        height: 100px;
        background: green;
      }

    </style>
  </head>
<body>
  <div class='outer'>
    <div class='inner'>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')


    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className)
      })
    }
  </script>
</body>
</html>

事件傳遞機制詳解:捕獲與冒泡

先捕獲再冒泡。

如何將 eventListener 掛在不同的階段 ? A:.addEventListener() 放第三個參數-布林,true:捕獲階段、false:冒泡階段(預設為 flase)。

點 click me => 發現先捕獲,再冒泡。

.outer 捕獲
.inner 捕獲
.btn 捕獲
.btn 冒泡
.inner 冒泡
.outer 冒泡

<body>
  <div class='outer'>
    <div class='inner'>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')


    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '捕獲')
      },true)

      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      },false)
    }
  </script>
</body>

target phase 會依據你先放哪個 EventListener 而是哪個 EventListener。
ex:我們在觸發 clickme 他是處於 target phase
.outer 捕獲
.inner 捕獲
.btn 冒泡
.btn 捕獲
.inner 冒泡
.outer 冒泡

  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')


    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      },false)

      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '捕獲')
      },true)


    }
  </script>

放一個監聽在捕獲階段,就可以記錄到所有按鈕的事件。

<body>
  <div class='outer'>
    <div class='inner'>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')

    window.addEventListener('click', function(e) {
      console.log(e)
    }, true)

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      },false)
    }

  </script>
</body>

超連結點下去會完全沒有用,一旦 call 了 e.preventDefault(),這個 e.preventDefault() 會沿路傳下去。所以儘管我們是在這邊加上的,傳到 a 還是會有效果,因此整個頁面的表單送出、超連結都不會有效果。

<body>
  <div class='outer'>
    <div class='inner'>
      <a href='./test'>click</a>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')

    window.addEventListener('click', function(e) {
      e.preventDefault()
    }, true)

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      },false)
    }

  </script>
</body>

七、別向上級回報:stopPropagation

冒泡的過程就像逐級向上回報

當我們點按鈕時,只會觸發按鈕的事件;點綠色的地方,綠色的仍會傳遞。

stopPropagation() 會阻止這個事件繼續傳遞。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>
      .outer {
        width: 500px;
        height: 200px;
        background: red;
      }

      .inner {
        width: 300px;
        height: 100px;
        background: green;
      }

    </style>
  </head>
<body>
  <div class='outer'>
    <div class='inner'>
      <a href='./test'>click</a>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn 冒泡')
      })

  </script>
</body>
</html>

發現,不管怎麼點都不會有 log,連剛剛的 EventListener 都不會觸發。這是因為你在 windows 這個階段就已經阻止他傳下去,所以下面的東西都不會收到事件。

<body>
  <div class='outer'>
    <div class='inner'>
      <a href='./test'>click</a>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')

    window.addEventListener('click', function(e) {
      e.stopPropagation()
    },true)

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn 冒泡')
      })

  </script>
</body>

一個按鈕可以加兩個 eventListener() 是沒有問題。兩個 eventListener() 都會被觸發。

 <script>
    addEvent('.outer')
    addEvent('.inner')

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        console.log('.btn click 1')
      })

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        console.log('.btn click 2')
      })

  </script>

加上 e.stopPropagation(),阻止冒泡,但兩個事件還是會被觸發。因為這兩個事件都在 target phase,所以雖然阻止事件傳遞原本的還是會被觸發。

<body>
  <div class='outer'>
    <div class='inner'>
      <a href='./test'>click</a>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn click 1')
      })

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn click 2')
      })

  </script>
</body>

加上 e.stopImmediatePropagation() 就會阻止其他任何的 EventListener => 只會觸發第一個。會當前立刻阻止所有事件傳遞。

  <script>
    addEvent('.outer')
    addEvent('.inner')

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopImmediatePropagation()
        console.log('.btn click 1')
      })

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn click 2')
      })

  </script>

八、新手 100% 會搞錯的事件機制問題

首先,我們有兩個按鈕,class 都叫 btn,

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>
    </style>
  </head>
<body>
  <div class='outer'>
    <button class='btn'>1</button>
    <button class='btn'>2</button>
  </div>
  <script>
    document.querySelector('.btn')
    .addEventListener('click',function() {

    })
  </script>
</body>
</html>

現在幫 .btn 加 EventListener,點了就是 1 => 點了 btn1 出現 1,但點了 btn2 沒反應。這是因為 document.querySelector 只會回傳第一個元素。也就是說,我們只有幫第一個元素加 EventListener。

    document.querySelector('.btn')
    .addEventListener('click',function() {
      alert(1)
    })

如果想幫每一個都加,使用 querySelectorAll => TypeError: document.querySelectorAll(...).addEventListener is not a function
這是因為 querySelectorAll 回傳的是一個 list。

    document.querySelectorAll('.btn')
    .addEventListener('click',function() {
      alert(1)
    })

修改成以下形式:發現不管點哪個 btn 都是 6
=> 這是因為 scope

<body>
  <div class='outer'>
    <button class='btn'>1</button>
    <button class='btn'>2</button>
    <button class='btn'>3</button>
    <button class='btn'>4</button>
    <button class='btn'>5</button>
  </div>
  <script>
    const elements = document.querySelectorAll('.btn')
    for (var i=0; i<elements.length; i++) {
      elements[i].addEventListener('click',function() {
        alert(i+1)
        })    
    }

  </script>
</body>

這個 function 是在我點擊那刻觸發,點擊那刻 i 的值是 elements.length ,所以迴圈跑完時 i = 5,所以當我點擊時 alert(i+1) = 6

  <script>
    const elements = document.querySelectorAll('.btn')
    for (var i=0; i<elements.length; i++) {
      elements[i].addEventListener('click',function() {
        alert(i+1)
        })    
    }
    alert(i)
  </script>

所以可以如何修正 ? (1) 用 let (2) 新增 data-value,以屬性的方式拿取。

  <div class='outer'>
    <button class='btn' data-value='1'>1</button>
    <button class='btn' data-value='2'>2</button>
    <button class='btn' data-value='3'>3</button>
    <button class='btn' data-value='4'>4</button>
    <button class='btn' data-value='5'>5</button>
  </div>
  <script>
    const elements = document.querySelectorAll('.btn')
    for (var i=0; i<elements.length; i++) {
      elements[i].addEventListener('click',function(e) {
        console.log(e.target.getAttribute('data-value'))
        })    
    }
  </script>

現在,我們想新增按鈕。

<body>
  <div class='outer'>
    <button class='add-btn'>add</button>
    <button class='btn' data-value='1'>1</button>
    <button class='btn' data-value='2'>2</button>
  </div>
  <script>
    let num=3
    const elements = document.querySelectorAll('.btn')
    for (var i=0; i<elements.length; i++) {
      elements[i].addEventListener('click',function(e) {
        alert(e.target.getAttribute('data-value'))
        })    
    }

    document.querySelector('.add-btn').addEventListener('click',       function() {
      const btn = document.createElement('button')
      btn.setAttribute('data-value', num)
      btn.innerText = num
      num++
      document.querySelector('.outer').appendChild(btn)
    })
  </script>
</body>

為甚麼點新增的按鈕,沒有反應 ?
當我在跑 querySelectorAll => 他的 element 也只有 1, 2 兩個按鈕,動態新增後才有新的按鈕,但動態新增的部分並未幫他們加 EventListener。所以他點了就不會有反應。

整理:

  1. 點擊產生事件,會從上傳到下,再從下傳到上。事件機制是不管如何都會發生。不管有沒有加監聽器,事件都會這樣傳。加了監聽器表示在傳遞時,Listener 就可以監聽到這個事件,拿到這個事件,然後我就可以決定怎麼處理。更詳細地說,事件不管怎樣都會傳,我加上監聽器去攔截事件,拿到事件決定對事件如何處理,再繼續傳遞下去或不傳遞。
  2. e.stopPropagation() 阻止事件傳遞,後面的連結會斷掉。所以我後續的 node 就收不到這個事件。
  3. 第三個參數,true/ false 是決定事件要加在哪個 phase
  4. 點到元素本身是 target phase,這時觸發的順序依照先捕獲後冒泡依序傳遞。

九、欸等等幫我拿餐點:event delegation

如果有一百個按鈕,就要加一百個 EventListener ? 有一千個按鈕就要加一千個 EventListener ? 將性質相似的每個元素加上其專屬的 EventListener 這樣顯然很沒效率。

所以怎麼辦呢 ?

我們可以利用事件冒泡的性質,每一個 click event 都會冒泡到 outer 去。所以在 outer 上加 EventListener 就好了。就可以處理下面所有的 btn,動態新增的也可以。

這就叫做 event delegation 事件代理。白話的說,一群人去麥當勞點餐,不可能所有人都在下面等,有一兩個在樓下幫忙取餐,其他人先占位子。那個人就負責拿所有人的餐點,現在這個 outer 就是那個幫大家拿餐點的人。

本來事件就會一直冒泡。


<body>
  <div class='outer'>
    <button class='add-btn'>add</button>
    <button class='btn' data-value='1'>1</button>
    <button class='btn' data-value='2'>2</button>
  </div>
  <script>
    let num=3

    document.querySelector('.add-btn').addEventListener('click', function() {
      const btn = document.createElement('button')
      btn.setAttribute('data-value', num)
      btn.innerText = num
      btn.classList.add('btn')
      num++
      document.querySelector('.outer').appendChild(btn)
    })

    document.querySelector('.outer').addEventListener('click', function(e) {
      if (e.target.classList.contains('btn')) {
        alert(e.target.getAttribute('data-value'))
      }
    })
  </script>
</body>

事件代理機制的優點:
(1) 有效率,不用浪費資源建立無數個做相同事件的 function。只要用一個 EventListener 就可以管理處理這些事情。
(2) 處理動態新增的信息。透過冒泡機制,讓底下就算是新增的東西,一樣可以接到他的事件。










Related Posts

《鳥哥 Linux 私房菜:基礎篇》Chapter 00 - 計算機概論

《鳥哥 Linux 私房菜:基礎篇》Chapter 00 - 計算機概論

[Day 02] 工廠模式,策略模式,裝飾者模式

[Day 02] 工廠模式,策略模式,裝飾者模式

前言 - Rust?

前言 - Rust?


Comments