You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/th/3-terrarium/3-intro-to-DOM-and-closures
Lee Stott 2daab5271b
Update Quiz Link
3 weeks ago
..
README.md Update Quiz Link 3 weeks ago
assignment.md 🌐 Update translations via Co-op Translator 4 weeks ago

README.md

โครงการ Terrarium ตอนที่ 3: การจัดการ DOM และ Closure

DOM และ Closure

ภาพสเก็ตโน้ตโดย Tomomi Imura

แบบทดสอบก่อนเรียน

แบบทดสอบก่อนเรียน

บทนำ

การจัดการ DOM หรือ "Document Object Model" เป็นส่วนสำคัญของการพัฒนาเว็บ ตามที่ MDN กล่าวไว้ว่า "Document Object Model (DOM) คือการแสดงข้อมูลของวัตถุที่ประกอบขึ้นเป็นโครงสร้างและเนื้อหาของเอกสารบนเว็บ" ความท้าทายในการจัดการ DOM บนเว็บมักเป็นแรงผลักดันให้ใช้เฟรมเวิร์ก JavaScript แทนการใช้ JavaScript แบบดั้งเดิมเพื่อจัดการ DOM แต่ในบทเรียนนี้เราจะจัดการด้วยตัวเอง!

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

JavaScript closures เป็นหัวข้อที่กว้างและซับซ้อน บทเรียนนี้จะกล่าวถึงแนวคิดพื้นฐานที่สุดที่ในโค้ดของ terrarium คุณจะพบ closure: ฟังก์ชันภายในและฟังก์ชันภายนอกที่ถูกสร้างขึ้นในลักษณะที่อนุญาตให้ฟังก์ชันภายในเข้าถึงขอบเขตของฟังก์ชันภายนอก สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทำงานนี้ โปรดเยี่ยมชม เอกสารที่ครอบคลุม

เราจะใช้ closure เพื่อจัดการ DOM

ลองนึกถึง DOM ว่าเป็นต้นไม้ที่แสดงถึงวิธีการทั้งหมดที่เอกสารเว็บเพจสามารถถูกจัดการได้ มีการเขียน API (Application Program Interfaces) ต่าง ๆ เพื่อให้นักพัฒนาโปรแกรมสามารถเข้าถึง DOM และแก้ไข เปลี่ยนแปลง จัดเรียงใหม่ และจัดการได้ตามต้องการ

การแสดง DOM tree

การแสดง DOM และ HTML markup ที่อ้างถึงมัน จาก Olfa Nasraoui

ในบทเรียนนี้ เราจะทำโครงการ terrarium แบบโต้ตอบให้เสร็จสมบูรณ์โดยสร้าง JavaScript ที่จะช่วยให้ผู้ใช้สามารถจัดการพืชบนหน้าเว็บได้

ความต้องการเบื้องต้น

คุณควรมี HTML และ CSS สำหรับ terrarium ของคุณสร้างไว้แล้ว เมื่อจบบทเรียนนี้คุณจะสามารถย้ายพืชเข้าและออกจาก terrarium โดยการลากและวางได้

งาน

ในโฟลเดอร์ terrarium ของคุณ สร้างไฟล์ใหม่ชื่อ script.js และนำเข้าไฟล์นั้นในส่วน <head>:

	<script src="./script.js" defer></script>

หมายเหตุ: ใช้ defer เมื่อนำเข้าไฟล์ JavaScript ภายนอกในไฟล์ html เพื่อให้ JavaScript ทำงานหลังจากไฟล์ HTML ถูกโหลดเสร็จสมบูรณ์ คุณสามารถใช้ attribute async ได้เช่นกัน ซึ่งอนุญาตให้สคริปต์ทำงานในขณะที่ไฟล์ HTML กำลังถูกแปลง แต่ในกรณีของเรา สิ่งสำคัญคือต้องให้ element HTML พร้อมใช้งานสำหรับการลากก่อนที่เราจะอนุญาตให้สคริปต์ลากทำงาน


องค์ประกอบ DOM

สิ่งแรกที่คุณต้องทำคือสร้างการอ้างอิงไปยังองค์ประกอบที่คุณต้องการจัดการใน DOM ในกรณีของเรา คือพืช 14 ชนิดที่รออยู่ในแถบด้านข้าง

งาน

