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