เจาะลึกเหตุการณ์ JavaScript

preventDefault และ stopPropagation: ควรใช้เมื่อใดและจะใช้ทำอะไรในแต่ละวิธี

สตีเฟน สเตอร์ชูร์
สตีเฟน สเตอร์ชูร์

Event.stopPropagation() และ Event.preventDefault()

การจัดการเหตุการณ์ JavaScript มักเป็นวิธีที่ตรงไปตรงมา โดยเฉพาะอย่างยิ่งเมื่อต้องจัดการกับโครงสร้าง HTML ที่เรียบง่าย (ค่อนข้างแบน) สิ่งต่างๆ จะมีส่วนร่วมมากขึ้นเมื่อเหตุการณ์ เดินทาง (หรือเผยแพร่) ผ่านลำดับชั้นขององค์ประกอบ กรณีนี้มักเกิดขึ้นเมื่อนักพัฒนาแอปเข้าถึง stopPropagation() และ/หรือ preventDefault() เพื่อแก้ปัญหาที่พบ หากคุณเคยคิดว่า "ฉันจะลองใช้ preventDefault() เท่านั้น และหากไม่ได้ผล ฉันจะลองใช้ stopPropagation() และถ้าไม่ได้ผล ฉันจะลองใช้ทั้ง 2 อย่าง" บทความนี้เหมาะสำหรับคุณ ฉันจะอธิบายให้ชัดเจนว่าแต่ละวิธีใช้ทำอะไร เมื่อใดควรใช้วิธีใด และมีตัวอย่างการทำงานที่หลากหลายให้คุณได้สำรวจ เป้าหมายของฉันคือการขจัดความสับสนของคุณให้ได้ทุกๆ วัน

ก่อนที่เราจะเจาะลึกลงไปจนเกินไป ข้อมูลสำคัญเกี่ยวกับการจัดการเหตุการณ์ 2 ประเภทที่ทำได้ใน JavaScript อย่างคร่าวๆ (ในเบราว์เซอร์สมัยใหม่ทั้งหมดคือ Internet Explorer ก่อนเวอร์ชัน 9 ไม่รองรับการบันทึกเหตุการณ์เลย)

สไตล์อีเวนติ้ง (จับภาพและฟองสบู่)

เบราว์เซอร์สมัยใหม่ทั้งหมดรองรับการบันทึกเหตุการณ์ แต่นักพัฒนาซอฟต์แวร์ไม่ค่อยนิยมใช้กัน เป็นที่น่าสนใจว่านี่เป็นเหตุการณ์รูปแบบเดียวที่ Netscape รองรับตั้งแต่แรก Microsoft Internet Explorer เป็นคู่แข่งรายใหญ่ที่สุดของ Netscape ไม่รองรับการบันทึกเหตุการณ์เลย แต่สนับสนุนเฉพาะกิจกรรมอีกรูปแบบหนึ่งที่เรียกว่าการแก้ไขเหตุการณ์ เมื่อสร้าง W3C แล้ว พบว่าเหมาะสมในเหตุการณ์ทั้ง 2 รูปแบบและประกาศว่าเบราว์เซอร์ควรรองรับทั้ง 2 รูปแบบ โดยใช้พารามิเตอร์ที่ 3 ของเมธอด addEventListener เริ่มแรก พารามิเตอร์ดังกล่าวเป็นเพียงบูลีน แต่เบราว์เซอร์ที่ทันสมัยทั้งหมดรองรับออบเจ็กต์ options เป็นพารามิเตอร์ที่ 3 ซึ่งคุณใช้เพื่อระบุได้ (และสิ่งอื่นๆ) หากต้องการใช้การบันทึกเหตุการณ์หรือไม่

someElement.addEventListener('click', myClickHandler, { capture: true | false });

โปรดทราบว่าออบเจ็กต์ options เป็นออบเจ็กต์ที่ไม่บังคับ เนื่องจากพร็อพเพอร์ตี้ capture ของออบเจ็กต์ดังกล่าว หากไม่ระบุ ค่าเริ่มต้นสำหรับ capture จะเป็น false ซึ่งหมายความว่าจะใช้การฟอกอากาศของเหตุการณ์

