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

preventDefault และ stopPropagation: กรณีที่ควรใช้แต่ละวิธีและแต่ละวิธีทํางานอย่างไร

การจัดการเหตุการณ์ 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 ครั้ง โดย 1 ครั้งใช้ capture = true และอีก 1 ครั้งใช้ capture = false) ตัวแฮนเดิลเหตุการณ์ทั้ง 2 รายการจะทํางานสําหรับองค์ประกอบเดียวกันอย่างแน่นอน แต่สิ่งสำคัญที่ต้องทราบด้วยว่า กล้องจะเริ่มทำงานเป็นระยะที่ต่างกัน (ระยะหนึ่งในระยะจับภาพและอีกระยะหนึ่งในช่วงฟองสบู่)

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

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

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

ถัดไปคือองค์ประกอบ <html>: "มีสิ่งใดกำลังรอเหตุการณ์คลิกในองค์ประกอบ <html> ในระยะการทํางานแบบบับเบิลหรือไม่

ถัดไปคือ document: "มีสิ่งใดกำลังรอเหตุการณ์คลิกใน document ในระยะการทํางานแบบ Bubbling อยู่ไหม"

สุดท้ายคือ window: "มีสิ่งใดกำลังรอเหตุการณ์คลิกในหน้าต่างในระยะการทํางานแบบ Bubbling ไหม"

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

คุณควรลองเล่นกับการบันทึกเหตุการณ์และการบับเบิ้ลเหตุการณ์ รวมถึงบันทึกโน้ตบางอย่างลงในคอนโซลเมื่อตัวแฮนเดิลเริ่มทำงาน การดูเส้นทางที่เหตุการณ์เกิดขึ้นจะมีประโยชน์อย่างยิ่ง ต่อไปนี้เป็นตัวอย่างที่คอยฟังองค์ประกอบทุกรายการในทั้ง 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 เดิม (ส่วนใหญ่) เราใช้คำว่า "ส่วนใหญ่" เนื่องจากมีบางรายการที่การเรียกใช้เมธอดนี้จะไม่ทําอะไรเลย (เนื่องจากเหตุการณ์ไม่แผ่กระจายตั้งแต่แรก) เหตุการณ์ เช่น 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() จะทําอย่างไร ดูเหมือนว่าจะใช้เพื่อวัตถุประสงค์ที่คล้ายกัน ใช่หรือไม่

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

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

เรามาเริ่มด้วยตัวอย่างง่ายๆ ที่จะทำความเข้าใจ คุณคาดหวังให้สิ่งใดเกิดขึ้นเมื่อคลิกลิงก์ในหน้าเว็บ แน่นอนว่าคุณคาดหวังว่าเบราว์เซอร์จะไปยัง URL ที่ระบุโดยลิงก์นั้น ในกรณีนี้ องค์ประกอบคือแท็กแอตทริบิวต์ "a" และเหตุการณ์คือเหตุการณ์การคลิก ชุดค่าผสมดังกล่าว (<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 แล้วดูเอาต์พุตคอนโซล (และข้อเท็จจริงที่ว่าระบบไม่ได้เปลี่ยนเส้นทางคุณไปยังเว็บไซต์ของ The 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 + เหตุการณ์ "keydown": preventDefault() สําหรับชุดค่าผสมนี้ถือว่าอันตราย ซึ่งทำให้หน้าเว็บไร้ประโยชน์โดยส่วนใหญ่ ป้องกันไม่ให้ใช้แป้นพิมพ์เลื่อน การกด Tab และการไฮไลต์แป้นพิมพ์

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

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

  • document + เหตุการณ์ "contextmenu": 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 ดังนั้นในสnippet นี้ เราจะหยุดเหตุการณ์ click, keydown, mousedown, contextmenu และ mousewheel ทั้งหมดไม่ให้เข้าถึงองค์ประกอบที่อาจรอรับเหตุการณ์เหล่านั้น นอกจากนี้เรายังเรียกใช้ stopImmediatePropagation เพื่อให้แฮนเดิลที่ต่อสายไปยังเอกสารหลังจากหน้านี้ถูกขัดขวางเช่นกัน

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

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

การสาธิตการใช้งานแบบสด

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

ขอขอบคุณ

รูปภาพหลักโดย Tom Wilson ใน Unsplash