dragElement(document.getElementById('plant1'));
dragElement(document.getElementById('plant2'));
dragElement(document.getElementById('plant3'));
dragElement(document.getElementById('plant4'));
dragElement(document.getElementById('plant5'));
dragElement(document.getElementById('plant6'));
dragElement(document.getElementById('plant7'));
dragElement(document.getElementById('plant8'));
dragElement(document.getElementById('plant9'));
dragElement(document.getElementById('plant10'));
dragElement(document.getElementById('plant11'));
dragElement(document.getElementById('plant12'));
dragElement(document.getElementById('plant13'));
dragElement(document.getElementById('plant14'));

เกิดอะไรขึ้นที่นี่? คุณกำลังอ้างอิงเอกสารและค้นหาใน DOM เพื่อหาองค์ประกอบที่มี Id เฉพาะ จำได้ไหมว่าในบทเรียนแรกเกี่ยวกับ HTML คุณได้ให้ Id เฉพาะกับแต่ละภาพพืช (id="plant1")? ตอนนี้คุณจะใช้ประโยชน์จากความพยายามนั้น หลังจากระบุองค์ประกอบแต่ละรายการ คุณส่งรายการนั้นไปยังฟังก์ชันที่เรียกว่า dragElement ซึ่งคุณจะสร้างในอีกสักครู่ ดังนั้นองค์ประกอบใน HTML จะสามารถลากได้ หรือจะสามารถลากได้ในไม่ช้า

ทำไมเราถึงอ้างอิงองค์ประกอบโดย Id? ทำไมไม่ใช้ CSS class? คุณอาจอ้างอิงบทเรียนก่อนหน้านี้เกี่ยวกับ CSS เพื่อหาคำตอบสำหรับคำถามนี้


Closure

ตอนนี้คุณพร้อมที่จะสร้าง closure dragElement ซึ่งเป็นฟังก์ชันภายนอกที่ล้อมรอบฟังก์ชันภายในหนึ่งหรือหลายฟังก์ชัน (ในกรณีของเรา เราจะมีสามฟังก์ชัน)

Closure มีประโยชน์เมื่อฟังก์ชันหนึ่งหรือหลายฟังก์ชันต้องการเข้าถึงขอบเขตของฟังก์ชันภายนอก นี่คือตัวอย่าง:

function displayCandy(){
	let candy = ['jellybeans'];
	function addCandy(candyType) {
		candy.push(candyType)
	}
	addCandy('gumdrops');
}
displayCandy();
console.log(candy)

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

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

งาน

ภายใต้การประกาศองค์ประกอบใน script.js สร้างฟังก์ชัน:

function dragElement(terrariumElement) {
	//set 4 positions for positioning on the screen
	let pos1 = 0,
		pos2 = 0,
		pos3 = 0,
		pos4 = 0;
	terrariumElement.onpointerdown = pointerDrag;
}

dragElement ได้รับวัตถุ terrariumElement จากการประกาศที่ด้านบนของสคริปต์ จากนั้นคุณตั้งค่าตำแหน่งท้องถิ่นที่ 0 สำหรับวัตถุที่ส่งผ่านไปยังฟังก์ชัน นี่คือตัวแปรท้องถิ่นที่จะถูกจัดการสำหรับแต่ละองค์ประกอบเมื่อคุณเพิ่มฟังก์ชันลากและวางภายใน closure ให้กับแต่ละองค์ประกอบ terrarium จะถูกเติมเต็มด้วยองค์ประกอบที่ถูกลากเหล่านี้ ดังนั้นแอปพลิเคชันจำเป็นต้องติดตามตำแหน่งที่วางไว้

นอกจากนี้ terrariumElement ที่ถูกส่งไปยังฟังก์ชันนี้จะถูกกำหนดเหตุการณ์ pointerdown ซึ่งเป็นส่วนหนึ่งของ web APIs ที่ออกแบบมาเพื่อช่วยในการจัดการ DOM onpointerdown จะทำงานเมื่อปุ่มถูกกด หรือในกรณีของเรา เมื่อองค์ประกอบที่สามารถลากได้ถูกสัมผัส เหตุการณ์นี้ทำงานได้ทั้ง เว็บและเบราว์เซอร์มือถือ โดยมีข้อยกเว้นบางประการ

event handler onclick มีการสนับสนุนข้ามเบราว์เซอร์มากกว่า; ทำไมคุณถึงไม่ใช้มันที่นี่? ลองคิดถึงประเภทของการโต้ตอบหน้าจอที่คุณพยายามสร้างที่นี่


ฟังก์ชัน Pointerdrag

