Node.js Generators & เปรียบเทียบกับการโทรกลับ
⚡ สรุปอย่างชาญฉลาด
Generatorใน Node.js ฟังก์ชันเรียกกลับ (callback) สามารถหยุดและเริ่มการทำงานใหม่ได้ตามต้องการ คำสั่ง `yield` จะหยุดฟังก์ชันชั่วคราว และเมธอด `next` จะเริ่มการทำงานต่อ การใช้คำสั่งทั้งสองนี้ร่วมกันจะลดความซับซ้อนของฟังก์ชันเรียกกลับที่ซ้อนกันหลายชั้นให้กลายเป็นโค้ดที่อ่านง่ายและเรียงลำดับได้

ในบทแนะนำนี้ เราจะมาดูตัวสร้าง (generator) และความแตกต่างระหว่างตัวสร้างกับฟังก์ชันเรียกกลับ (callback)
เครื่องกำเนิดไฟฟ้าคืออะไร?
Generatorกลายเป็นที่นิยมใน Node.js เพราะสิ่งที่พวกเขาสามารถทำได้
- Generatorคือการดำเนินการฟังก์ชันที่สามารถระงับและดำเนินการต่อในภายหลังได้
- Generatorมีประโยชน์สำหรับแนวคิดต่างๆ เช่น "การประมวลผลแบบไม่เร่งรีบ" (lazy execution) โดยการระงับการประมวลผลและกลับมาทำงานต่อตามต้องการ ค่าต่างๆ จะถูกดึงมาใช้ก็ต่อเมื่อจำเป็นเท่านั้น
Generatorมีสองวิธีหลักดังต่อไปนี้
- วิธีการให้ผลผลิต — เรียกใช้ภายในฟังก์ชันเพื่อหยุดการทำงาน ณ บรรทัดที่ระบุ
yieldปรากฏ - วิธีการต่อไป — เรียกใช้จากแอปพลิเคชันหลักเพื่อดำเนินการต่อฟังก์ชันที่หยุดชั่วคราว การทำงานจะดำเนินต่อไปจนกว่าจะมีการเรียกใช้ครั้งถัดไป
yieldหรือจนกว่าฟังก์ชันจะสิ้นสุด
เรามาดูตัวอย่างการใช้งานตัวสร้าง (generator) กัน ตัวอย่างนี้กำหนดฟังก์ชัน Add อย่างง่ายที่บวกตัวเลขสองตัว โดยหยุดการทำงานที่จุดต่างๆ เพื่อแสดงให้เห็นว่าตัวสร้างทำงานอย่างไร
function* Add(x) { yield x + 1; // The value passed into the second next() call lands here var y = yield(null); return x + y; } var gen = Add(5); gen.next(); // runs to the first yield, returns { value: 6, done: false } gen.next(6); // resumes, y becomes 6, returns { value: 11, done: true }
Code คำอธิบาย
- ขั้นตอนแรกคือการกำหนดฟังก์ชันตัวสร้าง โปรดสังเกต
*เพิ่มลงในไฟล์functionคำหลัก ฟังก์ชันนี้ถูกเรียกใช้Addและรับพารามิเตอร์x. - การขอ
yieldคีย์เวิร์ดนี้ใช้เฉพาะกับตัวสร้าง (generator) และจะหยุดการทำงานของฟังก์ชันกลางคัน การทำงานจะหยุดลงตรงนี้จนกว่าจะ...next()ถูกเรียก - ที่สอง
yieldทั้งหยุดการทำงานของฟังก์ชันชั่วคราวและรับค่า: ไม่ว่าค่าใดจะถูกส่งต่อไปยังส่วนถัดไปnext()การโทรกลายเป็นค่าของy. - การเรียกร้อง
gen.next()ครั้งแรกที่รันฟังก์ชัน จะรันไปจนถึงครั้งแรกyieldกำลังโทรgen.next(6)ดำเนินการต่อ กำหนดค่า 6 ให้กับyและกลับมาx + y.
⚠️ แก้ไข: รายการเดิมมีบรรทัดดังกล่าว y = 6 ทันทีหลังจาก var y = yield(null);การกำหนดค่าดังกล่าวจะเขียนทับค่าใดๆ ก็ตามที่มีอยู่ next() การจัดหา ซึ่งทำให้จุดประสงค์ของตัวอย่างนั้นเสียไป และทำให้ข้อที่สอง yield ไร้ความหมาย ค่าจะถูกส่งผ่านเข้ามาแล้ว gen.next(6)ซึ่งเป็นวิธีการที่ตัวสร้างข้อมูลรับข้อมูลจากผู้เรียกใช้งาน
Callback กับ Generator
Generators ช่วยแก้ปัญหาที่เรียกว่า "callback hell" ฟังก์ชัน callback บางครั้งอาจซ้อนกันลึกมากในระหว่างการพัฒนาแอปพลิเคชัน Node.js จนยากต่อการติดตาม
นี่คือจุดที่ตัวสร้าง (generator) เข้ามาช่วย ตัวอย่างที่พบได้บ่อยที่สุดอย่างหนึ่งคือการสร้างฟังก์ชันจับเวลา ตัวอย่างด้านล่างสร้างฟังก์ชันหน่วงเวลาอย่างง่าย แล้วเรียกใช้ฟังก์ชันนั้นด้วยค่าหน่วงเวลาที่ 1000, 2000 และ 3000 มิลลิวินาที
ขั้นตอน 1) กำหนดฟังก์ชันเรียกกลับ (callback function) พร้อมโค้ดหน่วงเวลาที่จำเป็น
function Timedelay(ptime, callback) { setTimeout(function() { callback("Pausing for " + ptime); }, ptime); // the delay must use ptime, the parameter name }
Code คำอธิบาย
- ฟังก์ชันที่เรียกว่า
Timedelayถูกสร้างขึ้นด้วยพารามิเตอร์ptimeซึ่งระบุจำนวนมิลลิวินาทีที่จะหยุดชั่วคราว setTimeoutรอเป็นเวลาตามจำนวนมิลลิวินาทีที่ระบุ แล้วจึงเรียกใช้ฟังก์ชันเรียกกลับที่กำหนดไว้
⚠️ แก้ไข: รหัสเดิมผ่านแล้ว time เป็นข้อโต้แย้งที่สองต่อ setTimeoutแต่ไม่มีตัวแปรชื่อนั้น time มีอยู่จริง — พารามิเตอร์คือ ptimeตามที่เขียนไว้ มันจะโยน ReferenceError: time is not defined.
ขั้นตอน 2) ทีนี้ลองพิจารณาโค้ดที่ใช้ Callback ดู ในการหน่วงเวลา 1000, 2000 และ 3000 มิลลิวินาที Callback จะต้องซ้อนกันอยู่ภายในกันและกัน
Timedelay(1000, function(message) { console.log(message); Timedelay(2000, function(message) { console.log(message); Timedelay(3000, function(message) { console.log(message); }); }); });
Code คำอธิบาย
Timedelayเรียกใช้เป็นฟังก์ชันเรียกกลับ (callback) โดยมีค่าเป็น 1000- ภายในฟังก์ชันเรียกกลับนั้น
Timedelayถูกเรียกอีกครั้งในปี 2000 - ภายในนั้น
Timedelayถูกเรียกเป็นครั้งที่สามด้วย 3000
⚠️ แก้ไข: แต่ละฟังก์ชันเรียกกลับในบันทึกเดิม msgแต่พารามิเตอร์นั้นมีชื่อว่า messageทั้งสามสายโยน ReferenceError: msg is not defined.
จากโค้ดด้านบน คุณจะเห็นว่ามันยิ่งซับซ้อนขึ้นเรื่อยๆ เมื่อมีการเรียกใช้ฟังก์ชันเพิ่มขึ้น — การซ้อนกันแบบนี้คือสิ่งที่เรียกว่า "callback hell"
ขั้นตอน 3) ทีนี้ลองดูพฤติกรรมเดียวกันนี้ที่เขียนด้วยตัวสร้าง (generator) ลำดับที่ได้จะอ่านได้เป็นเส้นตรงสามเส้นแทนที่จะเป็นการซ้อนกันสามระดับ
function* Messages() { console.log(yield Timedelay(1000)); console.log(yield Timedelay(2000)); console.log(yield Timedelay(3000)); }
Code คำอธิบาย
- มีการกำหนดฟังก์ชันตัวสร้างเพื่อเรียกใช้
Timedelayฟังก์ชัน yieldใช้กับTimedelayและกำหนดค่าพารามิเตอร์เป็น 1000yieldนำมาใช้อีกครั้งกับ 2000 และอีกครั้งกับ 3000
⚠️ แก้ไข: ต้นฉบับเขียนไว้ console,log(...) มีการใช้เครื่องหมายจุลภาคแทนจุดในทั้งสามบรรทัด ซึ่งเป็นข้อผิดพลาดทางไวยากรณ์และทำให้ไฟล์ไม่สามารถประมวลผลได้เลย
วิธีการเรียกใช้ Generator ด้วยฟังก์ชันนักวิ่ง
เครื่องกำเนิดไฟฟ้าข้างต้นอ่านง่าย แต่จะไม่ทำงานด้วยตัวเอง นี่คือรายละเอียดที่คำอธิบายส่วนใหญ่ละเลย และเป็นเหตุผลที่เวอร์ชันแบบเรียบง่ายอาจดูเหมือนเวทมนตร์ เครื่องกำเนิดไฟฟ้าเป็นเครื่องจักรที่หยุดชั่วคราว และต้องมีบางสิ่งบางอย่างคอยกดปุ่มอยู่เสมอ การเรียกใช้ Messages() ฟังก์ชันนี้เพียงแค่สร้างอ็อบเจ็กต์ generator แล้วส่งค่ากลับทันที โดยไม่มีตัวจับเวลาเริ่มทำงานและไม่มีการพิมพ์อะไรออกมา
ส่วนที่ขาดหายไปคือฟังก์ชันเรียกใช้งาน (runner): ฟังก์ชันขนาดเล็กที่เรียกใช้ฟังก์ชันอื่น next()รอให้กระบวนการแบบอะซิงโครนัสเสร็จสิ้น แล้วจึงเรียกใช้ next() อีกครั้งกับผลลัพธ์
// Timedelay now hands its result back to the runner function Timedelay(ptime) { return function(resume) { setTimeout(function() { resume("Pausing for " + ptime); }, ptime); }; } // The runner drives the generator to completion function run(generatorFunction) { var it = generatorFunction(); function step(value) { var result = it.next(value); if (result.done) return; // result.value is the function returned by Timedelay result.value(step); } step(); } run(Messages);
มีสามประเด็นสำคัญที่ทำให้สิ่งนี้ได้ผล ประการแรก Timedelay จะไม่รับ callback โดยตรงอีกต่อไป แต่จะส่งคืนฟังก์ชันที่ตัวรันสามารถเรียกใช้ได้ ซึ่งจะส่งคืนการควบคุมเมื่อตัวจับเวลาทำงาน ประการที่สอง it.next(value) ส่งผลลัพธ์ก่อนหน้าไปยังตัวสร้างที่หยุดชั่วคราว ซึ่งเป็นสิ่งที่ทำให้ yield เพื่อให้ปรากฏทางด้านขวาของงานที่ได้รับมอบหมาย ประการที่สาม นักวิ่งจะหยุดทันทีที่ result.done เมื่อเป็นจริงแล้ว การเรียกซ้ำก็จะสิ้นสุดลงเองตามธรรมชาติ รูปแบบการเรียกใช้งานนี้เป็นสิ่งที่ไลบรารีอย่างเช่น co ได้จัดเตรียมไว้ และเป็นกลไกที่ async/await ต่อมาจึงถูกซึมซับเข้าไปในภาษาเอง
Generators กับ async/await: ควรใช้อันไหน
Generatorก่อนหน้านี้ การใช้ s เป็นวิธีมาตรฐานในการเขียนโค้ดแบบอะซิงโครนัสแบบเรียบง่าย async/await มีอยู่จริง และทั้งสองอย่างมีแนวคิดพื้นฐานเดียวกันคือการหยุดฟังก์ชันไว้กลางคัน Node รองรับสิ่งนี้ async/await เป็นคุณสมบัติพื้นฐานตั้งแต่เวอร์ชัน 7.6 ซึ่งเปลี่ยนแปลงคำแนะนำสำหรับทุกคนที่เริ่มต้นทำงานใหม่ในวันนี้
| แง่มุม | เครื่องกำเนิดไฟฟ้า | async/รอ |
|---|---|---|
| ต้องการคนวิ่งส่งของ | ใช่ เขียนโดยคุณหรือห้องสมุด | ไม่ มันถูกสร้างขึ้นมาในภาษาอยู่แล้ว |
| คีย์เวิร์ดหยุดชั่วคราว | yield |
await |
| การจัดการข้อผิดพลาด | นักวิ่งต้องส่งต่อข้อผิดพลาด | การพยายามจับแบบธรรมดา |
| สามารถหยุดพักระหว่างทางได้ | ใช่ และสามารถกลับมาดำเนินการต่อได้ตามต้องการ | ไม่ ดำเนินการจนเสร็จสมบูรณ์ |
| เหมาะสำหรับ | ลำดับแบบขี้เกียจและสตรีมอนันต์ | งานอะซิงโครนัสทั่วไป |
ใช้ async/await สำหรับการทำงานแบบอะซิงโครนัสในโค้ดใหม่ เนื่องจากมันจะลบตัวรันเนอร์ออกและคืนค่าเป็น try และ catch แบบปกติ คงไว้ซึ่ง generator สำหรับสิ่งที่มันทำได้ดีเท่านั้น เช่น การสร้างค่าแบบไม่ทันที การวนซ้ำลำดับขนาดใหญ่หรืออนันต์ และกรณีใดก็ตามที่ผู้บริโภคเป็นผู้ตัดสินใจว่าจะคำนวณค่าถัดไปเมื่อใด
ร่วมกัน Generator ข้อผิดพลาดและวิธีแก้ไข
ปัญหาส่วนใหญ่เกี่ยวกับฟังก์ชัน Generator เกิดจากการลืมไปว่าฟังก์ชันนั้นจะไม่ทำงานจนกว่าจะมีสิ่งใดมาเรียกใช้ next()หรืออาจเกิดจากข้อผิดพลาดทางไวยากรณ์เล็กน้อยบริเวณเครื่องหมายดอกจัน อาการแต่ละอย่างด้านล่างจะระบุสาเหตุและวิธีแก้ไข
- ไม่มีอะไรเกิดขึ้นเมื่อเรียกใช้ตัวสร้าง: การเรียกฟังก์ชันนี้จะส่งคืนตัววนซ้ำ ไม่ใช่ผลลัพธ์ เรียกใช้
next()หรือส่งต่อให้คนวิ่งก็ได้ - ข้อผิดพลาดทางไวยากรณ์บน yield: เครื่องหมายดอกจันหายไป
yieldใช้ได้เฉพาะภายในfunction*. - ค่าที่ได้จาก next() คือ undefined: คนแรก
next()การเรียกไม่สามารถส่งค่าได้ เนื่องจากตัวสร้างยังไม่ถึงจุดสิ้นสุดyieldแต่. ส่งข้อมูลตั้งแต่การโทรครั้งที่สองเป็นต้นไป - ลูปอนันต์ในเกมวิ่ง:
result.doneไม่เคยมีการตรวจสอบเลย ดังนั้นnext()มีการเรียกใช้งานซ้ำๆ เกินกว่าจุดสิ้นสุด เงื่อนไข Always return when done is true เป็นจริง - ข้อผิดพลาดจะหายไปอย่างเงียบ ๆ: การปฏิเสธภายในขั้นตอนแบบอะซิงโครนัสจะไม่ถูกส่งกลับไปยังตัวสร้าง ส่งต่อด้วย
it.throw(err)เพื่อให้กลไก try-catch ที่เขียนไว้ภายในตัวสร้างโค้ดสามารถมองเห็นและจัดการกับข้อผิดพลาดได้จริง