การบันทึกเหตุการณ์

หากเครื่องจัดการเหตุการณ์กำลัง "ฟังในระยะการจับภาพ" หมายความว่าอย่างไร เราต้องทราบว่าเหตุการณ์เกิดขึ้นได้อย่างไร และวิธีการเดินทางเพื่อทำความเข้าใจเรื่องนี้ เหตุการณ์ต่อไปนี้เป็นจริงเกี่ยวกับเหตุการณ์ทั้งหมด แม้ว่าคุณในฐานะนักพัฒนาแอปจะไม่ได้ใช้ประโยชน์จากเหตุการณ์ดังกล่าว ใส่ใจ หรือนึกถึงเรื่องนี้ก็ตาม

เหตุการณ์ทั้งหมดจะเริ่มต้นที่หน้าต่างและเกิดขึ้นในระยะการจับภาพก่อน ซึ่งหมายความว่าเมื่อมีการส่งเหตุการณ์ เหตุการณ์จะเริ่มต้นหน้าต่างและเคลื่อน "ลง" ไปยังองค์ประกอบเป้าหมายก่อน ปัญหานี้จะเกิดขึ้นแม้ว่าคุณจะฟังเฉพาะในช่วงที่มีเสียงฟองก็ตาม ลองดูตัวอย่างมาร์กอัปและ JavaScript ต่อไปนี้

<html>
  <body>
    <div id="A">
      <div id="B">
        <div id="C"></div>
      </div>
    </div>
  </body>
</html>
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('#C was clicked');
  },
  true,
);

เมื่อผู้ใช้คลิกองค์ประกอบ #C ระบบจะส่งเหตุการณ์ที่เกิดขึ้นที่ window จากนั้นเหตุการณ์นี้จะถ่ายทอดผ่านรายการระดับล่าง ดังนี้

window => document => <html> => <body> => ไปเรื่อยๆ จนกว่าจะถึงเป้าหมาย

ไม่ว่าจะมีการรอฟังเหตุการณ์คลิกที่องค์ประกอบ window หรือ document หรือ <html> หรือองค์ประกอบ <body> (หรือองค์ประกอบอื่นๆ ที่อยู่ระหว่างทางไปยังเป้าหมาย) ก็ตาม เหตุการณ์จะยังคงเกิดขึ้นที่ window และเริ่มเส้นทางตามที่ได้อธิบายไว้