terrariumElement พร้อมที่จะถูกลากไปรอบ ๆ เมื่อเหตุการณ์ onpointerdown ถูกเรียกใช้ ฟังก์ชัน pointerDrag จะถูกเรียกใช้ เพิ่มฟังก์ชันนั้นใต้บรรทัดนี้: terrariumElement.onpointerdown = pointerDrag;:

งาน

function pointerDrag(e) {
	e.preventDefault();
	console.log(e);
	pos3 = e.clientX;
	pos4 = e.clientY;
}

มีหลายสิ่งเกิดขึ้นที่นี่ อย่างแรก คุณป้องกันเหตุการณ์เริ่มต้นที่ปกติจะเกิดขึ้นเมื่อ pointerdown โดยใช้ e.preventDefault(); ด้วยวิธีนี้คุณจะมีการควบคุมพฤติกรรมของอินเทอร์เฟซมากขึ้น

กลับมาที่บรรทัดนี้เมื่อคุณสร้างไฟล์สคริปต์เสร็จสมบูรณ์และลองทำโดยไม่มี e.preventDefault() - จะเกิดอะไรขึ้น?

ถัดไป เปิด index.html ในหน้าต่างเบราว์เซอร์ และตรวจสอบอินเทอร์เฟซ เมื่อคุณคลิกที่พืช คุณจะเห็นว่าเหตุการณ์ 'e' ถูกจับ ลองเจาะลึกเข้าไปในเหตุการณ์เพื่อดูว่ามีข้อมูลมากแค่ไหนที่ถูกรวบรวมโดยเหตุการณ์ pointerdown หนึ่งครั้ง!

จากนั้น สังเกตว่าตัวแปรท้องถิ่น pos3 และ pos4 ถูกตั้งค่าเป็น e.clientX คุณสามารถค้นหาค่าของ e ในหน้าต่างการตรวจสอบ ค่าพวกนี้จับพิกัด x และ y ของพืชในขณะที่คุณคลิกหรือสัมผัสมัน คุณจะต้องการการควบคุมที่ละเอียดอ่อนต่อพฤติกรรมของพืชเมื่อคุณคลิกและลากมัน ดังนั้นคุณจึงติดตามพิกัดของมัน

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

ทำฟังก์ชันเริ่มต้นให้เสร็จโดยเพิ่มการจัดการเหตุการณ์ pointer อีกสองรายการใต้ pos4 = e.clientY:

document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;

ตอนนี้คุณกำลังระบุว่าคุณต้องการให้พืชถูกลากไปพร้อมกับ pointer เมื่อคุณเคลื่อนมัน และให้การลากหยุดเมื่อคุณยกเลิกการเลือกพืช onpointermove และ onpointerup เป็นส่วนหนึ่งของ API เดียวกันกับ onpointerdown อินเทอร์เฟซจะโยนข้อผิดพลาดในตอนนี้เนื่องจากคุณยังไม่ได้กำหนดฟังก์ชัน elementDrag และ stopElementDrag ดังนั้นสร้างฟังก์ชันเหล่านั้นต่อไป

ฟังก์ชัน elementDrag และ stopElementDrag

คุณจะทำ closure ของคุณให้เสร็จโดยเพิ่มฟังก์ชันภายในอีกสองฟังก์ชันที่จะจัดการสิ่งที่เกิดขึ้นเมื่อคุณลากพืชและหยุดลากมัน พฤติกรรมที่คุณต้องการคือคุณสามารถลากพืชใด ๆ ได้ทุกเมื่อและวางมันไว้ที่ใดก็ได้บนหน้าจอ อินเทอร์เฟซนี้ค่อนข้างไม่จำกัด (ไม่มี drop zone ตัวอย่างเช่น) เพื่อให้คุณสามารถออกแบบ terrarium ของคุณได้ตามที่คุณต้องการโดยการเพิ่ม ลบ และจัดตำแหน่งพืชใหม่

งาน

เพิ่มฟังก์ชัน elementDrag หลังจากปิดวงเล็บปีกกาของ pointerDrag:

function elementDrag(e) {
	pos1 = pos3 - e.clientX;
	pos2 = pos4 - e.clientY;
	pos3 = e.clientX;
	pos4 = e.clientY;
	console.log(pos1, pos2, pos3, pos4);
	terrariumElement.style.top = terrariumElement.offsetTop - pos2 + 'px';
	terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
}

