บทความนี้เป็นความเข้าใจส่วนตัวเกี่ยวกับ nodejs ในการพัฒนาและการเรียนรู้จริง ตอนนี้รวบรวมไว้เพื่อใช้อ้างอิงในอนาคต ฉันจะรู้สึกเป็นเกียรติหากบทความนี้สามารถสร้างแรงบันดาลใจให้กับคุณได้
I/O : อินพุต/เอาต์พุต อินพุตและเอาต์พุตของระบบ
ระบบสามารถเข้าใจได้ในฐานะปัจเจกบุคคล เช่น บุคคล เมื่อคุณพูด มันเป็นเอาต์พุต และเมื่อคุณฟัง มันเป็นอินพุต
ความแตกต่างระหว่างการบล็อก I/O และ I/O ที่ไม่บล็อกอยู่ที่ว่า ระบบสามารถรับอินพุตอื่นในช่วงเวลาจากอินพุตไปยังเอาต์พุตได้หรือ ไม่
ต่อไปนี้เป็นสองตัวอย่างเพื่อแสดงให้เห็นว่า I/O ที่บล็อกและ I/O ที่ไม่บล็อกคืออะไร
:
ก่อนอื่นเราต้องกำหนดขอบเขตของระบบก่อน ในตัวอย่างนี้ ป้าโรงอาหาร และ พนักงานเสิร์ฟในร้านอาหารถือเป็นระบบหนึ่ง
จากนั้นไม่ว่าคุณจะยอมรับคำสั่งซื้อของผู้อื่นระหว่างการสั่งและการเสิร์ฟอาหารหรือไม่ คุณก็สามารถตรวจสอบได้ว่าจะบล็อก I/O หรือไม่บล็อก I/O
ส่วนป้าโรงอาหารนั้นไม่สามารถสั่งอาหารให้นักเรียนคนอื่นได้เมื่อสั่งอาหารแล้ว หลังจากที่นักเรียนสั่งอาหารและเสิร์ฟอาหารเสร็จแล้วเท่านั้น เธอจึงจะยอมรับคำสั่งของนักเรียนคนต่อไปได้ ดังนั้นป้าโรงอาหารจึงปิดกั้น I/O
สำหรับพนักงานเสิร์ฟในร้านอาหาร เขาสามารถให้บริการแขกคนต่อไปได้หลังจากสั่งอาหารและก่อนที่แขกจะเสิร์ฟอาหาร ดังนั้นพนักงานเสิร์ฟจึงมี I/O ที่ไม่ปิดกั้น
2. ทำงานบ้าน
เมื่อซักผ้าคุณไม่จำเป็นต้องรอเครื่องซักผ้า คุณสามารถกวาดพื้นและจัดโต๊ะให้เรียบร้อยได้ในเวลานี้ หลังจากจัดโต๊ะแล้ว เสื้อผ้าก็จะถูกซักและแขวนเสื้อผ้าให้แห้งเท่านั้น รวมเวลา 25 นาที
จริงๆ แล้วการซักรีดเป็น I/O ที่ไม่ปิดกั้น คุณสามารถทำอย่างอื่นได้ระหว่างใส่เสื้อผ้าลงในเครื่องซักผ้าและซักผ้าให้เสร็จ
เหตุผลที่ I/O แบบไม่บล็อกสามารถปรับปรุงประสิทธิภาพได้ก็คือ สามารถประหยัดเวลาในการรอที่ไม่จำเป็นได้
กุญแจสำคัญในการทำความเข้าใจ I/O ที่ไม่ปิดกั้นคือ
I / O ที่ไม่ปิดกั้นของ nodejs สะท้อนให้เห็นอย่างไร ดังที่ได้กล่าวไว้ก่อนหน้านี้ จุดสำคัญในการทำความเข้าใจ I/O ที่ไม่ปิดกั้นคือการกำหนดขอบเขตของระบบก่อนเป็นอันดับแรก ขอบเขตของระบบของโหนดคือ เธรดหลัก
หากไดอะแกรมสถาปัตยกรรมด้านล่างถูกแบ่งตามการบำรุงรักษาเธรด เส้นประทางด้านซ้ายคือเธรด nodejs และเส้นประทางด้านขวาคือเธรด C++
ตอนนี้เธรด nodejs จำเป็นต้องสืบค้นฐานข้อมูล นี่เป็นการดำเนินการ I/O ทั่วไป โดยจะไม่รอผลลัพธ์ของ I/O และจะประมวลผลการดำเนินการอื่นๆ ต่อไป หัวข้อสำหรับการคำนวณ
รอจนกระทั่งผลลัพธ์ออกมาและส่งคืนไปยังเธรด nodejs ก่อนที่จะรับผลลัพธ์ เธรด nodejs ยังสามารถดำเนินการ I/O อื่น ๆ ได้ ดังนั้นจึงไม่มีการบล็อก
เธรด nodejs เทียบเท่ากับส่วนด้านซ้ายเป็นบริกร และเธรด c++ คือเชฟ
ดังนั้น I/O ที่ไม่บล็อกของโหนดจึงเสร็จสมบูรณ์โดยการเรียกเธรดของผู้ปฏิบัติงาน C++
ดังนั้นจะแจ้งเตือนเธรด nodejs ได้อย่างไรเมื่อเธรด c ++ ได้รับผลลัพธ์ คำตอบคือ การขับเคลื่อนด้วยเหตุการณ์
การบล็อก: กระบวนการเข้าสู่โหมดสลีประหว่าง I/O และรอให้ I/O เสร็จสิ้นก่อนที่จะดำเนินการขั้นตอนถัดไป
การไม่บล็อก : ฟังก์ชันจะส่งคืนทันทีระหว่าง I/O และกระบวนการไม่รอ I/ โอ้ให้เสร็จ..
ดังนั้นจะทราบผลลัพธ์ที่ส่งคืนได้อย่างไร คุณต้องใช้ ไดรเวอร์เหตุการณ์
สิ่งที่เรียกว่า เหตุการณ์ที่ขับเคลื่อนด้วย สามารถเข้าใจได้เหมือนกับเหตุการณ์การคลิกส่วนหน้า ฉันเขียนเหตุการณ์การคลิกครั้งแรก แต่ฉันไม่รู้ว่ามันจะถูกทริกเกอร์เมื่อใด เธรดหลักเท่านั้นที่จะทริกเกอร์ รันฟังก์ชันที่ขับเคลื่อนด้วยเหตุการณ์
โหมดนี้เป็นโหมดผู้สังเกตการณ์ด้วย กล่าวคือ ฉันจะฟังเหตุการณ์ก่อน จากนั้นจึงดำเนินการเมื่อมีการทริกเกอร์
แล้วจะใช้ event drive ได้อย่างไร? คำตอบคือ การเขียนโปรแกรมแบบอะซิงโครนัส
ดังที่ได้กล่าวไปแล้ว nodejs มี I/O ที่ไม่บล็อคจำนวนมาก ดังนั้นผลลัพธ์ของ I/O ที่ไม่บล็อคจึงต้องได้รับผ่านฟังก์ชัน callback วิธีการใช้ฟังก์ชัน callback นี้เป็นการเขียนโปรแกรมแบบอะซิงโครนัส ตัวอย่างเช่น รหัสต่อไปนี้รับผลลัพธ์ผ่านฟังก์ชันโทรกลับ:
glob(__dirname+'/**/*', (err, res) => { ผลลัพธ์ = ความละเอียด console.log('รับผลลัพธ์') })
พารามิเตอร์แรกของฟังก์ชันการเรียกกลับของ nodejs คือข้อผิดพลาด และพารามิเตอร์ที่ตามมาคือผลลัพธ์ ทำไมทำเช่นนี้?
พยายาม { สัมภาษณ์ (ฟังก์ชัน () { console.log('ยิ้ม') - } จับ (ผิดพลาด) { console.log('ร้องไห้' ผิดพลาด) - สัมภาษณ์งาน (โทรกลับ) { setTimeout(() => { ถ้า(Math.random() < 0.1) { โทรกลับ ('ความสำเร็จ') } อื่น { โยนข้อผิดพลาดใหม่ ('ล้มเหลว') - }, 500) }
หลังจากดำเนินการแล้ว ระบบตรวจไม่พบและเกิดข้อผิดพลาดทั่วโลก ส่งผลให้โปรแกรม nodejs ทั้งหมดขัดข้อง
ไม่ถูกตรวจจับโดย try catch เนื่องจาก setTimeout จะเปิดลูปเหตุการณ์ขึ้นใหม่ ทุกครั้งที่เปิดลูปเหตุการณ์ บริบท call stack จะถูกสร้างใหม่ บริบทของ call stack ทุกอย่างแตกต่างออกไป ไม่มีการลอง catch ใน call stack ใหม่นี้ ดังนั้นข้อผิดพลาดจึงถูกส่งออกไปทั่วโลกและไม่สามารถตรวจจับได้ สำหรับรายละเอียด โปรดดูที่บทความนี้ ปัญหาเมื่อดำเนินการลองจับในคิวแบบอะซิงโครนัส
แล้วต้องทำอย่างไร? ส่งข้อผิดพลาดเป็นพารามิเตอร์:
สัมภาษณ์ฟังก์ชัน (โทรกลับ) { setTimeout(() => { ถ้า(Math.random() < 0.5) { โทรกลับ ('ความสำเร็จ') } อื่น { โทรกลับ (ข้อผิดพลาดใหม่ ('ล้มเหลว')) - }, 500) - สัมภาษณ์ (ฟังก์ชั่น (res) { ถ้า (อินสแตนซ์ของข้อผิดพลาดอีกครั้ง) { console.log('ร้องไห้') กลับ - console.log('ยิ้ม') })
แต่นี่เป็นเรื่องที่ยุ่งยากกว่า และคุณต้องตัดสินในการโทรกลับ ดังนั้นจึงมีกฎที่สมบูรณ์ หากไม่มีอยู่ แสดงว่าการดำเนินการสำเร็จ
สัมภาษณ์งาน (โทรกลับ) { setTimeout(() => { ถ้า(Math.random() < 0.5) { โทรกลับ (null, 'ความสำเร็จ') } อื่น { โทรกลับ (ข้อผิดพลาดใหม่ ('ล้มเหลว')) - }, 500) - สัมภาษณ์ (ฟังก์ชั่น (res) { ถ้า (คำตอบ) { กลับ - console.log('ยิ้ม') })วิธีการเขียนการเรียกกลับของ nodejs
ไม่เพียงแต่นำมาซึ่งพื้นที่การเรียกกลับเท่านั้น แต่ยังนำมาซึ่งปัญหาของ การควบคุมกระบวนการแบบอะซิงโครนัส ด้วย
การควบคุมกระบวนการแบบอะซิงโครนัสส่วนใหญ่หมายถึงวิธีจัดการกับตรรกะการทำงานพร้อมกันเมื่อเกิดการทำงานพร้อมกัน ยังคงใช้ตัวอย่างข้างต้น หากเพื่อนร่วมงานของคุณสัมภาษณ์สองบริษัท เขาจะไม่ได้รับการสัมภาษณ์จากบริษัทที่สามจนกว่าเขาจะสัมภาษณ์สองบริษัทได้สำเร็จ แล้วจะเขียนตรรกะนี้ได้อย่างไร คุณต้องเพิ่มจำนวนตัวแปรทั่วโลก:
var count = 0 สัมภาษณ์((ผิดพลาด) => { ถ้า (ผิดพลาด) { กลับ - นับ++ ถ้า (นับ >= 2) { // ตรรกะการประมวลผล} - สัมภาษณ์((ผิดพลาด) => { ถ้า (ผิดพลาด) { กลับ - นับ++ ถ้า (นับ >= 2) { // ตรรกะการประมวลผล} })
การเขียนแบบข้างบนนี้ลำบากและน่าเกลียดมาก ดังนั้นวิธีการเขียนของ Promise และ Async/Await จึงปรากฏในภายหลัง
ลูปเหตุการณ์ปัจจุบันไม่สามารถรับผลลัพธ์ได้ แต่ลูปเหตุการณ์ในอนาคตจะให้ผลลัพธ์แก่คุณ มันคล้ายกันมากกับสิ่งที่คนขี้โกงจะพูด
Promise ไม่ใช่แค่คนหลอกลวงเท่านั้น แต่ยังเป็นเครื่องจักรสถานะด้วย:
const pro = new Promise((แก้ไข, ปฏิเสธ) => { setTimeout(() => { แก้ไข ('2') }, 200) - console.log(pro) // Print: Promise { <pending> }
การดำเนินการ then หรือ catch จะ ส่งคืนสัญญาใหม่ สถานะสุดท้ายของสัญญาจะถูกกำหนดโดยผลการดำเนินการของฟังก์ชันการโทรกลับของ then และ catch:
สัมภาษณ์ฟังก์ชั่น () { คืนสัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { setTimeout(() => { ถ้า (Math.random() > 0.5) { แก้ไข ('ความสำเร็จ') } อื่น { ปฏิเสธ (ข้อผิดพลาดใหม่ ('ล้มเหลว')) - - - - var สัญญา = สัมภาษณ์ () var สัญญา 1 = สัญญา จากนั้น (() => { คืนสัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { setTimeout(() => { แก้ไข ('ยอมรับ') }, 400) - })
สถานะของสัญญา1 ถูกกำหนดโดยสถานะของสัญญาเป็นการตอบแทน นั่นคือ สถานะของสัญญา1 หลังจากดำเนินการตามสัญญาเป็นการตอบแทน ประโยชน์ของสิ่งนี้คืออะไร? วิธีนี้ จะช่วยแก้ปัญหาการโทรกลับนรก
var สัญญา = สัมภาษณ์ () .แล้ว(() => { กลับสัมภาษณ์() - .แล้ว(() => { กลับสัมภาษณ์() - .แล้ว(() => { กลับสัมภาษณ์() - .catch(e => { console.log(จ) })
จากนั้นหากสถานะของสัญญาที่ส่งคืนถูกปฏิเสธ การจับครั้งแรกจะถูกเรียก และครั้งต่อไปจะไม่ถูกเรียก ข้อควรจำ: สายที่ถูกปฏิเสธถือเป็นสายแรก และสายที่ได้รับการแก้ไขเป็นสายแรก
หากสัญญาเป็นเพียงการแก้ปัญหาการเรียกกลับแบบนรก ถือว่าน้อยเกินไปที่จะประมาทสัญญา หน้าที่หลักของสัญญาคือการแก้ปัญหาการควบคุมกระบวนการแบบอะซิงโครนัส หากคุณต้องการสัมภาษณ์สองบริษัทในเวลาเดียวกัน:
function interview() { คืนสัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { setTimeout(() => { ถ้า (Math.random() > 0.5) { แก้ไข ('ความสำเร็จ') } อื่น { ปฏิเสธ (ข้อผิดพลาดใหม่ ('ล้มเหลว')) - - - - สัญญา .all([สัมภาษณ์(), สัมภาษณ์()]) .แล้ว(() => { console.log('ยิ้ม') - //ถ้าบริษัทปฏิเสธก็จับซะ .catch(() => { console.log('ร้องไห้') })
อะไรคือ sync/await:
console.log(async function() { กลับ 4 - console.log(ฟังก์ชั่น() { คืนสัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { แก้ไข(4) - })
ผลลัพธ์ที่พิมพ์ออกมาจะเหมือนกัน กล่าวคือ async/await เป็นเพียงวากยสัมพันธ์สำหรับคำสัญญา
เรารู้ว่าการลอง catch จับข้อผิดพลาด ตาม call stack และสามารถจับข้อผิดพลาดเหนือ call stack เท่านั้น แต่ถ้าคุณใช้ await คุณสามารถตรวจพบข้อผิดพลาดในทุกฟังก์ชันใน call stack แม้ว่าข้อผิดพลาดจะเกิดขึ้นใน call stack ของลูปเหตุการณ์อื่น เช่น setTimeout
หลังจากเปลี่ยนโค้ดการสัมภาษณ์แล้ว คุณจะเห็นว่าโค้ดได้รับการปรับปรุงให้ดีขึ้นมาก
พยายาม { รอสัมภาษณ์(1) รอสัมภาษณ์(2) รอสัมภาษณ์(2) } จับ(e => { console.log(จ) })
จะเกิดอะไรขึ้นถ้าเป็นงานคู่ขนาน?
await Promise.all([interview(1), interview(2)])
เนื่องจาก I/0 ที่ไม่ปิดกั้นของ nodejs จึงจำเป็นต้องใช้วิธีการขับเคลื่อนด้วยเหตุการณ์เพื่อให้ได้ผลลัพธ์ I/O เพื่อให้บรรลุเหตุการณ์ วิธีการขับเคลื่อนเพื่อให้ได้ผลลัพธ์ คุณต้องใช้การเขียนโปรแกรมแบบอะซิงโครนัส เช่น ฟังก์ชันการโทรกลับ แล้วจะรันฟังก์ชันคอลแบ็กเหล่านี้อย่างไรเพื่อให้ได้ผลลัพธ์? จากนั้นคุณจะต้องใช้การวนซ้ำของเหตุการณ์
ลูปเหตุการณ์เป็นรากฐานสำคัญในการตระหนักถึงฟังก์ชัน I/O ที่ไม่บล็อกของ nodejs และลูปเหตุการณ์เป็นความสามารถที่ได้รับจากไลบรารี C ++ libuv
การสาธิตโค้ด:
const eventloop = { คิว: [], วนซ้ำ() { ในขณะที่ (this.queue.length) { const โทรกลับ = this.queue.shift() โทรกลับ() - setTimeout (this.loop.bind (นี้), 50) - เพิ่ม (โทรกลับ) { this.queue.push (โทรกลับ) - - eventloop.ห่วง() setTimeout(() => { eventloop.add(() => { console.log('1') - }, 500) setTimeout(() => { eventloop.add(() => { console.log('2') - }, 800)
setTimeout(this.loop.bind(this), 50)
ทำให้แน่ใจว่าหลังจาก 50ms จะตรวจสอบว่ามีการเรียกกลับในคิวหรือไม่ และหากเป็นเช่นนั้น ให้ดำเนินการ นี่เป็นการวนซ้ำของเหตุการณ์
แน่นอนว่าเหตุการณ์จริงมีความซับซ้อนกว่ามาก และมีคิวมากกว่าหนึ่งคิว ตัวอย่างเช่น มีคิวการดำเนินการไฟล์และคิวเวลา
const เหตุการณ์ลูป = { คิว: [], fsคิว: [], ตัวจับเวลาคิว: [], วนซ้ำ() { ในขณะที่ (this.queue.length) { const โทรกลับ = this.queue.shift() โทรกลับ() - this.fsQueue.forEach (โทรกลับ => { ถ้า (เสร็จแล้ว) { โทรกลับ() - - setTimeout (this.loop.bind (นี้), 50) - เพิ่ม (โทรกลับ) { this.queue.push (โทรกลับ) - }
ก่อนอื่น เราเข้าใจว่า I/O ที่ไม่ปิดกั้นคืออะไร กล่าวคือ ข้ามการดำเนินการของงานที่ตามมาทันทีเมื่อพบกับ I/O และจะไม่รอผลลัพธ์ของ I/O เมื่อประมวลผล I/O ฟังก์ชันการประมวลผลเหตุการณ์ที่เราลงทะเบียนจะถูกเรียกใช้ ซึ่งเรียกว่าขับเคลื่อนด้วยเหตุการณ์ การเขียนโปรแกรมแบบอะซิงโครนัสเป็นสิ่งจำเป็นในการดำเนินการไดรฟ์เหตุการณ์ การเขียนโปรแกรมแบบอะซิงโครนัสเป็นลิงค์ที่สำคัญที่สุดใน nodejs โดยเปลี่ยนจากฟังก์ชันการโทรกลับเป็นสัญญาและสุดท้ายเป็น async/await (โดยใช้วิธีซิงโครนัสเพื่อเขียนตรรกะแบบอะซิงโครนัส)