ในตัวอย่างของเรา เหตุการณ์การคลิกจะเผยแพร่ (คำสำคัญเนื่องจากจะเชื่อมโยงกับวิธีการทำงานของเมธอด stopPropagation() โดยตรง และจะอธิบายต่อไปในเอกสารนี้) จาก window ไปยังองค์ประกอบเป้าหมาย (ในกรณีนี้คือ #C) ผ่านทุกองค์ประกอบระหว่าง window กับ #C

ซึ่งหมายความว่ากิจกรรมการคลิกจะเริ่มต้นเวลา window และเบราว์เซอร์จะถามคำถามต่อไปนี้

"มีอะไรที่ฟังเหตุการณ์การคลิกบน window ในระยะจับภาพไหม" หากเป็นเช่นนั้น เครื่องจัดการเหตุการณ์ที่เหมาะสมจะเริ่มทำงาน ในตัวอย่างของเราไม่มีอะไรที่เป็นอยู่แล้ว ดังนั้นจึงไม่มีเครื่องจัดการที่จะเริ่มทำงาน

ถัดไป เหตุการณ์จะเผยแพร่ไปยัง document และเบราว์เซอร์จะถามว่า "มีอะไรที่ฟังเหตุการณ์การคลิกใน document ในระยะจับภาพไหม" หากเป็นเช่นนั้น เครื่องจัดการเหตุการณ์ ที่เหมาะสมจะเริ่มทำงาน

ถัดไป เหตุการณ์จะเผยแพร่ไปยังองค์ประกอบ <html> และเบราว์เซอร์จะถามว่า "มีอะไรที่ฟังการคลิกองค์ประกอบ <html> ในระยะจับภาพไหม" หากเป็นเช่นนั้น เครื่องจัดการเหตุการณ์ ที่เหมาะสมจะเริ่มทํางาน

ถัดไป เหตุการณ์จะเผยแพร่ไปยังองค์ประกอบ <body> และเบราว์เซอร์จะถามว่า "มีอะไรฟังเหตุการณ์การคลิกในองค์ประกอบ <body> ในช่วงจับภาพไหม" หากเป็นเช่นนั้น เครื่องจัดการเหตุการณ์ที่เหมาะสมจะเริ่มทำงาน

ถัดไป เหตุการณ์จะเผยแพร่ไปยังองค์ประกอบ #A เบราว์เซอร์จะถามอีกครั้งว่า "มีอะไรได้ยินเหตุการณ์การคลิกใน #A อยู่ในระยะจับภาพหรือไม่ และหากใช่ ตัวแฮนเดิลเหตุการณ์ที่เหมาะสมจะเริ่มทำงาน

ถัดไป เหตุการณ์จะเผยแพร่ไปยังองค์ประกอบ #B (และระบบจะถามคำถามเดียวกัน)

สุดท้าย เหตุการณ์จะบรรลุเป้าหมายและเบราว์เซอร์จะถามว่า "มีอะไรที่ฟังเหตุการณ์การคลิกในองค์ประกอบ #C ในระยะจับภาพไหม" คำตอบในครั้งนี้คือ "ใช่" ระยะเวลาสั้นๆ นี้เมื่อกิจกรรมอยู่ที่เป้าหมายเรียกว่า "ระยะเป้าหมาย" ที่จุดนี้ เครื่องจัดการเหตุการณ์จะเริ่มทำงาน เบราว์เซอร์จะ Console.log "#C ถูกคลิก" เท่านี้ก็เสร็จเรียบร้อยแล้ว ถูกต้องไหม ผิด เรายังไม่หยุดแค่นั้น กระบวนการนี้จะดำเนินไปอย่างต่อเนื่อง แต่ตอนนี้เปลี่ยนไปเป็นช่วงของการฟอกอากาศแล้ว

ฟองสบู่ของกิจกรรม

เบราว์เซอร์จะถามข้อมูลต่อไปนี้

"มีการฟังเหตุการณ์การคลิกใน #C ในระยะที่เดือดพล่านไหม" โปรดให้ความสนใจอย่างใกล้ชิดที่นี่ เพื่อฟังการคลิก (หรือประเภทเหตุการณ์ใดก็ได้) โดยสมบูรณ์ทั้งในระยะบันทึกและการเกิดฟอง และหากคุณได้เดินสายเครื่องจัดการเหตุการณ์ในทั้ง 2 เฟส (เช่น โดยการเรียกใช้ .addEventListener() 2 ครั้ง โดยครั้งหนึ่งด้วย capture = true และอีกครั้งด้วย capture = false) ก็แสดงว่าตัวแฮนเดิลเหตุการณ์ทั้ง 2 เหตุการณ์จะเริ่มทำงานสำหรับองค์ประกอบเดียวกันอย่างแน่นอน แต่สิ่งสำคัญอีกอย่างที่ควรทราบก็คือไฟเหล่านี้จะเริ่มกระจายเป็นระยะต่างๆ (ระยะแรกอยู่ในระยะแคปเจอร์ และอีกระยะในช่วงการฟอกอากาศ)

ถัดไป เหตุการณ์จะเผยแพร่ (โดยทั่วไปจะใช้คำว่า "bubble" เพราะดูเหมือนว่าเหตุการณ์กำลัง "ขึ้น" แผนผัง DOM) ไปยังองค์ประกอบระดับบนสุด #B และเบราว์เซอร์จะถามว่า "มีการฟังเหตุการณ์การคลิกบน #B ในช่วงที่ทำให้เกิดฟองไหม" ในตัวอย่างของเราไม่มีอะไรเลย ไม่มีเครื่องจัดการที่จะเริ่มทำงาน

ถัดไป กิจกรรมจะแสดงเป็นบับเบิลไปยัง #A และเบราว์เซอร์จะถามว่า "มีอะไรที่ฟังเหตุการณ์การคลิกบน #A ในช่วงที่ฟองอากาศหรือไม่"

ถัดไป เหตุการณ์จะบับเบิลเป็น <body>: "มีอะไรที่ฟังเหตุการณ์การคลิกบนองค์ประกอบ <body> ในช่วงที่ฟองอากาศไหม"

ถัดไป องค์ประกอบ <html>: "มีอะไรที่คอยฟังเหตุการณ์การคลิกในองค์ประกอบ <html> ในช่วงที่ฟองอากาศไหม

ถัดมา document: "มีอะไรที่คอยฟังเหตุการณ์คลิกที่ document ในช่วงที่ฟองอากาศไหม"

สุดท้าย window: "มีอะไรที่คอยฟังเหตุการณ์การคลิกบนหน้าต่างในช่วงที่ฟองอากาศอยู่ไหม"

ในที่สุด นั่นเป็นการเดินทางที่ไกล กิจกรรมของเราอาจจะเหนื่อยมากแล้วในตอนนี้ แต่เชื่อไหมว่าเหตุการณ์ต่างๆ นั้นผ่านพ้นไปหมดแล้ว โดยส่วนใหญ่แล้ว สิ่งนี้จะไม่เคยสังเกตมาก่อนเนื่องจากนักพัฒนาซอฟต์แวร์มักสนใจเพียงเฟสของเหตุการณ์เดียวหรืออีกเฟสหนึ่ง (และมักจะเป็นช่วงที่เกิดฟองสบู่)

คุณควรลองสนุกไปกับการบันทึกเหตุการณ์ การฟองไฟเหตุการณ์ และการบันทึกโน้ตบางส่วนไปยังคอนโซลเมื่อเครื่องจัดการเริ่มทำงาน การได้เห็นเส้นทางที่เหตุการณ์ ใช้นั้นมีรายละเอียดมาก ต่อไปนี้คือตัวอย่างที่ฟังทุกองค์ประกอบในทั้ง 2 เฟส

<html>
  <body>
    <div id="A">
      <div id="B">
        <div id="C"></div>
      </div>
    </div>
  </body>
</html>
document.addEventListener(
  'click',
  function (e) {
    console.log('click on document in capturing phase');
  },
  true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('click on <html> in capturing phase');
  },
  true,
);
document.body.addEventListener(
  'click',
  function (e) {
    console.log('click on <body> in capturing phase');
  },
  true,
);
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('click on #A in capturing phase');
  },
  true,
);
document.getElementById('B').addEventListener(
  'click',
  function (e) {
    console.log('click on #B in capturing phase');
  },
  true,
);
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('click on #C in capturing phase');
  },
  true,
);

