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

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

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

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

รูปแบบเหตุการณ์ (การบันทึกและการรวม)

เบราว์เซอร์สมัยใหม่ทั้งหมดรองรับการบันทึกเหตุการณ์ แต่นักพัฒนาแอปไม่ค่อยได้ใช้ ที่น่าสนใจคือรูปแบบนี้คือรูปแบบเดียวที่ Netscape รองรับในตอนแรก คู่แข่งรายใหญ่ที่สุดของ Netscape อย่าง Microsoft Internet Explorer นั้นไม่รองรับการบันทึกเหตุการณ์เลย แต่รองรับเหตุการณ์อีกรูปแบบหนึ่งที่เรียกว่าการทําให้เหตุการณ์ปรากฏ เมื่อก่อตั้ง 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) คุณจะเห็นตัวจัดการเหตุการณ์เหล่านี้ทั้งหมดทำงาน องค์ประกอบ #C ของคอนโซลเอาต์พุต (พร้อมภาพหน้าจอด้วย) มีการจัดรูปแบบ CSS เล็กน้อยเพื่อให้เห็นองค์ประกอบต่างๆ ชัดเจนขึ้น

"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 ทำอะไรได้บ้าง

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

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

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

ขอขอบคุณ

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