ในฟังก์ชันนี้ คุณทำการแก้ไขตำแหน่งเริ่มต้น 1-4 ที่คุณตั้งค่าเป็นตัวแปรท้องถิ่นในฟังก์ชันภายนอก เกิดอะไรขึ้นที่นี่?

เมื่อคุณลาก คุณกำหนดค่าใหม่ให้กับ pos1 โดยทำให้มันเท่ากับ pos3 (ซึ่งคุณตั้งค่าไว้ก่อนหน้านี้เป็น e.clientX) ลบด้วยค่าปัจจุบันของ e.clientX คุณทำการดำเนินการที่คล้ายกันกับ pos2 จากนั้นคุณตั้งค่าใหม่ให้กับ pos3 และ pos4 เป็นพิกัด X และ Y ใหม่ขององค์ประกอบ คุณสามารถดูการเปลี่ยนแปลงเหล่านี้ในคอนโซลขณะที่คุณลาก จากนั้นคุณจัดการสไตล์ css ของพืชเพื่อกำหนดตำแหน่งใหม่ของมันตามตำแหน่งใหม่ของ pos1 และ pos2 โดยคำนวณพิกัด X และ Y ด้านบนและด้านซ้ายของพืชโดยเปรียบเทียบ offset กับตำแหน่งใหม่เหล่านี้

offsetTop และ offsetLeft เป็นคุณสมบัติ CSS ที่ตั้งค่าตำแหน่งขององค์ประกอบตามตำแหน่งของ parent ซึ่ง parent สามารถเป็นองค์ประกอบใด ๆ ที่ไม่ได้ถูกตั้งค่าเป็น static

การคำนวณตำแหน่งใหม่ทั้งหมดนี้ช่วยให้คุณปรับแต่งพฤติกรรมของ terrarium และพืชได้อย่างละเอียด

งาน

งานสุดท้ายในการทำอินเทอร์เฟซให้เสร็จคือการเพิ่มฟังก์ชัน stopElementDrag หลังจากปิดวงเล็บปีกกาของ elementDrag:

function stopElementDrag() {
	document.onpointerup = null;
	document.onpointermove = null;
}

ฟังก์ชันเล็ก ๆ นี้รีเซ็ตเหตุการณ์ onpointerup และ onpointermove เพื่อให้คุณสามารถเริ่มต้นความคืบหน้าของพืชใหม่ได้โดยเริ่มลากมันอีกครั้ง หรือเริ่มลากพืชใหม่

จะเกิดอะไรขึ้นถ้าคุณไม่ตั้งค่าเหตุการณ์เหล่านี้เป็น null?

ตอนนี้คุณได้ทำโครงการของคุณเสร็จสมบูรณ์แล้ว!

🥇ขอแสดงความยินดี! คุณได้สร้าง terrarium ที่สวยงามเสร็จสมบูรณ์แล้ว! terrarium ที่เสร็จสมบูรณ์


🚀ความท้าทาย

เพิ่ม event handler ใหม่ใน closure ของคุณเพื่อทำสิ่งเพิ่มเติมกับพืช เช่น การดับเบิลคลิกที่พืชเพื่อทำให้มันอยู่ด้านหน้า ลองสร้างสรรค์ดู!

แบบทดสอบหลังเรียน

แบบทดสอบหลังเรียน

ทบทวนและศึกษาด้วยตนเอง

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

ค้นหาข้อมูลเพิ่มเติมเกี่ยวกับ pointer events ได้ที่ เอกสาร W3C และ เอกสาร MDN

ตรวจสอบความสามารถของเบราว์เซอร์เสมอโดยใช้ CanIUse.com

งานที่ได้รับมอบหมาย

ทำงานเพิ่มเติมกับ DOM


ข้อจำกัดความรับผิดชอบ:
เอกสารนี้ได้รับการแปลโดยใช้บริการแปลภาษา AI Co-op Translator แม้ว่าเราจะพยายามให้การแปลมีความถูกต้อง แต่โปรดทราบว่าการแปลโดยอัตโนมัติอาจมีข้อผิดพลาดหรือความไม่ถูกต้อง เอกสารต้นฉบับในภาษาดั้งเดิมควรถือเป็นแหล่งข้อมูลที่เชื่อถือได้ สำหรับข้อมูลที่สำคัญ ขอแนะนำให้ใช้บริการแปลภาษามืออาชีพ เราไม่รับผิดชอบต่อความเข้าใจผิดหรือการตีความผิดที่เกิดจากการใช้การแปลนี้