document.addEventListener(
  'click',
  function (e) {
    console.log('click on document in bubbling phase');
  },
  false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('click on <html> in bubbling phase');
  },
  false,
);
document.body.addEventListener(
  'click',
  function (e) {
    console.log('click on <body> in bubbling phase');
  },
  false,
);
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('click on #A in bubbling phase');
  },
  false,
);
document.getElementById('B').addEventListener(
  'click',
  function (e) {
    console.log('click on #B in bubbling phase');
  },
  false,
);
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('click on #C in bubbling phase');
  },
  false,
);

เอาต์พุตคอนโซลจะขึ้นอยู่กับองค์ประกอบที่คุณคลิก หากคลิกองค์ประกอบ "ลึกที่สุด" ในแผนผัง DOM (องค์ประกอบ #C) คุณจะเห็นตัวแฮนเดิลเหตุการณ์ทั้งหมดนี้เริ่มทำงาน ด้วยการจัดรูปแบบ CSS เล็กน้อยเพื่อให้เห็นได้ชัดเจนยิ่งขึ้นว่าองค์ประกอบใดเป็นองค์ประกอบใด ต่อไปนี้คือองค์ประกอบ #C ของเอาต์พุตคอนโซล (พร้อมภาพหน้าจอด้วย)

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"

คุณสามารถเล่นเกมนี้แบบอินเทอร์แอกทีฟได้ในการสาธิตสดด้านล่าง คลิกองค์ประกอบ #C และสังเกตเอาต์พุตของคอนโซล

event.stopPropagation()

เมื่อเราเข้าใจเกี่ยวกับแหล่งที่มาของเหตุการณ์และวิธีเดินทาง (เช่น การเผยแพร่) ผ่าน DOM ทั้งในขั้นตอนการจับภาพและระยะการฟองสบู่ เราจึงหันไปสนใจ event.stopPropagation() แทน

เรียกใช้เมธอด stopPropagation() ในเหตุการณ์ DOM แบบเนทีฟ (ส่วนใหญ่) ได้ ผมพูดว่า "ส่วนใหญ่" เพราะมี 2-3 รายการที่การเรียกใช้วิธีนี้ไม่ได้ทำอะไร (เพราะเหตุการณ์ไม่ได้เริ่มต้นจากเหตุการณ์) กิจกรรมอย่าง focus, blur, load, scroll และอีก 2-3 อย่างจัดอยู่ในหมวดหมู่นี้ คุณเรียกใช้ stopPropagation() ได้แต่จะไม่มีอะไรที่น่าสนใจเกิดขึ้น เนื่องจากเหตุการณ์เหล่านี้ไม่เผยแพร่

แต่ stopPropagation มีหน้าที่อะไร

ก็เหมือนกับที่คำโฆษณาบอกไว้อยู่แล้ว เมื่อคุณเรียกเหตุการณ์ดังกล่าว เหตุการณ์จะหยุดเผยแพร่ไปยังองค์ประกอบใดๆ ที่จะเดินทางไปยังองค์ประกอบนั้น นี่เป็นความจริงสำหรับทั้ง 2 อย่าง (การจับภาพและการกะพริบ) ดังนั้น หากคุณเรียกใช้ stopPropagation() ที่ใดก็ได้ในช่วงการจับภาพ เหตุการณ์จะไม่ไปถึงระยะเป้าหมายหรือระยะลอยตัว ถ้าคุณเรียกสิ่งนี้ในช่วงที่ฟองอากาศ มันจะเข้าสู่ขั้นตอนการจับภาพไปแล้ว แต่จะยุติการ "เด้งดึ๋ง" ขึ้นมาจากจุดที่คุณเรียก

กลับไปที่ตัวอย่างมาร์กอัปเดิม คุณคิดว่าจะเกิดอะไรขึ้นหากเราเรียกใช้ stopPropagation() ในระยะจับภาพที่องค์ประกอบ #B

ผลลัพธ์ที่ได้มีดังนี้

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"

คุณสามารถเล่นเกมนี้แบบอินเทอร์แอกทีฟได้ในการสาธิตสดด้านล่าง คลิกองค์ประกอบ #C ในการสาธิตสดและสังเกตเอาต์พุตของคอนโซล

ลองหยุดการนำไปใช้งานที่ #A ในช่วงที่เกิดการฟอกอากาศไหม ซึ่งจะส่งผลให้เกิดเอาต์พุตต่อไปนี้

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"

คุณสามารถเล่นเกมนี้แบบอินเทอร์แอกทีฟได้ในการสาธิตสดด้านล่าง คลิกองค์ประกอบ #C ในการสาธิตสดและสังเกตเอาต์พุตของคอนโซล

ขออีกเรื่องหนึ่งไว้ใช้สนุกนะ จะเกิดอะไรขึ้นหากเราเรียกใช้ stopPropagation() ในระยะเป้าหมายสำหรับ #C อย่าลืมว่า "ระยะเป้าหมาย" คือชื่อที่กำหนดให้กับช่วงเวลาที่เหตุการณ์เกิดขึ้นที่เป้าหมาย ผลลัพธ์ที่ได้มีดังนี้

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"

โปรดทราบว่าเครื่องจัดการเหตุการณ์สำหรับ #C ที่เราบันทึกการ "คลิก #C ในขั้นตอนการบันทึก" ยังดำเนินการอยู่ แต่ตัวแฮนเดิลที่เราบันทึก "คลิก #C ในขั้นตอนการสร้างปัญหา" ไม่ดำเนินการ วิธีนี้น่าจะเหมาะสมที่สุด เราเรียก stopPropagation() จากไปยังต้นทาง ดังนั้นเหตุการณ์ดังกล่าวจึงจะยุติการเผยแพร่

คุณสามารถเล่นเกมนี้แบบอินเทอร์แอกทีฟได้ในการสาธิตสดด้านล่าง คลิกองค์ประกอบ #C ในการสาธิตสดและสังเกตเอาต์พุตของคอนโซล

ในการสาธิตสดเหล่านี้ เราขอแนะนำให้คุณลองเล่นดู ลองคลิกที่องค์ประกอบ #A เท่านั้น หรือองค์ประกอบ body เท่านั้น ลองคาดการณ์สิ่งที่จะเกิดขึ้น จากนั้นสังเกตการณ์ว่าคุณถูกต้องหรือไม่ เมื่อถึงจุดนี้คุณก็น่าจะคาดการณ์ได้ค่อนข้างแม่นยำแล้ว

event.stopImmediatePropagation()

วิธีที่แปลกและไม่ได้ใช้บ่อยนี้คืออะไร วิธีนี้คล้ายกับ stopPropagation แต่แทนที่จะหยุดเหตุการณ์ไม่ให้เดินทางไปยังองค์ประกอบที่สืบทอดมา (การบันทึก) หรือระดับบน (การฟอกอากาศ) วิธีนี้จะใช้ได้ก็ต่อเมื่อมีตัวแฮนเดิลเหตุการณ์มากกว่า 1 รายการอยู่ในองค์ประกอบเดียว เนื่องจาก addEventListener() รองรับเหตุการณ์มัลติแคสต์ จึงสามารถต่อสายตัวแฮนเดิลเหตุการณ์ไปยังองค์ประกอบเดียวได้มากกว่า 1 ครั้ง เมื่อเกิดกรณีนี้ขึ้น (ในเบราว์เซอร์ส่วนใหญ่) เครื่องจัดการเหตุการณ์จะทำงานตามลำดับที่มีการเดินสาย การเรียกใช้ stopImmediatePropagation() จะป้องกันไม่ให้ตัวแฮนเดิลที่ตามมาเริ่มทำงาน ลองพิจารณาตัวอย่างต่อไปนี้

<html>
  <body>
    <div id="A">I am the #A element</div>
  </body>
</html>
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I shall run first!');
  },
  false,
);

