
บทความนี้เป็นความเข้าใจส่วนตัวเกี่ยวกับ 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 (โดยใช้วิธีซิงโครนัสเพื่อเขียนตรรกะแบบอะซิงโครนัส)