
เดิมทีโหนดถูกสร้างขึ้นเพื่อสร้างเว็บเซิร์ฟเวอร์ประสิทธิภาพสูง เนื่องจากเป็นรันไทม์ฝั่งเซิร์ฟเวอร์สำหรับ JavaScript จึงมีคุณสมบัติต่างๆ เช่น ขับเคลื่อนด้วยเหตุการณ์ I/O แบบอะซิงโครนัส และเธรดเดี่ยว โมเดลการเขียนโปรแกรมแบบอะซิงโครนัสที่อิงตามลูปเหตุการณ์ทำให้โหนดสามารถจัดการการทำงานพร้อมกันในระดับสูงและปรับปรุงประสิทธิภาพของเซิร์ฟเวอร์ได้อย่างมาก ในเวลาเดียวกัน เนื่องจากยังคงรักษาคุณลักษณะแบบเธรดเดี่ยวของ JavaScript ไว้ โหนดจึงไม่จำเป็นต้องจัดการกับปัญหาต่างๆ เช่น การซิงโครไนซ์สถานะและ การหยุดชะงักภายใต้มัลติเธรด ไม่มีค่าใช้จ่ายด้านประสิทธิภาพที่เกิดจากการสลับบริบทของเธรด จากคุณลักษณะเหล่านี้ Node มีข้อดีโดยธรรมชาติคือประสิทธิภาพสูงและการทำงานพร้อมกันสูง และสามารถสร้างแพลตฟอร์มแอปพลิเคชันเครือข่ายความเร็วสูงและปรับขนาดได้ต่างๆ ตามคุณลักษณะดังกล่าว
บทความนี้จะเจาะลึกถึงการใช้งานและกลไกการทำงานของโหนดแบบอะซิงโครนัสและลูปเหตุการณ์ ฉันหวังว่ามันจะเป็นประโยชน์กับคุณ
เหตุใด Node จึงใช้อะซิงโครนัสเป็นรูปแบบการเขียนโปรแกรมหลัก
ดังที่ได้กล่าวไว้ก่อนหน้านี้ โหนดถูกสร้างขึ้นเพื่อสร้างเว็บเซิร์ฟเวอร์ที่มีประสิทธิภาพสูง สมมติว่ามีงานที่ไม่เกี่ยวข้องหลายชุดที่ต้องทำให้เสร็จในสถานการณ์ทางธุรกิจ มีโซลูชันหลักที่ทันสมัยสองรายการ:
การดำเนินการแบบอนุกรมแบบเธรดเดียว
เสร็จสมบูรณ์แบบขนานกับหลายเธรด
การดำเนินการแบบอนุกรมแบบเธรดเดียวคือโมเดลการเขียนโปรแกรมแบบซิงโครนัส แม้ว่าจะสอดคล้องกับวิธีคิดของโปรแกรมเมอร์เป็นลำดับมากกว่า และทำให้เขียนโค้ดได้สะดวกยิ่งขึ้น เนื่องจากดำเนินการ I/O แบบซิงโครนัส จึงสามารถประมวลผลได้เฉพาะ I/O เท่านั้น ในเวลาเดียวกัน คำขอเดียวจะทำให้เซิร์ฟเวอร์ตอบสนองช้าและไม่สามารถใช้งานได้ในสถานการณ์แอปพลิเคชันที่มีการทำงานพร้อมกันสูง นอกจากนี้ เนื่องจากจะบล็อก I/O CPU จึงมักจะรอให้ I/O เสร็จสมบูรณ์และไม่สามารถทำได้ สิ่งอื่นๆ ซึ่งจะจำกัดพลังการประมวลผลของ CPU เพื่อใช้ประโยชน์อย่างเต็มที่ ในที่สุดก็จะนำไปสู่ประสิทธิภาพต่ำ
และโมเดลการเขียนโปรแกรมแบบมัลติเธรดจะทำให้นักพัฒนาปวดหัวเนื่องจากปัญหาต่างๆ เช่น การซิงโครไนซ์สถานะและการหยุดชะงักในการเขียนโปรแกรม แม้ว่ามัลติเธรดสามารถปรับปรุงการใช้งาน CPU บน CPU แบบมัลติคอร์ได้อย่างมีประสิทธิภาพ
แม้ว่าโมเดลการเขียนโปรแกรมของการประมวลผลแบบอนุกรมแบบเธรดเดียวและการดำเนินการแบบขนานแบบมัลติเธรดจะมีข้อดีในตัวเอง แต่ก็มีข้อบกพร่องในแง่ของประสิทธิภาพและความยากลำบากในการพัฒนา
นอกจากนี้ เริ่มต้นจากความเร็วในการตอบสนองต่อคำขอของไคลเอนต์ หากไคลเอนต์ได้รับทรัพยากรสองรายการในเวลาเดียวกัน ความเร็วในการตอบสนองของวิธีซิงโครนัสจะเป็นผลรวมของความเร็วในการตอบสนองของทรัพยากรทั้งสอง และความเร็วในการตอบสนองของ วิธีแบบอะซิงโครนัสจะอยู่ตรงกลางของทั้งสอง วิธีที่ใหญ่ที่สุดคือข้อได้เปรียบด้านประสิทธิภาพที่ชัดเจนมากเมื่อเทียบกับการซิงโครไนซ์ เมื่อความซับซ้อนของแอปพลิเคชันเพิ่มขึ้น สถานการณ์นี้จะพัฒนาไปสู่การตอบสนองต่อคำขอ n รายการในเวลาเดียวกัน และข้อดีของอะซิงโครนัสเมื่อเปรียบเทียบกับการซิงโครไนซ์จะถูกเน้น
โดยสรุป Node ให้คำตอบ: ใช้เธรดเดี่ยวเพื่อหลีกเลี่ยงการหยุดชะงักแบบหลายเธรด การซิงโครไนซ์สถานะ และปัญหาอื่น ๆ ใช้ I/O แบบอะซิงโครนัสเพื่อป้องกันไม่ให้เธรดเดี่ยวถูกบล็อกเพื่อใช้งาน CPU ได้ดีขึ้น นี่คือสาเหตุที่ Node ใช้อะซิงโครนัสเป็นรูปแบบการเขียนโปรแกรมหลัก
นอกจากนี้ เพื่อชดเชยข้อบกพร่องของเธรดเดี่ยวที่ไม่สามารถใช้ CPU แบบมัลติคอร์ได้ Node ยังจัดเตรียมกระบวนการย่อยที่คล้ายกับ Web Workers ในเบราว์เซอร์ ซึ่งสามารถใช้ CPU ได้อย่างมีประสิทธิภาพผ่านกระบวนการของผู้ปฏิบัติงาน
หลังจากพูดถึงว่าทำไมเราจึงควรใช้อะซิงโครนัส แล้วจะใช้งานอะซิงโครนัสได้อย่างไร?
มีการดำเนินการแบบอะซิงโครนัสสองประเภทที่เรามักจะเรียก: ประเภทแรกคือการดำเนินการที่เกี่ยวข้องกับ I/O เช่นไฟล์ I/O และเครือข่าย I/O; อีกประเภทหนึ่งคือการดำเนินการที่ไม่เกี่ยวข้องกับ I/O เช่น setTimeOut และ setInterval แน่นอนว่าอะซิงโครนัสที่เรากำลังพูดถึงนั้นหมายถึงการดำเนินการที่เกี่ยวข้องกับ I/O ซึ่งก็คือ I/O แบบอะซิงโครนัส
มีการเสนอ I/O แบบอะซิงโครนัสด้วยความหวังว่าการเรียก I/O จะไม่ปิดกั้นการทำงานของโปรแกรมที่ตามมา และเวลาเดิมที่รอให้ I/O เสร็จสมบูรณ์จะถูกจัดสรรให้กับธุรกิจที่จำเป็นอื่น ๆ เพื่อดำเนินการ เพื่อให้บรรลุเป้าหมายนี้ คุณต้องใช้ I/O ที่ไม่บล็อก
การบล็อก I/O หมายความว่าหลังจากที่ CPU เริ่มต้นการโทร I/O แล้ว CPU จะบล็อกจนกว่า I/O จะเสร็จสมบูรณ์ เมื่อทราบการบล็อก I/O แล้ว I/O ที่ไม่บล็อกก็เข้าใจได้ง่าย CPU จะกลับมาทันทีหลังจากเริ่มการเรียก I/O แทนที่จะบล็อกและรอ CPU สามารถจัดการธุรกรรมอื่น ๆ ก่อนที่ I/O จะเสร็จสิ้น แน่นอนว่าเมื่อเปรียบเทียบกับการบล็อก I/O แล้ว I/O ที่ไม่บล็อกมีการปรับปรุงประสิทธิภาพมากกว่า
ดังนั้น เนื่องจากมีการใช้ I/O แบบไม่บล็อก และ CPU สามารถกลับมาได้ทันทีหลังจากเริ่มต้นการเรียก I/O แล้วมันจะรู้ได้อย่างไรว่า I/O เสร็จสมบูรณ์แล้ว คำตอบคือการสำรวจ
เพื่อให้ได้สถานะการเรียก I/O ทันเวลา CPU จะเรียกการดำเนินการ I/O ซ้ำๆ อย่างต่อเนื่องเพื่อยืนยันว่า I/O เสร็จสมบูรณ์หรือไม่ เทคโนโลยีการเรียกซ้ำๆ นี้เพื่อพิจารณาว่าการดำเนินการเสร็จสมบูรณ์หรือไม่เรียกว่าการโพล .
แน่นอนว่าการโพลจะทำให้ CPU ดำเนินการตัดสินสถานะซ้ำๆ ซึ่งทำให้เปลืองทรัพยากรของ CPU นอกจากนี้ ช่วงเวลาการโพลยังควบคุมได้ยาก หากช่วงเวลายาวเกินไป การดำเนินการ I/O ให้เสร็จสิ้นจะไม่ได้รับการตอบสนองที่ทันเวลา ซึ่งจะลดความเร็วการตอบสนองของแอปพลิเคชันทางอ้อม หากช่วงเวลาสั้นเกินไป CPU จะถูกใช้ในการโพลอย่างหลีกเลี่ยงไม่ได้ ซึ่งใช้เวลานานกว่าและลดการใช้ทรัพยากรของ CPU
ดังนั้น แม้ว่าการโพลจะเป็นไปตามข้อกำหนดที่ว่า I/O ที่ไม่บล็อกไม่ได้บล็อกการทำงานของโปรแกรมที่ตามมา สำหรับแอปพลิเคชัน ก็ยังคงถือเป็นการซิงโครไนซ์ประเภทหนึ่งเท่านั้น เนื่องจากแอปพลิเคชันยังคงต้องรอ I/ โอที่จะกลับมาอย่างสมบูรณ์ยังคงใช้เวลารอคอยอยู่มาก
I/O แบบอะซิงโครนัสที่สมบูรณ์แบบที่เราคาดหวังควรเป็นว่าแอปพลิเคชันเริ่มต้นการโทรแบบไม่บล็อก ไม่จำเป็นต้องสอบถามสถานะของการโทร I/O อย่างต่อเนื่องผ่านการโพล แต่งานถัดไปสามารถดำเนินการได้โดยตรง I/O เสร็จสมบูรณ์ เพียงส่งข้อมูลไปยังแอปพลิเคชันผ่านเซมาฟอร์หรือโทรกลับ
จะใช้ I/O แบบอะซิงโครนัสนี้ได้อย่างไร คำตอบคือเธรดพูล
แม้ว่าบทความนี้จะกล่าวถึงอยู่เสมอว่าโหนดถูกดำเนินการในเธรดเดียว แต่เธรดเดียวในที่นี้หมายความว่าโค้ด JavaScript จะถูกดำเนินการบนเธรดเดียว สำหรับส่วนต่างๆ เช่น การดำเนินการ I/O ที่ไม่เกี่ยวข้องกับตรรกะทางธุรกิจหลัก โดยการรันในการใช้งานอื่นๆ ในรูปแบบของเธรดจะไม่ส่งผลกระทบหรือขัดขวางการทำงานของเธรดหลัก ในทางกลับกัน มันสามารถปรับปรุงประสิทธิภาพการประมวลผลของเธรดหลักและรับรู้ I/O แบบอะซิงโครนัส
ผ่านกลุ่มเธรด ปล่อยให้เธรดหลักทำการเรียก I/O เท่านั้น ปล่อยให้เธรดอื่นทำการบล็อก I/O หรือ I/O ที่ไม่บล็อก บวกกับเทคโนโลยีการสำรวจเพื่อดำเนินการรับข้อมูลให้เสร็จสมบูรณ์ จากนั้นใช้การสื่อสารระหว่างเธรดเพื่อทำให้ I/O เสร็จสมบูรณ์ /O ข้อมูลที่ได้รับจะถูกส่งผ่าน ซึ่งใช้ I/O แบบอะซิงโครนัสได้อย่างง่ายดาย:

เธรดหลักทำการเรียก I/O ในขณะที่เธรดพูลดำเนินการดำเนินการ I/O ดำเนินการรับข้อมูลให้เสร็จสิ้น จากนั้นส่งข้อมูลไปยังเธรดหลักผ่านการสื่อสารระหว่างเธรดเพื่อทำการเรียก I/O และเธรดหลักให้เสร็จสมบูรณ์ ใช้ซ้ำ ฟังก์ชันการโทรกลับเปิดเผยข้อมูลแก่ผู้ใช้ ซึ่งจากนั้นจะใช้ข้อมูลเพื่อดำเนินการให้เสร็จสิ้นในระดับตรรกะทางธุรกิจ นี่คือกระบวนการ I/O แบบอะซิงโครนัสที่สมบูรณ์ในโหนด สำหรับผู้ใช้ ไม่จำเป็นต้องกังวลเกี่ยวกับรายละเอียดการใช้งานที่ยุ่งยากของเลเยอร์ที่ซ่อนอยู่ พวกเขาเพียงแค่ต้องเรียก API แบบอะซิงโครนัสที่ห่อหุ้มโดย Node และส่งผ่านฟังก์ชันการโทรกลับที่จัดการตรรกะทางธุรกิจ ดังที่แสดงด้านล่าง:
const fs = need ("FS" ;
fs.readFile('example.js', (ข้อมูล) => {
// ประมวลผลตรรกะทางธุรกิจ}); กลไกการใช้งานพื้นฐานแบบอะซิงโครนัสของ Nodejs นั้นแตกต่างกันบนแพลตฟอร์มที่แตกต่างกัน: ภายใต้ Windows IOCP ส่วนใหญ่จะใช้เพื่อส่งการเรียก I/O ไปยังเคอร์เนลของระบบและรับการดำเนินการ I/O ที่เสร็จสมบูรณ์จากเคอร์เนลที่ติดตั้ง ด้วยลูปเหตุการณ์เพื่อทำให้กระบวนการ I/O แบบอะซิงโครนัสเสร็จสมบูรณ์ กระบวนการนี้ดำเนินการผ่าน epoll ภายใต้ Linux; ผ่าน kqueue ภายใต้ FreeBSD และผ่านพอร์ตเหตุการณ์ภายใต้ Solaris เธรดพูลได้รับการจัดเตรียมโดยตรงจากเคอร์เนล (IOCP) ภายใต้ Windows ในขณะที่ซีรีส์ *nix ถูกนำมาใช้โดย libuv เอง
เนื่องจากความแตกต่างระหว่างแพลตฟอร์ม Windows และแพลตฟอร์ม *nix Node จึงจัดให้มี libuv เป็นเลเยอร์การห่อหุ้มเชิงนามธรรม เพื่อให้การตัดสินความเข้ากันได้ของแพลตฟอร์มทั้งหมดเสร็จสมบูรณ์โดยเลเยอร์นี้ ทำให้มั่นใจได้ว่าโหนดชั้นบนและพูลเธรดแบบกำหนดเองชั้นล่างและ IOCP เป็นอิสระจากกัน โหนดจะกำหนดเงื่อนไขของแพลตฟอร์มในระหว่างการคอมไพล์และเลือกคอมไพล์ไฟล์ต้นฉบับในไดเร็กทอรี unix หรือไดเร็กทอรี win ลงในโปรแกรมเป้าหมาย:

ข้างต้นคือการใช้งานแบบอะซิงโครนัสของโหนด
(ขนาดของเธรดพูลสามารถตั้งค่าผ่านตัวแปรสภาพแวดล้อม UV_THREADPOOL_SIZE ค่าเริ่มต้นคือ 4 ผู้ใช้สามารถปรับขนาดของค่านี้ตามสถานการณ์จริง)
จากนั้น คำถามคือ หลังจากได้รับข้อมูลที่ส่งผ่านโดย เธรดพูล เธรดหลักทำงานอย่างไร ฟังก์ชันการโทรกลับจะถูกเรียกเมื่อใด คำตอบคือเหตุการณ์วนซ้ำ
เนื่องจากใช้ฟังก์ชันการเรียกกลับเพื่อประมวลผลข้อมูล I/O จึงหลีกเลี่ยงไม่ได้ที่จะเกี่ยวข้องกับปัญหาว่าจะเรียกฟังก์ชันการเรียกกลับเมื่อใดและอย่างไร ในการพัฒนาจริง สถานการณ์การโทรกลับแบบอะซิงโครนัสหลายแบบมักจะเกี่ยวข้อง วิธีจัดการการโทรกลับแบบอะซิงโครนัสเหล่านี้อย่างสมเหตุสมผล และให้แน่ใจว่าความคืบหน้าของการเรียกกลับแบบอะซิงโครนัสอย่างเป็นระเบียบนั้นเป็นปัญหาที่ยาก ยิ่งไปกว่านั้น I/O แบบอะซิงโครนัส นอกเหนือจาก /O แล้ว ยังมีการเรียกแบบอะซิงโครนัสที่ไม่ใช่ I/O เช่น ตัวจับเวลา API ดังกล่าวเป็นแบบเรียลไทม์สูงและมีลำดับความสำคัญที่สูงกว่าจะกำหนดเวลาการโทรกลับด้วยลำดับความสำคัญที่แตกต่างกันได้อย่างไร
ดังนั้น จะต้องมีกลไกการจัดกำหนดการเพื่อประสานงานอะซิงโครนัสที่มีลำดับความสำคัญและประเภทที่แตกต่างกัน เพื่อให้แน่ใจว่างานเหล่านี้ทำงานในลักษณะที่เป็นระเบียบบนเธรดหลัก เช่นเดียวกับเบราว์เซอร์ Node ได้เลือกลูปเหตุการณ์เพื่อทำการยกภาระหนักนี้
โหนดแบ่งงานออกเป็นเจ็ดประเภทตามประเภทและลำดับความสำคัญ: ตัวจับเวลา รอดำเนินการ ไม่ได้ใช้งาน จัดเตรียม สำรวจ ตรวจสอบ และปิด สำหรับงานแต่ละประเภท จะมีคิวงานเข้าก่อนออกก่อนเพื่อจัดเก็บงานและการเรียกกลับ (ตัวจับเวลาจะถูกจัดเก็บไว้ในฮีปบนสุดขนาดเล็ก) ตามเจ็ดประเภทนี้ Node แบ่งการดำเนินการของลูปเหตุการณ์ออกเป็นเจ็ดขั้นตอนต่อไปนี้:
ลำดับความสำคัญในการดำเนินการของขั้นตอน
ในขั้นตอนนี้ ลูปเหตุการณ์จะตรวจสอบโครงสร้างข้อมูล (ฮีปขั้นต่ำ) ที่เก็บตัวจับเวลา สำรวจตัวจับเวลาในนั้น เปรียบเทียบเวลาปัจจุบันและเวลาหมดอายุทีละรายการ และพิจารณาว่าตัวจับเวลาหมดอายุแล้วหรือไม่ ตัวจับเวลาจะเป็น ฟังก์ชั่นการโทรกลับจะถูกนำออกมาและดำเนินการ
ระยะจะดำเนินการเรียกกลับเมื่อเครือข่าย, IO และข้อยกเว้นอื่นๆ เกิดขึ้น ข้อผิดพลาดบางอย่างที่รายงานโดย *nix จะได้รับการจัดการในขั้นตอนนี้ นอกจากนี้ การเรียกกลับ I/O บางส่วนที่ควรดำเนินการในเฟสโพลของรอบก่อนหน้าจะถูกเลื่อนออกไปเป็นระยะนี้
จะใช้ภายในลูปเหตุการณ์เท่านั้น
ดึงเหตุการณ์ I/O ใหม่ ดำเนินการเรียกกลับที่เกี่ยวข้องกับ I/O (การโทรกลับเกือบทั้งหมด ยกเว้นการโทรกลับที่ปิดเครื่อง การโทรกลับตามกำหนดเวลา และ setImmediate() ) โหนดจะบล็อกที่นี่ในเวลาที่เหมาะสม
โพลล์ นั่นคือ ขั้นตอนการโพลเป็นขั้นตอนที่สำคัญที่สุดของลูปเหตุการณ์ การเรียกกลับสำหรับ I/O เครือข่ายและ I/O ไฟล์จะได้รับการประมวลผลในขั้นตอนนี้เป็นหลัก สเตจนี้มีสองฟังก์ชันหลัก:
คำนวณว่าสเตจนี้ควรบล็อกและสำรวจความคิดเห็นสำหรับ I/O นานเท่าใด
จัดการการเรียกกลับในคิว I/O
เมื่อลูปเหตุการณ์เข้าสู่เฟสโพลและไม่ได้ตั้งเวลาไว้:
หากคิวโพลไม่ว่างเปล่า ลูปเหตุการณ์จะข้ามคิว โดยดำเนินการพร้อมกันจนกว่าคิวจะว่างหรือถึงจำนวนสูงสุดที่สามารถดำเนินการได้
หากคิวการโพลว่างเปล่า หนึ่งในสองสิ่งจะเกิดขึ้น:
หากมีการเรียกกลับ setImmediate() ที่จำเป็นต้องดำเนินการ เฟสการโพลจะสิ้นสุดทันที และเฟสการตรวจสอบจะถูกป้อนเพื่อดำเนินการการเรียกกลับ
หากไม่มีการโทรกลับ setImmediate() ให้ดำเนินการ ลูปเหตุการณ์จะยังคงอยู่ในระยะนี้เพื่อรอการเพิ่มการโทรกลับลงในคิว จากนั้นจึงดำเนินการทันที ลูปเหตุการณ์จะรอจนกว่าการหมดเวลาจะหมดลง เหตุผลที่ฉันเลือกหยุดที่นี่ก็เพราะว่า Node จัดการ IO เป็นหลัก เพื่อให้สามารถตอบสนองต่อ IO ได้ทันท่วงทีมากขึ้น
เมื่อคิวแบบสำรวจว่างเปล่าลูปเหตุการณ์จะตรวจสอบตัวจับเวลาที่ถึงขีด จำกัด เวลาของพวกเขา หากตัวจับเวลาตั้งแต่หนึ่งตัวขึ้นไปถึงเกณฑ์เวลา ลูปเหตุการณ์จะกลับไปที่เฟสตัวจับเวลาเพื่อดำเนินการเรียกกลับสำหรับตัวจับเวลาเหล่านี้
เฟสนี้จะเรียกใช้การเรียกกลับของ setImmediate() ตามลำดับ
ระยะนี้จะดำเนินการเรียกกลับเพื่อปิดทรัพยากร เช่น socket.on('close', ...) การดำเนินการขั้นตอนนี้ล่าช้าจะมีผลกระทบเพียงเล็กน้อยและมีลำดับความสำคัญต่ำที่สุด
เมื่อกระบวนการโหนดเริ่มต้น กระบวนการจะเริ่มต้นลูปเหตุการณ์ รันโค้ดอินพุตของผู้ใช้ ทำการเรียก API แบบอะซิงโครนัสที่สอดคล้องกัน การตั้งเวลาจับเวลา ฯลฯ จากนั้นเริ่มเข้าสู่ลูปเหตุการณ์:
┌───────── ── ────────────────┐ ┌─>│ ตัวจับเวลา │ │ │ รอการติดต่อกลับ │ │ │ ไม่ได้ใช้งาน เตรียมตัว │ │ ┌─────────────┴────────────┐ │ เข้ามา: │ │ │ โพล │<─────┤ การเชื่อมต่อ │ │────7เฉียง │ │ ตรวจสอบ │ └──┤ ปิดการโทรกลับ │ └─────────────────────────────┘
การวนซ้ำแต่ละครั้งของลูปเหตุการณ์ (มักเรียกว่าขีด) จะเป็นดังที่ระบุไว้ข้างต้น ลำดับความสำคัญ order เข้าสู่ขั้นตอนการดำเนินการเจ็ดขั้นตอน แต่ละขั้นตอนจะดำเนินการเรียกกลับจำนวนหนึ่งในคิว เหตุผลที่ดำเนินการเพียงหมายเลขหนึ่งเท่านั้นแต่ไม่ได้ดำเนินการทั้งหมดคือเพื่อป้องกันไม่ให้เวลาดำเนินการของขั้นตอนปัจจุบันยาวเกินไปและ หลีกเลี่ยงความล้มเหลวในขั้นตอนต่อไป
ตกลงข้างต้นคือโฟลว์การดำเนินการพื้นฐานของลูปเหตุการณ์ ทีนี้มาดูคำถามอื่นกัน
สำหรับสถานการณ์ต่อไปนี้:
const server = net.createServer (() => {}) ฟัง (8080);
server.on('listening', () => {}); เมื่อบริการเชื่อมโยงกับพอร์ต 8000 ได้สำเร็จ นั่นคือเมื่อเรียก listen() สำเร็จ การเรียกกลับของเหตุการณ์ listening ยังไม่ถูกผูกไว้ ดังนั้น หลังจากพอร์ตถูกผูกไว้สำเร็จการโทรกลับของเหตุการณ์ listening ที่เราผ่านจะไม่ถูกดำเนินการ
เมื่อนึกถึงคำถามอื่นเราอาจมีความต้องการบางอย่างในระหว่างการพัฒนาเช่นการจัดการข้อผิดพลาดการทำความสะอาดทรัพยากรที่ไม่จำเป็นและงานอื่น ๆ ที่มีลำดับความสำคัญต่ำ หาก setImmediate() ถูกส่งผ่านแบบอะซิงโครนัสเช่นในรูปแบบของการโทรกลับเวลาการดำเนินการของพวกเขาไม่สามารถรับประกันได้และประสิทธิภาพแบบเรียลไทม์ไม่สูง แล้วจะจัดการกับตรรกะเหล่านี้อย่างไร?
จากปัญหาเหล่านี้โหนดได้อ้างอิงจากเบราว์เซอร์และใช้ชุดกลไกไมโครงาน ใน Node นอกเหนือจากการเรียก new Promise().then() ฟังก์ชันการเรียกกลับที่ส่งผ่านจะถูกห่อหุ้มไว้ในไมโครทาสก์ การเรียกกลับของ process.nextTick() จะถูกห่อหุ้มไว้ในไมโครทาสก์ด้วย และลำดับความสำคัญในการดำเนินการของ หลังจะสูงกว่าอดีต
ด้วยไมโครทาสก์ กระบวนการดำเนินการของลูปเหตุการณ์คืออะไร换句话说,微任务的执行时机在什么时候?
ในโหนด 11 และเวอร์ชันที่ใหม่กว่า เมื่อมีการดำเนินการงานในขั้นตอนหนึ่ง คิวไมโครทาสก์จะถูกดำเนินการทันทีและคิวจะถูกล้าง
การดำเนินการไมโครทาสก์เริ่มต้นหลังจากดำเนินการขั้นตอนก่อนโหนด 11
ดังนั้น ด้วยไมโครทาสก์ แต่ละรอบของลูปเหตุการณ์จะดำเนินการงานในระยะตัวจับเวลาก่อน จากนั้นจึงล้างคิวไมโครทาสก์ของ process.nextTick() และ new Promise().then() ตามลำดับ จากนั้นจึงดำเนินการต่อไป งานถัดไปในสเตจตัวจับเวลาหรือสเตจถัดไป นั่นคือ งานในสเตจที่รอดำเนินการ และอื่นๆ ตามลำดับนี้
การใช้ process.nextTick() ทำให้ Node สามารถแก้ปัญหาการเชื่อมโยงพอร์ตข้างต้นได้ ภายในเมธอด listen() การออกเหตุการณ์ listening จะถูกห่อหุ้มไว้ในการโทรกลับและส่งผ่านไปยัง process.nextTick() ดังที่แสดงในตัวอย่างต่อไปนี้ code:
function listen() {
// 进行监听端口的操作...
// สรุปการออกเหตุการณ์ `listening` ให้เป็น callback และส่งผ่านไปยัง `process.nextTick()` ใน process.nextTick(() => {
ปล่อย ('ฟัง');
-
}; หลังจากที่โค้ดปัจจุบันถูกดำเนินการแล้ว ไมโครทาสก์จะเริ่มดำเนินการ ดังนั้นจึงออกเหตุการณ์ listening และทริกเกอร์การเรียกกลับของเหตุการณ์
เนื่องจากความคาดเดาไม่ได้และความซับซ้อนของตัวอะซิงโครนัสเอง ในกระบวนการใช้ API แบบอะซิงโครนัสที่ Node มอบให้ แม้ว่าเราจะเชี่ยวชาญหลักการดำเนินการของลูปเหตุการณ์แล้ว แต่ก็ยังอาจมีปรากฏการณ์บางอย่างที่ไม่เป็นไปตามสัญชาตญาณหรือคาดหวังได้ .
ตัวอย่างเช่น ลำดับการดำเนินการของตัวจับเวลา ( setTimeout , setImmediate ) จะแตกต่างกันไปขึ้นอยู่กับบริบทที่ถูกเรียก ถ้าทั้งสองถูกเรียกจากบริบทระดับบนสุด เวลาดำเนินการจะขึ้นอยู่กับประสิทธิภาพของกระบวนการหรือเครื่องจักร
ลองดูตัวอย่างต่อไปนี้:
setTimeout(() => {
console.log('หมดเวลา');
}, 0);
setimmediate (() => {
console.log('ทันที');
}); ผลการดำเนินการของโค้ดข้างต้นคืออะไร? ตามคำอธิบายของเราเกี่ยวกับลูปเหตุการณ์ตอนนี้คุณอาจมีคำตอบนี้: เนื่องจากเฟสตัวจับเวลาจะถูกดำเนินการก่อนที่ขั้นตอนการตรวจสอบการโทรกลับของ setTimeout() จะถูกดำเนินการก่อนจากนั้นจะเรียกกลับของ setImmediate() ดำเนินการ
ในความเป็นจริง ผลลัพธ์ของโค้ดนี้ไม่แน่นอน การหมดเวลาอาจถูกส่งออกก่อน หรือทันทีอาจถูกส่งออกก่อน นี่เป็นเพราะตัวจับเวลาทั้งสองถูกเรียกในบริบททั่วโลก จริงๆ แล้วยังไม่แน่ชัด setTimeout() จะถูกดำเนินการในระยะตัวจับเวลาแรกหรือไม่ ดังนั้นผลลัพธ์เอาต์พุตที่แตกต่างกันจึงจะปรากฏขึ้น
(เมื่อค่าของ delay (พารามิเตอร์ที่สองของ setTimeout ) มากกว่า 2147483647 หรือน้อยกว่า 1 delay จะถูกตั้งค่าเป็น 1 )
ลองดูที่รหัสต่อไปนี้:
const fs = ต้องการ ('fs');
fs.readFile(__ชื่อไฟล์, () => {
setTimeout(() => {
console.log('หมดเวลา');
}, 0);
setImmediate(() => {
console.log('ทันที');
-
}) จะเห็นได้ว่าในโค้ดนี้ ตัวจับเวลาทั้งสองถูกห่อหุ้มไว้ในฟังก์ชันการเรียกกลับและส่งผ่านไปยัง readFile เห็นได้ชัดว่าเมื่อมีการเรียกการเรียกกลับ เวลาปัจจุบันจะต้องมากกว่า 1 ms ดังนั้นการเรียกกลับของ setTimeout จะ ยาวกว่าการโทรกลับ timeout immediate setImmediate
ข้างต้นคือสิ่งที่เกี่ยวข้องกับตัวจับเวลาที่คุณต้องใส่ใจเมื่อใช้ Node นอกจากนี้คุณยังต้องให้ความสนใจกับลำดับการดำเนินการของ process.nextTick() , new Promise().then() และ setImmediate() .
: บทความเริ่มต้นด้วยคำอธิบายที่ละเอียดยิ่งขึ้นเกี่ยวกับหลักการดำเนินการของการวนรอบเหตุการณ์โหนดจากสองมุมมองว่าทำไมต้องใช้แบบอะซิงโครนัสและวิธีการใช้แบบอะซิงโครนัสและกล่าวถึงเรื่องที่เกี่ยวข้องบางอย่างที่ต้องการความสนใจ คุณ.