document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I shall run second!');
    e.stopImmediatePropagation();
  },
  false,
);

document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
  },
  false,
);

ตัวอย่างข้างต้นจะแสดงเอาต์พุตของคอนโซลดังต่อไปนี้

"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"

โปรดทราบว่าเครื่องจัดการเหตุการณ์ที่ 3 จะไม่ทำงานเนื่องจากเครื่องจัดการเหตุการณ์ที่ 2 จะเรียกใช้ e.stopImmediatePropagation() แต่หากเราเรียก e.stopPropagation() แทน เครื่องจัดการที่ 3 จะยังคงทำงานอยู่

event.preventDefault()

หาก stopPropagation() ป้องกันไม่ให้กิจกรรมเลื่อนเป็น "ลง" (จับภาพ) หรือ "ขึ้น" (กะพริบ) preventDefault() จะทำอะไร ดูเหมือนแอปจะทำอะไรคล้ายๆ กันอยู่นะ ใช่ไหม

ไม่ครับ แม้ว่าทั้งคู่จะมีความสับสนอยู่บ้าง แต่จริงๆ แล้วพวกเขาไม่ได้เกี่ยวข้องกันมากนัก เมื่อเห็น preventDefault() ให้เพิ่มคำว่า "การดำเนินการ" ในหัวของคุณ ให้คิดว่า "ป้องกันการดำเนินการเริ่มต้น"

