"Event Loop" ของ Node เป็นแกนหลักของความสามารถในการจัดการกับการทำงานร่วมกันขนาดใหญ่และปริมาณงานที่สูง นี่คือสถานที่ที่วิเศษที่สุด ตามนี้ Node.js สามารถเข้าใจได้ว่าเป็น "เธรดเดี่ยว" และยังช่วยให้สามารถดำเนินการโดยพลการในพื้นหลัง บทความนี้จะแสดงให้เห็นว่าเหตุการณ์ลูปทำงานอย่างไรและคุณสามารถรู้สึกถึงความมหัศจรรย์ของมันได้เช่นกัน
การเขียนโปรแกรมที่ขับเคลื่อนด้วยเหตุการณ์
เพื่อทำความเข้าใจกับลูปเหตุการณ์เราต้องเข้าใจการเขียนโปรแกรมไดรฟ์กิจกรรมก่อน มันปรากฏในปี 1960 วันนี้การเขียนโปรแกรมที่ขับเคลื่อนด้วยเหตุการณ์ใช้กันอย่างแพร่หลายในการเขียนโปรแกรม UI หนึ่งในการใช้งานหลักของ JavaScript คือการโต้ตอบกับ DOM ดังนั้นจึงเป็นเรื่องธรรมดาที่จะใช้ API ตามเหตุการณ์
เพียงกำหนด: การเขียนโปรแกรมที่ขับเคลื่อนด้วยเหตุการณ์ควบคุมกระบวนการแอปพลิเคชันผ่านการเปลี่ยนแปลงเหตุการณ์หรือสถานะ โดยทั่วไปจะถูกนำไปใช้ผ่านการตรวจสอบเหตุการณ์ เมื่อตรวจพบเหตุการณ์ (เช่นการเปลี่ยนแปลงสถานะ) ฟังก์ชันการโทรกลับที่สอดคล้องกันจะถูกเรียก ฟังดูคุ้นเคย? อันที่จริงนี่เป็นหลักการทำงานพื้นฐานของลูปเหตุการณ์ Node.js
หากคุณคุ้นเคยกับการพัฒนา JavaScript ฝั่งไคลเอ็นต์ให้คิดถึงวิธีการ. on*() เหล่านั้นเช่น element.onclick () ซึ่งใช้เพื่อรวมกับองค์ประกอบ DOM เพื่อผ่านการโต้ตอบของผู้ใช้ โหมดการทำงานนี้อนุญาตให้มีการเรียกใช้หลายเหตุการณ์ในอินสแตนซ์เดียว node.js ทริกเกอร์โหมดนี้ผ่าน Eventemitter (Event Generator) เช่นในซ็อกเก็ตและโมดูล "HTTP" ทางฝั่งเซิร์ฟเวอร์ การเปลี่ยนแปลงสถานะอย่างน้อยหนึ่งครั้งอาจถูกทริกเกอร์จากอินสแตนซ์เดียว
รูปแบบทั่วไปอีกประการหนึ่งคือการแสดงความสำเร็จและความล้มเหลว โดยทั่วไปมีวิธีการใช้งานทั่วไปสองวิธีในขณะนี้ สิ่งแรกคือการส่ง "ข้อผิดพลาดข้อผิดพลาด" ไปยังการโทรกลับซึ่งโดยทั่วไปจะส่งผ่านไปยังฟังก์ชันการโทรกลับเป็นพารามิเตอร์แรก ประเภทที่สองคือการใช้โหมดการออกแบบสัญญาและเพิ่ม ES6 หมายเหตุ* โหมดสัญญาใช้วิธีการเขียนโซ่ฟังก์ชั่น jQuery เพื่อหลีกเลี่ยงฟังก์ชั่นการโทรกลับลึกเช่น:
การคัดลอกรหัสมีดังนี้:
$ .getJson ('/getUser') เสร็จแล้ว (SuccessHandler) .Fail (FailHandler)
โมดูล "FS" (ระบบไฟล์) ส่วนใหญ่ใช้รูปแบบของการผ่านข้อยกเว้นในการโทรกลับ ทางเทคนิคทริกเกอร์การโทรบางอย่างเช่นเหตุการณ์ที่แนบมา fs.ReadFile () แต่ API นั้นใช้เพื่อเตือนผู้ใช้ให้แสดงความสำเร็จหรือความล้มเหลวของการดำเนินการเท่านั้น API ดังกล่าวได้รับการคัดเลือกด้วยเหตุผลทางสถาปัตยกรรมไม่ใช่ข้อ จำกัด ทางเทคนิค
ความเข้าใจผิดที่พบบ่อยคือตัวปล่อยเหตุการณ์นั้นเป็นแบบอะซิงโครนัสโดยเนื้อแท้เมื่อเรียกเหตุการณ์ แต่สิ่งนี้ไม่ถูกต้อง นี่คือตัวอย่างโค้ดง่ายๆเพื่อพิสูจน์สิ่งนี้
การคัดลอกรหัสมีดังนี้:
ฟังก์ชั่น myemitter () {
Eventemitter.call (นี่);
-
util.inherits (MyEmitter, Eventemitter);
myemitter.prototype.dostuff = ฟังก์ชัน Dostuff () {
console.log ('ก่อน')
emitter.emit ('ไฟ')
console.log ('After')}
-
var me = myemitter ใหม่ ();
me.on ('Fire', function () {
console.log ('Emit Fired');
-
me.dostuff ();
// เอาต์พุต:
// ก่อน
// ปล่อยออกมา
// หลังจาก
หมายเหตุ* หาก emitter.emit เป็นแบบอะซิงโครนัสเอาท์พุทควรเป็น
// ก่อน
// หลังจาก
// ปล่อยออกมา
Eventemitter มักจะปรากฏแบบอะซิงโครนัสเพราะมันมักจะใช้เพื่อแจ้งการดำเนินการที่ต้องเสร็จสิ้นแบบอะซิงโครนัส แต่ Eventemitter API นั้นมีการซิงโครนัสอย่างสมบูรณ์ ฟังก์ชั่นการฟังสามารถดำเนินการแบบอะซิงโครนัสได้ แต่โปรดทราบว่าฟังก์ชั่นการฟังทั้งหมดจะถูกดำเนินการแบบซิงโครนัสตามลำดับที่เพิ่มเข้ามา
ภาพรวมกลไกและการรวมเธรด
โหนดนั้นขึ้นอยู่กับหลายไลบรารี หนึ่งในนั้นคือ Libuv ซึ่งเป็นห้องสมุดที่จัดการคิวและการประหารชีวิตแบบอะซิงโครนัสอย่างน่าอัศจรรย์
โหนดใช้ฟังก์ชั่นที่มีอยู่ให้มากที่สุดเท่าที่จะทำได้เพื่อใช้เคอร์เนลระบบปฏิบัติการ ตัวอย่างเช่นมีการสร้างคำขอตอบสนองการเชื่อมต่อจะถูกส่งต่อและมอบหมายไปยังระบบสำหรับการประมวลผล ตัวอย่างเช่นการเชื่อมต่อที่เข้ามาจะถูกคิวผ่านระบบปฏิบัติการจนกว่าจะสามารถประมวลผลได้ด้วยโหนด
คุณอาจเคยได้ยินว่าโหนดมีพูลเธรดและคุณอาจสงสัยว่า: "ถ้าโหนดจัดการงานตามลำดับทำไมคุณต้องมีพูลเธรด" นี่เป็นเพราะในเคอร์เนลไม่ใช่งานทั้งหมดที่ดำเนินการแบบอะซิงโครนัส ในกรณีนี้ node.js จะต้องสามารถล็อคเธรดเป็นระยะเวลาหนึ่งในขณะที่มันทำงานเพื่อให้สามารถดำเนินการลูปเหตุการณ์ต่อไปโดยไม่ถูกบล็อก
ต่อไปนี้เป็นไดอะแกรมตัวอย่างง่ายๆเพื่อแสดงกลไกการทำงานภายใน:
ดุรไทม์ส│
│รอการโทรกลับ│
│─────7 ๆ บ้าง
การเชื่อมต่อแบบสำรวจ
│──────7เฉียง
──┤ setimmediate │
มีปัญหาบางอย่างในการทำความเข้าใจกลไกการทำงานภายในของลูปเหตุการณ์:
การโทรกลับทั้งหมดจะตั้งไว้ล่วงหน้าผ่าน process.nexttick () ก่อนสิ้นสุดระยะหนึ่งของลูปเหตุการณ์ (ตัวอย่างเช่นตัวจับเวลา) และการเปลี่ยนไปสู่ระยะถัดไป สิ่งนี้จะหลีกเลี่ยงการเรียกซ้ำที่อาจเกิดขึ้นเพื่อดำเนินการ nexttick () ทำให้เกิดการวนซ้ำที่ไม่มีที่สิ้นสุด
"รอการเรียกกลับ" เป็นการโทรกลับในคิวการโทรกลับซึ่งจะไม่ถูกประมวลผลโดยวัฏจักรลูปอื่น ๆ (ตัวอย่างเช่นส่งผ่านไปยัง fs.write)
ตัวปล่อยกิจกรรมและลูปเหตุการณ์
โดยการสร้าง Eventemitter การโต้ตอบกับลูปเหตุการณ์สามารถทำให้ง่ายขึ้น มันเป็น encapsulation สากลที่ทำให้คุณสามารถสร้าง API ที่ใช้เหตุการณ์ได้ง่ายขึ้น การโต้ตอบทั้งสองมักทำให้นักพัฒนารู้สึกสับสน
ตัวอย่างต่อไปนี้แสดงให้เห็นว่าการลืมว่าเหตุการณ์จะถูกทริกเกอร์ซิงโครนัสอาจทำให้เหตุการณ์พลาด
การคัดลอกรหัสมีดังนี้:
// หลังจาก v0.10 ต้องการ ('เหตุการณ์') ไม่จำเป็นต้องใช้ Eventemitter อีกต่อไป
var eventemitter = ต้องการ ('เหตุการณ์');
var util = ต้องการ ('util');
ฟังก์ชั่น mything () {
Eventemitter.call (นี่);
dofirsthing ();
this.emit ('Thing1');
-
util.inherits (mything, eventemitter);
var mt = new mything ();
mt.on ('thing1', ฟังก์ชั่น onthing1 () {
// ขออภัยเหตุการณ์นี้จะไม่เกิดขึ้น
-
เหตุการณ์ 'สิ่งที่ 1' ด้านบนจะไม่ถูกจับโดย mything () เพราะ mything () จะต้องได้รับการสร้างอินสแตนซ์ก่อนที่มันจะสามารถฟังเหตุการณ์ นี่คือวิธีแก้ปัญหาง่ายๆโดยไม่ต้องเพิ่มการปิดเพิ่มเติม:
การคัดลอกรหัสมีดังนี้:
var eventemitter = ต้องการ ('เหตุการณ์');
var util = ต้องการ ('util');
ฟังก์ชั่น mything () {
Eventemitter.call (นี่);
dofirsthing ();
setimmediate (emitthing1, this);
-
util.inherits (mything, eventemitter);
ฟังก์ชั่น emitthing1 (ตัวเอง) {
self.emit ('สิ่งที่ 1');
-
var mt = new mything ();
mt.on ('thing1', ฟังก์ชั่น onthing1 () {
// ดำเนินการ
-
รูปแบบต่อไปนี้สามารถใช้งานได้ แต่การแสดงบางอย่างก็หายไป:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชั่น mything () {
Eventemitter.call (นี่);
dofirsthing ();
// การใช้ฟังก์ชัน#bind () จะสูญเสียประสิทธิภาพ
setimmediate (this.emit.bind (นี่, 'thing1'));
-
util.inherits (mything, eventemitter);
ปัญหาอื่นคือการเรียกเกิดข้อผิดพลาด (ข้อยกเว้น) เป็นเรื่องยากที่จะหาปัญหาในแอปพลิเคชันของคุณ แต่ไม่มีสแต็กการโทร (หมายเหตุ *E.Stack) การดีบักแทบจะเป็นไปไม่ได้เลย สแต็กการโทรจะหายไปเมื่อข้อผิดพลาดถูกร้องขอโดยรีโมตแบบอะซิงโครนัส มีวิธีแก้ปัญหาที่เป็นไปได้สองประการ: การเรียกใช้แบบซิงโครนัสหรือตรวจสอบให้แน่ใจว่าข้อผิดพลาดจะถูกส่งผ่านด้วยข้อมูลสำคัญอื่น ๆ ตัวอย่างต่อไปนี้แสดงให้เห็นถึงวิธีแก้ปัญหาทั้งสองนี้:
การคัดลอกรหัสมีดังนี้:
mything.prototype.foo = function foo () {
// ข้อผิดพลาดนี้จะถูกทริกเกอร์แบบอะซิงโครนัส
var er = dofirsthing ();
ถ้า (เอ้อ) {
// เมื่อทริกเกอร์คุณต้องสร้างข้อผิดพลาดใหม่ที่เก็บข้อมูลสแต็กในสถานที่
Setimmediate (Emiterror, ข้อผิดพลาดใหม่ ('สิ่งที่ไม่ดี'));
กลับ;
-
// ทริกเกอร์ข้อผิดพลาดและประมวลผลทันที (ซิงโครไนซ์)
var er = dosecondthing ();
ถ้า (เอ้อ) {
this.emit ('ข้อผิดพลาด', 'สิ่งที่ไม่ดีมากขึ้น');
กลับ;
-
-
ประเมินสถานการณ์ เมื่อเกิดข้อผิดพลาดเกิดขึ้นก็เป็นไปได้ที่จะดำเนินการทันที หรืออาจเป็นเรื่องเล็กน้อยและสามารถจัดการได้ง่ายหรือจัดการในภายหลัง นอกจากนี้การผ่านข้อผิดพลาดผ่านตัวสร้างไม่ใช่ความคิดที่ดีเนื่องจากอินสแตนซ์วัตถุที่สร้างขึ้นนั้นไม่สมบูรณ์ ข้อยกเว้นคือกรณีที่เกิดข้อผิดพลาดโดยตรงในตอนนี้
บทสรุป
บทความนี้สำรวจกลไกการทำงานภายในและรายละเอียดทางเทคนิคของลูปเหตุการณ์สั้น ๆ ทั้งหมดได้รับการพิจารณาอย่างรอบคอบ อีกบทความหนึ่งจะหารือเกี่ยวกับการมีปฏิสัมพันธ์ระหว่างลูปเหตุการณ์และเมล็ดของระบบและแสดงให้เห็นถึงความมหัศจรรย์ของ nodejs ที่ทำงานแบบอะซิงโครนัส