และการดำเนินการเริ่มต้นที่คุณอาจถามคืออะไร ขออภัย คำตอบไม่ค่อยชัดเจนนัก เนื่องจากขึ้นอยู่กับองค์ประกอบของ + เหตุการณ์ที่เป็นประเด็นเป็นหลัก และเพื่อทำให้สถานการณ์สับสน บางครั้งไม่มีการดำเนินการเริ่มต้นเลย

เรามาเริ่มกันด้วยตัวอย่างง่ายๆ ที่เข้าใจได้ คุณคาดหวังว่าจะเกิดอะไรขึ้น เมื่อคลิกลิงก์บนหน้าเว็บ แน่นอนว่าคุณคาดหวังให้เบราว์เซอร์ไปยัง URL ที่ระบุโดยลิงก์นั้น ในกรณีนี้ องค์ประกอบจะเป็นแท็ก Anchor และเหตุการณ์คือเหตุการณ์การคลิก ชุดค่าผสมดังกล่าว (<a> + click) มี "การดำเนินการเริ่มต้น" ที่เป็นการไปยัง href ของลิงก์ หากคุณต้องการป้องกันให้เบราว์เซอร์ทำงานเริ่มต้นนั้น ควรทำอย่างไร กล่าวคือ สมมติว่าคุณต้องการป้องกันไม่ให้เบราว์เซอร์ไปยัง URL ที่ระบุโดยแอตทริบิวต์ href ขององค์ประกอบ <a> นี่คือสิ่งที่ preventDefault() จะทำให้คุณ ลองดูตัวอย่างนี้

<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
  'click',
  function (e) {
    e.preventDefault();
    console.log('Maybe we should just play some of their music right here instead?');
  },
  false,
);

คุณสามารถเล่นเกมนี้แบบอินเทอร์แอกทีฟได้ในการสาธิตสดด้านล่าง คลิกลิงก์ The Avett Brothers และดูเอาต์พุตของคอนโซล (และข้อเท็จจริงที่ว่าคุณไม่เปลี่ยนเส้นทางไปยังเว็บไซต์ Avett Brothers)

โดยปกติแล้ว การคลิกลิงก์ที่มีข้อความว่า The Avett Brothers จะเรียกดู www.theavettbrothers.com อย่างไรก็ตาม ในกรณีนี้ เราได้ใช้เครื่องจัดการเหตุการณ์การคลิกกับองค์ประกอบ <a> และระบุว่าควรป้องกันการดำเนินการเริ่มต้น ดังนั้น เมื่อผู้ใช้คลิกลิงก์นี้ ระบบก็จะไม่พาผู้ใช้ไปที่ใด และคอนโซลจะบันทึกเพียงว่า "บางทีเราน่าจะเปิดเพลงของพวกเขาที่นี่แทน"

ชุดค่าผสมองค์ประกอบ/เหตุการณ์ใดอีกบ้างที่ทำให้คุณป้องกันการดำเนินการเริ่มต้นได้ ผมไม่สามารถแสดงรายการทั้งหมด และบางครั้งคุณต้องทดลองเพื่อดู แต่นี่คือตัวอย่างสั้นๆ

  • องค์ประกอบ <form> + เหตุการณ์ "submit": preventDefault() สำหรับชุดค่าผสมนี้จะป้องกันไม่ให้ส่งแบบฟอร์ม ซึ่งจะเป็นประโยชน์หากคุณต้องการทำการตรวจสอบและหากเกิดข้อผิดพลาด คุณสามารถเรียกใช้ preventDefault แบบมีเงื่อนไขเพื่อหยุดการส่งแบบฟอร์ม

  • องค์ประกอบ <a> + เหตุการณ์ "คลิก": preventDefault() สำหรับชุดค่าผสมนี้ป้องกันไม่ให้เบราว์เซอร์ไปยัง URL ที่ระบุในแอตทริบิวต์ href ขององค์ประกอบ <a>

  • document + เหตุการณ์ "mouseWheel": preventDefault() สำหรับชุดค่าผสมนี้ป้องกันไม่ให้เลื่อนหน้าเว็บด้วยเมาส์ (แต่การเลื่อนด้วยแป้นพิมพ์จะยังคงทำงานได้)
    ↜ การดำเนินการนี้ต้องโทรหา addEventListener() ด้วย { passive: false }

  • document + เหตุการณ์ "คีย์ดาวน์": preventDefault() สำหรับชุดค่าผสมนี้เป็นอันตรายถึงชีวิต เนื่องจากจะทำให้หน้าแทบไม่มีประโยชน์ ทำให้ไม่สามารถเลื่อนแป้นพิมพ์ แท็บ และการไฮไลต์แป้นพิมพ์

  • document + เหตุการณ์ "mousedown": preventDefault() สำหรับชุดค่าผสมนี้จะป้องกันการไฮไลต์ข้อความด้วยเมาส์และการทำงาน "เริ่มต้น" อื่นๆ ที่ระบบจะเรียกใช้ด้วยเมาส์ลง

  • องค์ประกอบ <input> + เหตุการณ์ "keypress": preventDefault() สำหรับชุดค่าผสมนี้จะป้องกันไม่ให้อักขระที่ผู้ใช้พิมพ์เข้าถึงองค์ประกอบอินพุต (แต่อย่าทำวิธีนี้ บางกรณีมีเหตุผลที่ถูกต้องอยู่แล้ว)

  • document + เหตุการณ์ "เมนูตามบริบท": preventDefault() สำหรับชุดค่าผสมนี้ป้องกันไม่ให้เมนูตามบริบทของเบราว์เซอร์เนทีฟปรากฏขึ้นเมื่อผู้ใช้คลิกขวาหรือกดค้าง (หรือในลักษณะอื่นใดที่เมนูตามบริบทอาจปรากฏ)

รายการนี้เป็นเพียงตัวอย่างบางส่วนเท่านั้น แต่ก็หวังว่าจะช่วยให้คุณได้แนวคิดนำ preventDefault() ไปใช้

มุกตลกที่ใช้ได้จริงหรือ

จะเกิดอะไรขึ้นหากคุณstopPropagation() และ preventDefault() ในระยะจับภาพ โดยเริ่มต้นที่เอกสาร ความฮากำลังเกิดขึ้น! ข้อมูลโค้ดต่อไปนี้จะทำให้หน้าเว็บ ไม่มีประโยชน์เลย

function preventEverything(e) {
  e.preventDefault();
  e.stopPropagation();
  e.stopImmediatePropagation();
}

document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });

ฉันไม่รู้จริงๆ ว่าทำไมคุณถึงอยากทำแบบนั้น (ยกเว้นฉันเล่นมุกตลกกับใครสักคนนะ) แต่การนึกถึงสิ่งที่เกิดขึ้นที่นี่และรู้ว่าอะไรทำให้สถานการณ์แบบนี้เกิดขึ้น

เหตุการณ์ทั้งหมดเกิดขึ้นที่ window ดังนั้นในข้อมูลโค้ดนี้ เราจึงจะหยุดการทำงานในแทร็กทั้งหมด เหตุการณ์ click, keydown, mousedown, contextmenu และ mousewheel ทั้งหมดนับตั้งแต่ที่ไปจนถึงองค์ประกอบใดๆ ที่อาจรับฟังอยู่ นอกจากนี้เรายังเรียก stopImmediatePropagation เพื่อป้องกันไม่ให้เครื่องจัดการที่ต่อเข้ากับเอกสารหลังจากเอกสารนี้ถูกป้องกันด้วย

โปรดทราบว่า stopPropagation() และ stopImmediatePropagation() ไม่ใช่ (อย่างน้อยก็ไม่ใช่ส่วนใหญ่) สิ่งที่แสดงผลหน้าเว็บซึ่งไร้ประโยชน์ เพื่อป้องกันไม่ให้กิจกรรมไปถึงจุดที่ควรจะเป็น

แต่เราเรียกว่า preventDefault() ด้วยเช่นกัน ซึ่งคุณจะจำไม่ได้ว่าใช้ การดำเนินการ เริ่มต้น ดังนั้นการดำเนินการเริ่มต้นทั้งหมด (เช่น การเลื่อนลูกกลิ้งเมาส์ การเลื่อนหรือไฮไลต์ด้วยแป้นพิมพ์ หรือการกด Tab การคลิกลิงก์ การแสดงเมนูบริบท ฯลฯ) จึงจะได้รับการป้องกันไม่ให้ทั้งหมด ซึ่งจะทำให้ออกจากหน้าอยู่ในสถานะที่ไม่มีประโยชน์พอสมควร

การสาธิตแบบสด

หากต้องการสำรวจตัวอย่างทั้งหมดจากบทความนี้อีกครั้งในที่เดียว ให้ดูการสาธิตแบบฝังที่ด้านล่าง

ข้อความแสดงการยอมรับ

รูปภาพหลักของ Tom Wilson ใน Unsplash