ปลดล็อกศักยภาพสูงสุดของ JavaScript Generators ด้วย 'yield*' คู่มือนี้จะสำรวจกลไกการมอบหมายกรณีใช้งานจริง และรูปแบบขั้นสูงเพื่อสร้างแอปพลิเคชันที่มีความเป็นโมดูล อ่านง่าย และปรับขนาดได้ เหมาะสำหรับทีมพัฒนาทั่วโลก
JavaScript Generator Delegation: การควบคุมนิพจน์ Yield อย่างเชี่ยวชาญเพื่อการพัฒนาทั่วโลก
ในภูมิทัศน์ของการพัฒนาเว็บสมัยใหม่ที่สว่างไสวและเปลี่ยนแปลงตลอดเวลา JavaScript ยังคงส่งเสริมให้นักพัฒนาสร้างสรรค์โครงสร้างที่ทรงพลังสำหรับการจัดการการดำเนินการแบบอะซิงโครนัสที่ซับซ้อน การจัดการสตรีมข้อมูลขนาดใหญ่ และการสร้างกระแสควบคุมที่ซับซ้อน ในบรรดาคุณสมบัติที่ทรงพลังเหล่านี้ Generators โดดเด่นเป็นเสาหลักสำหรับการสร้างตัวทำซ้ำ การจัดการสถานะ และการจัดการลำดับการดำเนินการที่ซับซ้อน อย่างไรก็ตาม ความสง่างามและประสิทธิภาพที่แท้จริงของ Generators มักจะปรากฏชัดที่สุดเมื่อเราเจาะลึกแนวคิดของ Generator Delegation โดยเฉพาะอย่างยิ่งผ่านการใช้ yield* expression
คู่มือฉบับสมบูรณ์นี้ออกแบบมาสำหรับนักพัฒนาทั่วโลก ตั้งแต่มืออาชีพที่มีประสบการณ์ที่ต้องการเพิ่มพูนความเข้าใจ ไปจนถึงผู้ที่ยังใหม่ต่อความซับซ้อนของ JavaScript ขั้นสูง เราจะออกเดินทางเพื่อสำรวจ Generator Delegation คลี่คลายกลไกต่างๆ แสดงการใช้งานจริง และเปิดเผยว่ามันช่วยให้การจัดองค์ประกอบและโครงสร้างแบบแยกส่วนที่มีประสิทธิภาพในโค้ดของคุณเป็นไปได้อย่างไร เมื่อสิ้นสุดบทความนี้ คุณจะไม่เพียงแต่เข้าใจ "วิธีการ" แต่ยังรวมถึง "เหตุผล" ในการใช้ yield* เพื่อสร้างแอปพลิเคชัน JavaScript ที่แข็งแกร่งขึ้น อ่านง่ายขึ้น และบำรุงรักษาได้ง่ายขึ้น โดยไม่คำนึงถึงตำแหน่งทางภูมิศาสตร์หรือภูมิหลังทางวิชาชีพของคุณ
การทำความเข้าใจ Generator Delegation นั้นเป็นมากกว่าการเรียนรู้ไวยากรณ์อีกรูปแบบหนึ่ง มันคือการยอมรับกระบวนทัศน์ที่ส่งเสริมสถาปัตยกรรมโค้ดที่สะอาดขึ้น การจัดการทรัพยากรที่ดีขึ้น และการจัดการเวิร์กโฟลว์ที่ซับซ้อนได้อย่างมีสัญชาตญาณ เป็นแนวคิดที่ข้ามประเภทโครงการเฉพาะ โดยพบประโยชน์ในทุกสิ่งตั้งแต่ตรรกะส่วนต่อประสานผู้ใช้ฝั่งหน้า ไปจนถึงการประมวลผลข้อมูลฝั่งเซิร์ฟเวอร์ และแม้แต่งานคำนวณเฉพาะทาง มาเจาะลึกและปลดล็อกศักยภาพสูงสุดของ JavaScript Generators กัน!
รากฐาน: การทำความเข้าใจ JavaScript Generators
ก่อนที่เราจะชื่นชมความซับซ้อนของ Generator Delegation ได้อย่างแท้จริง สิ่งสำคัญคือต้องมีความเข้าใจที่มั่นคงเกี่ยวกับ JavaScript Generators คืออะไร และทำงานอย่างไร Generators เปิดตัวใน ECMAScript 2015 (ES6) ให้วิธีการสร้างตัวทำซ้ำที่ทรงพลัง ช่วยให้ฟังก์ชันสามารถหยุดการทำงานชั่วคราวและดำเนินการต่อได้ในภายหลัง โดยการสร้างค่าต่างๆ เมื่อเวลาผ่านไป
Generators คืออะไร? ไวยากรณ์ function*
โดยพื้นฐานแล้ว ฟังก์ชัน Generator จะถูกกำหนดโดยใช้ไวยากรณ์ function* (สังเกตเครื่องหมายดอกจัน) เมื่อเรียกใช้ฟังก์ชัน Generator มันจะไม่ดำเนินการเนื้อหาของฟังก์ชันทันที แต่จะคืนอ็อบเจกต์พิเศษที่เรียกว่า Generator object Generator object นี้เป็นไปตามทั้งโปรโตคอล iterable และ iterator ซึ่งหมายความว่าสามารถวนซ้ำได้ (เช่น โดยใช้ลูป for...of) และมีเมธอด next()
แต่ละครั้งที่เรียกเมธอด next() บน Generator object จะทำให้ฟังก์ชัน Generator ดำเนินการต่อจนกว่าจะเจอ yield expression ค่าที่ระบุหลังจาก yield จะถูกส่งคืนเป็นคุณสมบัติ value ของอ็อบเจกต์ในรูปแบบ { value: any, done: boolean } เมื่อฟังก์ชัน Generator เสร็จสิ้น (ไม่ว่าจะโดยการถึงจุดสิ้นสุดหรือการดำเนินการ return statement) คุณสมบัติ done จะกลายเป็น true
มาดูตัวอย่างง่ายๆ เพื่อแสดงพฤติกรรมพื้นฐานนี้:
function* simpleGenerator() {
yield 'First value';
yield 'Second value';
return 'All done'; // This value will be the last 'value' property when done is true
}
const myGenerator = simpleGenerator();
console.log(myGenerator.next()); // { value: 'First value', done: false }
console.log(myGenerator.next()); // { value: 'Second value', done: false }
console.log(myGenerator.next()); // { value: 'All done', done: true }
console.log(myGenerator.next()); // { value: undefined, done: true }
ดังที่คุณสังเกตเห็น การดำเนินการของ simpleGenerator จะหยุดชั่วคราวที่แต่ละ yield statement จากนั้นจึงดำเนินการต่อเมื่อเรียก .next() ครั้งถัดไป ความสามารถพิเศษในการหยุดชั่วคราวและดำเนินการต่อคือสิ่งที่ทำให้ Generators มีความยืดหยุ่นและทรงพลังสำหรับกระบวนทัศน์การเขียนโปรแกรมต่างๆ โดยเฉพาะอย่างยิ่งเมื่อจัดการกับลำดับ การดำเนินการแบบอะซิงโครนัส หรือการจัดการสถานะ
โปรโตคอล Iterator และ Generator Objects
Generator object ใช้โปรโตคอล iterator ซึ่งหมายความว่ามีเมธอด next() ที่ส่งคืนอ็อบเจกต์ที่มีคุณสมบัติ value และ done เนื่องจากยังใช้โปรโตคอล iterable (ผ่านเมธอด [Symbol.iterator]() ที่ส่งคืน this) คุณจึงสามารถใช้มันโดยตรงกับโครงสร้างต่างๆ เช่น ลูป for...of และ spread syntax (...)
function* numberSequence() {
yield 1;
yield 2;
yield 3;
}
const sequence = numberSequence();
// Using for...of loop
for (const num of sequence) {
console.log(num); // 1, then 2, then 3
}
// Generators can also be spread into arrays
const values = [...numberSequence()];
console.log(values); // [1, 2, 3]
ความเข้าใจพื้นฐานเกี่ยวกับฟังก์ชัน Generator, คีย์เวิร์ด yield และ Generator object นี้เป็นรากฐานที่เราจะใช้สร้างความรู้เกี่ยวกับ Generator Delegation เมื่อมีพื้นฐานเหล่านี้ เราก็พร้อมที่จะสำรวจวิธีการจัดองค์ประกอบและมอบหมายการควบคุมระหว่าง Generators ต่างๆ ซึ่งนำไปสู่โครงสร้างโค้ดที่มีประสิทธิภาพและเป็นโมดูลอย่างไม่น่าเชื่อ
พลังของการมอบหมาย: yield* Expression
แม้ว่าคีย์เวิร์ด yield แบบพื้นฐานจะยอดเยี่ยมสำหรับการสร้างค่าเดี่ยวๆ แต่จะเกิดอะไรขึ้นเมื่อคุณต้องการสร้างลำดับของค่าที่ Generator อื่นรับผิดชอบอยู่แล้ว หรือบางทีคุณอาจต้องการแบ่งงานของ Generator ของคุณออกเป็น sub-Generators? นี่คือจุดที่ Generator Delegation ซึ่งเปิดใช้งานโดย yield* expression เข้ามามีบทบาท มันเป็น syntactic sugar แต่มีประสิทธิภาพอย่างยิ่ง ซึ่งช่วยให้ Generator สามารถมอบหมายการดำเนินการ yield และ return ทั้งหมดให้กับ Generator อื่นหรืออ็อบเจกต์ iterable อื่นๆ
yield* คืออะไร?
yield* expression ถูกใช้ภายในฟังก์ชัน Generator เพื่อมอบหมายการดำเนินการไปยังอ็อบเจกต์ iterable อื่น เมื่อ Generator พบ yield* someIterable มันจะหยุดการดำเนินการของตัวเองชั่วคราวและเริ่มวนซ้ำ someIterable สำหรับทุกค่าที่ yielded โดย someIterable Generator ที่มอบหมายจะ yield ค่านั้นไปเรื่อยๆ จนกว่า someIterable จะหมด (คือคุณสมบัติ done ของมันจะกลายเป็น true)
ที่สำคัญคือ เมื่อ iterable ที่มอบหมายเสร็จสิ้น ค่าส่งคืนของมัน (หากมี) จะกลายเป็นค่าของ yield* expression นั้นเองใน Generator ที่มอบหมาย ซึ่งช่วยให้การจัดองค์ประกอบและการไหลของข้อมูลเป็นไปอย่างราบรื่น ช่วยให้คุณสามารถเชื่อมโยงฟังก์ชัน Generator เข้าด้วยกันในลักษณะที่ใช้งานง่ายและมีประสิทธิภาพ
yield* ช่วยลดความซับซ้อนของการจัดองค์ประกอบได้อย่างไร
พิจารณาสถานการณ์ที่คุณมีแหล่งข้อมูลหลายแหล่ง ซึ่งแต่ละแหล่งสามารถแสดงด้วย Generator และคุณต้องการรวมเข้าเป็นสตรีมเดียวที่รวมเป็นหนึ่งเดียว หากไม่มี yield* คุณจะต้องวนซ้ำ sub-Generator แต่ละตัวด้วยตนเอง โดย yield ค่าของมันทีละค่า สิ่งนี้สามารถกลายเป็นเรื่องยุ่งยากและซ้ำซากได้อย่างรวดเร็ว โดยเฉพาะอย่างยิ่งกับชั้นของการซ้อน
yield* จะช่วยลดการวนซ้ำด้วยตนเองนี้ ทำให้โค้ดของคุณสะอาดและประกาศชัดเจนยิ่งขึ้น มันจัดการวงจรชีวิตทั้งหมดของ iterable ที่มอบหมาย รวมถึง:
- การ yield ค่าทั้งหมดที่สร้างโดย iterable ที่มอบหมาย
- การส่งผ่านอาร์กิวเมนต์ใดๆ ที่ส่งไปยังเมธอด
next()ของ Generator ที่มอบหมายไปยังเมธอดnext()ของ Generator ที่มอบหมาย - การแพร่กระจายการเรียก
throw()และreturn()จาก Generator ที่มอบหมายไปยัง Generator ที่มอบหมาย - การจับค่าส่งคืนของ Generator ที่มอบหมาย
การจัดการที่ครอบคลุมนี้ทำให้ yield* เป็นเครื่องมือที่ขาดไม่ได้สำหรับการสร้างระบบที่ใช้ Generator แบบแยกส่วนและจัดองค์ประกอบได้ ซึ่งมีประโยชน์อย่างยิ่งในโครงการขนาดใหญ่หรือเมื่อทำงานร่วมกับทีมระหว่างประเทศที่ความชัดเจนของโค้ดและการบำรุงรักษาเป็นสิ่งสำคัญยิ่ง
ความแตกต่างระหว่าง yield และ yield*
สิ่งสำคัญคือต้องแยกความแตกต่างระหว่างคีย์เวิร์ดทั้งสอง:
yield: หยุด Generator ชั่วคราวและส่งคืนค่าเดียว มันเหมือนกับการส่งสินค้าหนึ่งชิ้นออกจากสายพานลำเลียงของโรงงาน Generator เองยังคงควบคุมและเพียงแค่ออกผลผลิตหนึ่งชิ้นyield*: หยุด Generator ชั่วคราวและมอบหมายการควบคุมไปยัง iterable อื่น (มักจะเป็น Generator อื่น) มันเหมือนกับการเปลี่ยนเส้นทางผลผลิตทั้งหมดของสายพานลำเลียงไปยังหน่วยประมวลผลพิเศษอื่น และเมื่อหน่วยนั้นเสร็จสิ้น สายพานลำเลียงหลักจะดำเนินการต่อเท่านั้น Generator ที่มอบหมายจะละทิ้งการควบคุมและปล่อยให้ iterable ที่มอบหมายทำงานจนเสร็จสิ้น
มาแสดงด้วยตัวอย่างที่ชัดเจน:
function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
function* generateLetters() {
yield 'A';
yield 'B';
yield 'C';
}
function* combinedGenerator() {
console.log('Starting combined generator...');
yield* generateNumbers(); // Delegates to generateNumbers
console.log('Numbers generated, now generating letters...');
yield* generateLetters(); // Delegates to generateLetters
console.log('Letters generated, all done.');
return 'Combined sequence completed.';
}
const combined = combinedGenerator();
console.log(combined.next()); // { value: 'Starting combined generator...', done: false }
console.log(combined.next()); // { value: 1, done: false }
console.log(combined.next()); // { value: 2, done: false }
console.log(combined.next()); // { value: 3, done: false }
console.log(combined.next()); // { value: 'Numbers generated, now generating letters...', done: false }
console.log(combined.next()); // { value: 'A', done: false }
console.log(combined.next()); // { value: 'B', done: false }
console.log(combined.next()); // { value: 'C', done: false }
console.log(combined.next()); // { value: 'Letters generated, all done.', done: false }
console.log(combined.next()); // { value: 'Combined sequence completed.', done: true }
console.log(combined.next()); // { value: undefined, done: true }
ในตัวอย่างนี้ combinedGenerator ไม่ได้ yield 1, 2, 3, A, B, C อย่างชัดเจน แต่ใช้ yield* เพื่อ "แทรก" ผลลัพธ์ของ generateNumbers และ generateLetters เข้าไปในลำดับของตัวเอง กระแสการควบคุมจะถ่ายโอนระหว่าง Generators ได้อย่างราบรื่น นี่แสดงให้เห็นถึงพลังอันยิ่งใหญ่ของ yield* ในการสร้างลำดับที่ซับซ้อนจากส่วนที่เล็กและเป็นอิสระ
ความสามารถในการมอบหมายนี้มีค่าอย่างยิ่งในระบบซอฟต์แวร์ขนาดใหญ่ ช่วยให้นักพัฒนาสามารถกำหนดความรับผิดชอบที่ชัดเจนสำหรับแต่ละ Generator และรวมเข้าด้วยกันได้อย่างยืดหยุ่น ตัวอย่างเช่น ทีมหนึ่งอาจรับผิดชอบ Generator การแยกวิเคราะห์ข้อมูล ทีมอื่น Generator การตรวจสอบข้อมูล และทีมที่สาม Generator การจัดรูปแบบผลลัพธ์ จากนั้น yield* จะช่วยให้สามารถรวมส่วนประกอบเฉพาะเหล่านี้เข้าด้วยกันได้อย่างง่ายดาย ส่งเสริมความเป็นโมดูลและเร่งการพัฒนาในทุกภูมิภาคทางภูมิศาสตร์และทีมที่รับผิดชอบ
การเจาะลึกกลไก Generator Delegation
ในการใช้พลังของ yield* ได้อย่างแท้จริง เป็นประโยชน์ที่จะเข้าใจว่ามีอะไรเกิดขึ้นภายใน yield* expression ไม่ใช่แค่การวนซ้ำง่ายๆ แต่เป็นกลไกที่ซับซ้อนสำหรับการมอบหมายการโต้ตอบกับผู้เรียกของ Generator ภายนอกไปยัง iterable ภายในอย่างสมบูรณ์ ซึ่งรวมถึงการแพร่กระจายค่า ข้อผิดพลาด และสัญญาณการเสร็จสิ้น
yield* ทำงานภายในอย่างไร: มุมมองโดยละเอียด
เมื่อ Generator ที่มอบหมาย (เรียกว่า outer) พบ yield* innerIterable มันจะทำการวนซ้ำที่คล้ายกับ pseudo-code เชิงแนวคิดนี้:
function* outerGenerator() {
// ... some code ...
let resultOfInner = yield* innerGenerator(); // This is the delegation point
// ... some code that uses resultOfInner ...
}
// Conceptually, yield* behaves like:
function* outerGeneratorConceptual() {
// ...
const inner = innerGenerator(); // Get the inner generator/iterator
let nextValueFromOuter = undefined;
let nextResultFromInner;
while (true) {
// 1. Send the value/error received by outer.next() / outer.throw() to inner.
// 2. Get the result from inner.next() / inner.throw().
try {
if (hadThrownError) { // If outer.throw() was called
nextResultFromInner = inner.throw(errorFromOuter);
hadThrownError = false; // Reset flag
} else if (hadReturnedValue) { // If outer.return() was called
nextResultFromInner = inner.return(valueFromOuter);
hadReturnedValue = false; // Reset flag
} else { // Normal next() call
nextResultFromInner = inner.next(nextValueFromOuter);
}
} catch (e) {
// If inner throws an error, it propagates to outer's caller
throw e;
}
// 3. If inner is done, break the loop and use its return value.
if (nextResultFromInner.done) {
// The value of the yield* expression itself is the return value of the inner generator.
break;
}
// 4. If inner is not done, yield its value to outer's caller.
nextValueFromOuter = yield nextResultFromInner.value;
// The value received here is what was passed to outer.next(value)
}
return nextResultFromInner.value; // Return value of yield*
}
pseudo-code นี้เน้นย้ำถึงประเด็นสำคัญหลายประการ:
- การวนซ้ำผ่าน iterable อื่น:
yield*จะวนซ้ำ innerIterable อย่างมีประสิทธิภาพ โดย yield ค่าแต่ละค่าที่สร้างขึ้น - การสื่อสารสองทาง: ค่าที่ส่งเข้าไปใน Generator
outerผ่านเมธอดnext(value)จะถูกส่งต่อไปยังเมธอดnext(value)ของ Generatorinnerโดยตรง ในทำนองเดียวกัน ค่าที่ yield โดย Generatorinnerจะถูกส่งออกโดย Generatorouterสิ่งนี้สร้างช่องทางที่โปร่งใส - การแพร่กระจายข้อผิดพลาด: หากข้อผิดพลาดถูกโยนเข้าไปใน Generator
outer(ผ่านเมธอดthrow(error)) มันจะถูกแพร่กระจายไปยัง Generatorinnerทันที หาก Generatorinnerไม่จัดการกับข้อผิดพลาดนั้น ข้อผิดพลาดจะแพร่กระจายกลับไปยังผู้เรียกของ Generatorouter - การจับค่าส่งคืน: เมื่อ
innerIterableหมดลง (คือคุณสมบัติdoneของมันกลายเป็นtrue) คุณสมบัติvalueสุดท้ายของมันจะกลายเป็นผลลัพธ์ของyield*expression ทั้งหมดใน Generatorouterนี่เป็นคุณสมบัติที่สำคัญสำหรับการรวบรวมผลลัพธ์หรือรับสถานะสุดท้ายจากงานที่มอบหมาย
ตัวอย่างโดยละเอียด: แสดงการแพร่กระจาย next(), return() และ throw()
มาสร้างตัวอย่างที่ซับซ้อนยิ่งขึ้นเพื่อแสดงความสามารถในการสื่อสารทั้งหมดผ่าน yield*
function* delegatingGenerator() {
console.log('Outer: Starting delegation...');
try {
const resultFromInner = yield* delegatedGenerator(); // Delegate to delegatedGenerator
console.log(`Outer: Delegation finished. Inner returned: ${resultFromInner}`);
} catch (e) {
console.error(`Outer: Caught error from inner: ${e.message}`);
}
console.log('Outer: Resuming after delegation...');
yield 'Outer: Final value';
return 'Outer: All done!';
}
function* delegatedGenerator() {
console.log('Inner: Started.');
const dataFromOuter1 = yield 'Inner: Please provide data 1'; // Receives value from outer.next()
console.log(`Inner: Received data 1 from outer: ${dataFromOuter1}`);
try {
const dataFromOuter2 = yield 'Inner: Please provide data 2'; // Receives value from outer.next()
console.log(`Inner: Received data 2 from outer: ${dataFromOuter2}`);
if (dataFromOuter2 === 'error') {
throw new Error('Inner: Deliberate error!');
}
} catch (e) {
console.error(`Inner: Caught an error: ${e.message}`);
yield 'Inner: Recovered from error.'; // Yields a value after error handling
return 'Inner: Returning early due to error recovery';
}
yield 'Inner: Performing more work.';
return 'Inner: Task completed successfully.'; // This will be the result of yield*
}
const delegator = delegatingGenerator();
console.log('--- Initializing ---');
console.log(delegator.next()); // Outer: Starting delegation... { value: 'Inner: Please provide data 1', done: false }
console.log('--- Sending "Hello" to inner ---');
console.log(delegator.next('Hello from outer!')); // Inner: Received data 1 from outer: Hello from outer! { value: 'Inner: Please provide data 2', done: false }
console.log('--- Sending "World" to inner ---');
console.log(delegator.next('World from outer!')); // Inner: Received data 2 from outer: World from outer! { value: 'Inner: Performing more work.', done: false }
console.log('--- Continuing ---');
console.log(delegator.next()); // { value: 'Inner: Task completed successfully.', done: false }
// Outer: Delegation finished. Inner returned: Inner: Task completed successfully.
console.log(delegator.next()); // { value: 'Outer: Resuming after delegation...', done: false }
console.log(delegator.next()); // { value: 'Outer: Final value', done: false }
console.log(delegator.next()); // { value: 'Outer: All done!', done: true }
const delegatorWithError = delegatingGenerator();
console.log('\n--- Initializing (Error Scenario) ---');
console.log(delegatorWithError.next()); // Outer: Starting delegation... { value: 'Inner: Please provide data 1', done: false }
console.log('--- Sending "ErrorTrigger" to inner ---');
console.log(delegatorWithError.next('ErrorTrigger')); // Inner: Received data 1 from outer: ErrorTrigger! { value: 'Inner: Please provide data 2', done: false }
console.log('--- Sending "error" to inner to trigger error ---');
console.log(delegatorWithError.next('error'));
// Inner: Received data 2 from outer: error
// Inner: Caught an error: Inner: Deliberate error!
// { value: 'Inner: Recovered from error.', done: false } (Note: This yield comes from the inner's catch block)
console.log('--- Continuing after inner error handling ---');
console.log(delegatorWithError.next()); // { value: 'Inner: Returning early due to error recovery', done: false }
// Outer: Delegation finished. Inner returned: Inner: Returning early due to error recovery
console.log(delegatorWithError.next()); // { value: 'Outer: Resuming after delegation...', done: false }
console.log(delegatorWithError.next()); // { value: 'Outer: Final value', done: false }
console.log(delegatorWithError.next()); // { value: 'Outer: All done!', done: true }
ตัวอย่างเหล่านี้แสดงให้เห็นอย่างชัดเจนว่า yield* ทำหน้าที่เป็นช่องทางที่แข็งแกร่งสำหรับการควบคุมและข้อมูล มันรับประกันว่า Generator ที่มอบหมายไม่จำเป็นต้องทราบกลไกภายในของ Generator ที่มอบหมาย มันเพียงแค่ส่งผ่านคำขอโต้ตอบและ yield ค่าจนกว่างานที่มอบหมายจะเสร็จสมบูรณ์ กลไกการสร้างนามธรรมที่ทรงพลังนี้เป็นรากฐานสำหรับการสร้างระบบ Generator ที่แยกส่วนและบำรุงรักษาได้อย่างมาก โดยเฉพาะอย่างยิ่งเมื่อจัดการกับกระแสสถานะที่ซับซ้อนหรือสตรีมข้อมูลแบบอะซิงโครนัสที่อาจเกี่ยวข้องกับส่วนประกอบที่พัฒนาโดยทีมหรือบุคคลต่างๆ ทั่วโลก
กรณีใช้งานจริงสำหรับ Generator Delegation
ความเข้าใจเชิงทฤษฎีของ yield* ฉายแสงอย่างแท้จริงเมื่อเราสำรวจการใช้งานจริง การมอบหมาย Generator ไม่ใช่แนวคิดทางวิชาการเพียงอย่างเดียว แต่เป็นเครื่องมือที่ทรงพลังในการแก้ไขปัญหาการเขียนโปรแกรมในโลกแห่งความเป็นจริง การปรับปรุงการจัดระเบียบโค้ด และการอำนวยความสะดวกในการจัดการกระแสควบคุมที่ซับซ้อนในหลากหลายสาขา
การดำเนินการแบบอะซิงโครนัสและกระแสควบคุม
หนึ่งในการใช้งานที่เก่าแก่และมีผลกระทบมากที่สุดของ Generators และโดยการขยาย yield* คือการจัดการการดำเนินการแบบอะซิงโครนัส ก่อนการใช้งาน async/await อย่างแพร่หลาย Generators ซึ่งมักจะรวมกับฟังก์ชัน runner (เช่น ไลบรารีที่ใช้ thunk/promise อย่างง่าย) ได้มอบวิธีการที่ดูเหมือนจะซิงโครนัสในการเขียนโค้ดแบบอะซิงโครนัส แม้ว่า async/await จะเป็นไวยากรณ์ที่ต้องการสำหรับงานอะซิงโครนัสทั่วไปในปัจจุบัน แต่การทำความเข้าใจรูปแบบอะซิงโครนัสที่ใช้ Generator จะช่วยเพิ่มความชื่นชมในวิธีการแก้ไขปัญหาที่ซับซ้อน และสำหรับสถานการณ์ที่ async/await อาจไม่เหมาะสม
ตัวอย่าง: การจำลองการเรียก API แบบอะซิงโครนัสด้วยการมอบหมาย
สมมติว่าคุณต้องดึงข้อมูลผู้ใช้ และจากนั้นตาม ID ของผู้ใช้รายนั้น ให้ดึงคำสั่งซื้อของเขา ด้วย yield* คุณสามารถจัดองค์ประกอบสิ่งเหล่านี้ให้เป็นลำดับ:
// A simple "runner" function that executes a generator using Promises
// (Simplified for demonstration; real-world runners like 'co' are more robust)
function run(generatorFunc) {
const generator = generatorFunc();
function advance(value) {
const result = generator.next(value);
if (result.done) {
return Promise.resolve(result.value);
}
// For yielded values that are Promises, wait for them to resolve
// If a Promise rejects, throw the error into the generator
return Promise.resolve(result.value).then(advance, err => generator.throw(err));
}
return advance();
}
// Mock asynchronous functions
const fetchUser = (id) => new Promise(resolve => {
setTimeout(() => {
console.log(`API: Fetching user ${id}...`);
resolve({ id: id, name: `User ${id}`, email: `user${id}@example.com` });
}, 500);
});
const fetchUserOrders = (userId) => new Promise(resolve => {
setTimeout(() => {
console.log(`API: Fetching orders for user ${userId}...`);
resolve([{ orderId: `O${userId}-001`, amount: 120 }, { orderId: `O${userId}-002`, amount: 250 }]);
}, 700);
});
// Delegated generator for fetching user details
function* getUserDetails(userId) {
console.log(`Delegate: Fetching user ${userId} details...`);
const user = yield fetchUser(userId); // Yields a Promise, which the runner handles
console.log(`Delegate: User ${userId} details fetched.`);
return user;
}
// Delegated generator for fetching user's orders
function* getUserOrderHistory(user) {
console.log(`Delegate: Fetching orders for ${user.name}...`);
const orders = yield fetchUserOrders(user.id); // Yields a Promise
console.log(`Delegate: Orders for ${user.name} fetched.`);
return orders;
}
// Main orchestrating generator using delegation
function* getUserData(userId) {
console.log(`Orchestrator: Starting data retrieval for user ${userId}.`);
const user = yield* getUserDetails(userId); // Delegate to get user details
const orders = yield* getUserOrderHistory(user); // Delegate to get user orders
console.log(`Orchestrator: All data for user ${userId} retrieved.`);
return { user, orders };
}
run(function* () {
try {
// The yield* here delegates execution to getUserData
// The result of getUserData (which includes its returned value) is captured
const data = yield* getUserData(123);
console.log('\nFinal Result:');
console.log(JSON.stringify(data, null, 2));
} catch (error) {
console.error('An error occurred:', error);
}
});
/* Expected output (timing dependent due to setTimeout):
Orchestrator: Starting data retrieval for user 123.
Delegate: Fetching user 123 details...
API: Fetching user 123...
Delegate: User 123 details fetched.
Delegate: Fetching orders for User 123...
API: Fetching orders for user 123...
Delegate: Orders for User 123 fetched.
Orchestrator: All data for user 123 retrieved.
Final Result:
{
"user": {
"id": 123,
"name": "User 123",
"email": "user123@example.com"
},
"orders": [
{
"orderId": "O123-001",
"amount": 120
},
{
"orderId": "O123-002",
"amount": 250
}
]
}
*/
ตัวอย่างนี้แสดงให้เห็นว่า yield* ช่วยให้คุณสามารถจัดองค์ประกอบขั้นตอนแบบอะซิงโครนัส ทำให้กระแสการทำงานดูเหมือนเป็นเชิงเส้นและซิงโครนัสภายใน Generator ได้อย่างไรแต่ละ Generator ที่มอบหมายจะจัดการกับงานย่อยที่เฉพาะเจาะจง (การดึงผู้ใช้ การดึงคำสั่งซื้อ) ส่งเสริมความเป็นโมดูล รูปแบบนี้ได้รับความนิยมอย่างสูงจากไลบรารีอย่าง Co ซึ่งแสดงให้เห็นถึงวิสัยทัศน์ของความสามารถของ Generator มาก่อนที่ไวยากรณ์ async/await ดั้งเดิมจะแพร่หลาย
การแยกวิเคราะห์โครงสร้างข้อมูลที่ซับซ้อน
Generators เหมาะสำหรับการแยกวิเคราะห์หรือประมวลผลสตรีมข้อมูลแบบ lazy ซึ่งหมายความว่ามันจะประมวลผลข้อมูลเฉพาะเมื่อจำเป็นเท่านั้น เมื่อแยกวิเคราะห์รูปแบบข้อมูลแบบลำดับชั้นที่ซับซ้อน หรือสตรีมเหตุการณ์ คุณสามารถมอบหมายส่วนต่างๆ ของตรรกะการแยกวิเคราะห์ให้กับ sub-Generators ที่เชี่ยวชาญเฉพาะทางได้
ตัวอย่าง: การแยกวิเคราะห์สตรีมภาษา Markup ที่ทำให้ง่ายขึ้น
ลองนึกถึงสตรีมของโทเค็นจากตัวแยกวิเคราะห์สำหรับภาษา Markup ที่กำหนดเอง คุณอาจมี Generator สำหรับย่อหน้า Generator สำหรับรายการ และ Generator หลักที่มอบหมายให้กับสิ่งเหล่านี้ตามประเภทของโทเค็น
function* parseParagraph(tokens) {
let content = '';
let token = tokens.next();
while (!token.done && token.value.type !== 'END_PARAGRAPH') {
content += token.value.data + ' ';
token = tokens.next();
}
return { type: 'paragraph', content: content.trim() };
}
function* parseListItem(tokens) {
let itemContent = '';
let token = tokens.next();
while (!token.done && token.value.type !== 'END_LIST_ITEM') {
itemContent += token.value.data + ' ';
token = tokens.next();
}
return { type: 'listItem', content: itemContent.trim() };
}
function* parseList(tokens) {
const items = [];
let token = tokens.next(); // Consume START_LIST
while (!token.done && token.value.type !== 'END_LIST') {
if (token.value.type === 'START_LIST_ITEM') {
// Delegate to parseListItem, passing the remaining tokens as an iterable
items.push(yield* parseListItem(tokens));
} else {
// Handle unexpected token or advance
}
token = tokens.next();
}
return { type: 'list', items: items };
}
function* documentParser(tokenStream) {
const elements = [];
// Directly iterate over the tokenStream iterator
for (const token of tokenStream) {
if (token.type === 'START_PARAGRAPH') {
// Delegate to parseParagraph with the same tokenStream iterator
elements.push(yield* parseParagraph(tokenStream));
} else if (token.type === 'START_LIST') {
// Delegate to parseList with the same tokenStream iterator
elements.push(yield* parseList(tokenStream));
} else if (token.type === 'TEXT') {
// Handle top-level text if needed, or error
elements.push({ type: 'text', content: token.data });
}
// Ignore other control tokens that are handled by delegates, or error
}
return { type: 'document', elements: elements };
}
// Simulate a token stream iterator
const tokenStreamData = [
{ type: 'START_PARAGRAPH' },
{ type: 'TEXT', data: 'This is the first paragraph.' },
{ type: 'END_PARAGRAPH' },
{ type: 'TEXT', data: 'Some introductory text.'},
{ type: 'START_LIST' },
{ type: 'START_LIST_ITEM' },
{ type: 'TEXT', data: 'First item.' },
{ type: 'END_LIST_ITEM' },
{ type: 'START_LIST_ITEM' },
{ type: 'TEXT', data: 'Second item.' },
{ type: 'END_LIST_ITEM' },
{ type: 'END_LIST' },
{ type: 'START_PARAGRAPH' },
{ type: 'TEXT', data: 'Another paragraph.' },
{ type: 'END_PARAGRAPH' },
];
const parser = documentParser(tokenStreamData[Symbol.iterator]());
const parsedDocument = [...parser]; // Run the generator to completion
console.log('\nParsed Document Structure:');
console.log(JSON.stringify(parsedDocument, null, 2));
/* Expected output:
Parsed Document Structure:
[
{
"type": "paragraph",
"content": "This is the first paragraph."
},
{
"type": "text",
"content": "Some introductory text."
},
{
"type": "list",
"items": [
{
"type": "listItem",
"content": "First item."
},
{
"type": "listItem",
"content": "Second item."
}
]
},
{
"type": "paragraph",
"content": "Another paragraph."
}
]
*/
ในตัวอย่างที่แข็งแกร่งนี้ documentParser มอบหมายให้กับ parseParagraph และ parseList ที่สำคัญคือ parseList มอบหมายให้กับ parseListItem อีกที สังเกตว่าสตรีมโทเค็น (ตัวทำซ้ำ) ถูกส่งลงไป และแต่ละ Generator ที่มอบหมายจะใช้เฉพาะโทเค็นที่ต้องการเท่านั้น โดยส่งคืนส่วนที่แยกวิเคราะห์ได้ วิธีการแบบแยกส่วนนี้ทำให้ตัวแยกวิเคราะห์ง่ายต่อการขยาย แก้ไขข้อบกพร่อง และบำรุงรักษา ซึ่งเป็นข้อได้เปรียบที่สำคัญสำหรับทีมที่ทำงานทั่วโลกเกี่ยวกับไปป์ไลน์การประมวลผลข้อมูลที่ซับซ้อน
สตรีมข้อมูลอนันต์และการประเมินแบบ Lazy
Generators เหมาะสำหรับการแสดงลำดับที่อาจเป็นอนันต์หรือมีค่าใช้จ่ายในการคำนวณสูงในการสร้างทั้งหมด การมอบหมายช่วยให้คุณสามารถจัดองค์ประกอบลำดับดังกล่าวได้อย่างมีประสิทธิภาพ
ตัวอย่าง: การจัดองค์ประกอบลำดับอนันต์
function* naturalNumbers() {
let i = 1;
while (true) {
yield i++;
}
}
function* evenNumbers() {
// Iterate over the naturalNumbers generator using for...of
for (const num of naturalNumbers()) {
if (num % 2 === 0) {
yield num;
}
}
}
function* oddNumbers() {
// Iterate over the naturalNumbers generator using for...of
for (const num of naturalNumbers()) {
if (num % 2 !== 0) {
yield num;
}
}
}
function* mixedSequence(count) {
let i = 0;
const evens = evenNumbers();
const odds = oddNumbers();
while (i < count) {
// Yield an even number
yield evens.next().value;
i++;
if (i < count) { // Ensure we don't yield extra if count is odd
// Yield an odd number
yield odds.next().value;
i++;
}
}
}
function* compositeSequence(limit) {
console.log('Composite: Yielding first 3 even numbers...');
let evens = evenNumbers();
for (let i = 0; i < 3; i++) {
yield evens.next().value;
}
console.log('Composite: Now delegating to a mixed sequence for 4 items...');
// The yield* expression itself evaluates to the return value of the delegated generator.
// Here, mixedSequence doesn't have an explicit return, so it will be undefined.
yield* mixedSequence(4);
console.log('Composite: Finally, yielding a few more natural numbers...');
let naturals = naturalNumbers();
for (let i = 0; i < 2; i++) {
yield naturals.next().value;
}
return 'Composite sequence generation complete.';
}
const seq = compositeSequence();
console.log('--- Running composite sequence ---');
console.log(seq.next()); // Composite: Yielding first 3 even numbers... { value: 2, done: false }
console.log(seq.next()); // { value: 4, done: false }
console.log(seq.next()); // { value: 6, done: false }
console.log(seq.next()); // Composite: Now delegating to a mixed sequence for 4 items... { value: 2, done: false } (from mixedSequence)
console.log(seq.next()); // { value: 1, done: false } (from mixedSequence)
console.log(seq.next()); // { value: 4, done: false } (from mixedSequence)
console.log(seq.next()); // { value: 3, done: false } (from mixedSequence)
console.log(seq.next()); // Composite: Finally, yielding a few more natural numbers... { value: 1, done: false }
console.log(seq.next()); // { value: 2, done: false }
console.log(seq.next()); // { value: 'Composite sequence generation complete.', done: true }
สิ่งนี้แสดงให้เห็นว่า yield* สามารถถักทอเข้าด้วยกันได้อย่างสง่างามลำดับอนันต์ที่แตกต่างกัน โดยนำค่าจากแต่ละลำดับตามที่จำเป็น โดยไม่ต้องสร้างลำดับทั้งหมดลงในหน่วยความจำ การประเมินแบบ lazy นี้เป็นรากฐานของการประมวลผลข้อมูลที่มีประสิทธิภาพ โดยเฉพาะอย่างยิ่งในสภาพแวดล้อมที่มีทรัพยากรจำกัด หรือเมื่อจัดการกับสตรีมข้อมูลที่ไม่มีที่สิ้นสุด นักพัฒนาในสาขาต่างๆ เช่น การคำนวณทางวิทยาศาสตร์ การสร้างแบบจำลองทางการเงิน หรือการวิเคราะห์ข้อมูลแบบเรียลไทม์ ซึ่งมักจะกระจายอยู่ทั่วโลก พบว่ารูปแบบนี้มีประโยชน์อย่างยิ่งในการจัดการหน่วยความจำและภาระงานในการคำนวณ
เครื่องสถานะและจัดการเหตุการณ์
Generators สามารถสร้างเครื่องสถานะได้อย่างเป็นธรรมชาติ เนื่องจากสามารถหยุดการดำเนินการชั่วคราวและดำเนินการต่อได้ในจุดที่เฉพาะเจาะจง ซึ่งสอดคล้องกับสถานะต่างๆ การมอบหมายช่วยให้สามารถสร้างเครื่องสถานะแบบลำดับชั้นหรือแบบซ้อนได้
ตัวอย่าง: กระแสปฏิสัมพันธ์ของผู้ใช้
พิจารณาฟอร์มแบบหลายขั้นตอน หรือวิซาร์ดแบบโต้ตอบที่แต่ละขั้นตอนสามารถเป็น sub-generator ได้
function* loginProcess() {
console.log('Login: Starting login process.');
const username = yield 'LOGIN: Enter username';
const password = yield 'LOGIN: Enter password';
console.log(`Login: Authenticating ${username}...`);
// Simulate async auth
yield new Promise(res => setTimeout(() => res(), 200));
if (username === 'admin' && password === 'pass') {
return { status: 'success', user: username };
}
else {
throw new Error('Invalid credentials');
}
}
function* profileSetupProcess(user) {
console.log(`Profile: Starting setup for ${user}.`);
const profileName = yield 'PROFILE: Enter profile name';
const avatarUrl = yield 'PROFILE: Enter avatar URL';
console.log('Profile: Saving profile data...');
yield new Promise(res => setTimeout(() => res(), 300));
return { profileName, avatarUrl };
}
function* applicationFlow() {
console.log('App: Application flow initiated.');
let userSession;
try {
userSession = yield* loginProcess(); // Delegate to login
console.log(`App: Login successful for ${userSession.user}.`);
} catch (e) {
console.error(`App: Login failed: ${e.message}`);
yield 'App: Please try again.';
return 'Failed to log in.'; // Exit application flow
}
const profileData = yield* profileSetupProcess(userSession.user); // Delegate to profile setup
console.log('App: Profile setup complete.');
yield `App: Welcome, ${profileData.profileName}! Your avatar is at ${profileData.avatarUrl}.`;
return 'Application ready.';
}
const app = applicationFlow();
console.log('--- Step 1: Init ---');
console.log(app.next()); // App: Application flow initiated. { value: 'LOGIN: Enter username', done: false }
console.log('--- Step 2: Provide username ---');
console.log(app.next('admin')); // Login: Starting login process. { value: 'LOGIN: Enter password', done: false }
console.log('--- Step 3: Provide password (correct) ---');
console.log(app.next('pass')); // Login: Authenticating admin... { value: Promise, done: false } (from simulated async)
// After the promise resolves, the next yield from profileSetupProcess will be returned
console.log(app.next()); // App: Login successful for admin. { value: 'PROFILE: Enter profile name', done: false }
console.log('--- Step 4: Provide profile name ---');
console.log(app.next('GlobalDev')); // Profile: Starting setup for admin. { value: 'PROFILE: Enter avatar URL', done: false }
console.log('--- Step 5: Provide avatar URL ---');
console.log(app.next('https://example.com/avatar.jpg')); // Profile: Saving profile data... { value: Promise, done: false }
console.log(app.next()); // App: Profile setup complete. { value: 'App: Welcome, GlobalDev! Your avatar is at https://example.com/avatar.jpg.', done: false }
console.log(app.next()); // { value: 'Application ready.', done: true }
// --- Error scenario ---
const appWithError = applicationFlow();
console.log('\n--- Error Scenario: Init ---');
// Simulate the sequence of calls that leads to an error
let result;
result = appWithError.next(); // App: Application flow initiated. { value: 'LOGIN: Enter username', done: false }
result = appWithError.next('baduser'); // Login: Starting login process. { value: 'LOGIN: Enter password', done: false }
result = appWithError.next('wrongpass'); // Login: Authenticating baduser... { value: Promise, done: false } (simulated async)
result = appWithError.next(); // Trigger the error in loginProcess, caught by applicationFlow's try/catch
// App: Login failed: Invalid credentials { value: 'App: Please try again.', done: false }
result = appWithError.next(); // Continues after error handling
// { value: 'Failed to log in.', done: true }
console.log(`Final error result: ${JSON.stringify(result)}`);
ที่นี่ applicationFlow generator มอบหมายให้กับ loginProcess และ profileSetupProcess แต่ละ sub-generator จัดการส่วนต่างๆ ของเส้นทางผู้ใช้ หาก loginProcess ล้มเหลว applicationFlow สามารถจับข้อผิดพลาดและตอบสนองได้อย่างเหมาะสมโดยไม่จำเป็นต้องทราบขั้นตอนภายในของ loginProcess ซึ่งมีค่าอย่างยิ่งสำหรับการสร้างส่วนต่อประสานผู้ใช้ที่ซับซ้อน ระบบธุรกรรม หรือเครื่องมือบรรทัดคำสั่งแบบโต้ตอบที่ต้องการการควบคุมที่แม่นยำเหนืออินพุตของผู้ใช้และสถานะแอปพลิเคชัน ซึ่งมักจะจัดการโดยนักพัฒนาที่แตกต่างกันในโครงสร้างทีมแบบกระจาย
การสร้าง Iterator ที่กำหนดเอง
Generators โดยธรรมชาติให้วิธีการที่ตรงไปตรงมาในการสร้างตัวทำซ้ำที่กำหนดเอง เมื่อตัวทำซ้ำเหล่านี้ต้องรวมข้อมูลจากแหล่งต่างๆ หรือใช้ขั้นตอนการแปลงหลายขั้นตอน yield* จะอำนวยความสะดวกในการจัดองค์ประกอบ
ตัวอย่าง: การรวมและกรองแหล่งข้อมูล
function* filterEven(source) {
for (const item of source) {
if (typeof item === 'number' && item % 2 === 0) {
yield item;
}
}
}
function* addPrefix(source, prefix) {
for (const item of source) {
yield `${prefix}${item}`;
}
}
function* mergeAndProcess(source1, source2, prefix) {
console.log('Processing first source (filtering evens)...');
yield* filterEven(source1); // Delegate to filter even numbers from source1
console.log('Processing second source (adding prefix)...');
yield* addPrefix(source2, prefix); // Delegate to add prefix to source2 items
return 'Merged and processed all sources.';
}
const dataStream1 = [1, 2, 3, 4, 5, 6];
const dataStream2 = ['alpha', 'beta', 'gamma'];
const processedData = mergeAndProcess(dataStream1, dataStream2, 'ID-');
console.log('\n--- Merged and Processed Output ---');
for (const item of processedData) {
console.log(item);
}
// Expected output:
// Processing first source (filtering evens)...
// 2
// 4
// 6
// Processing second source (adding prefix)...
// ID-alpha
// ID-beta
// ID-gamma
ตัวอย่างนี้แสดงให้เห็นว่า yield* สามารถจัดองค์ประกอบขั้นตอนการประมวลผลข้อมูลที่แตกต่างกันได้อย่างไรแต่ละ Generator ที่มอบหมายมีความรับผิดชอบเดียว (การกรอง การเพิ่มคำนำหน้า) และ Generator หลัก mergeAndProcess จะจัดการขั้นตอนเหล่านี้ รูปแบบนี้ช่วยเพิ่มความสามารถในการนำกลับมาใช้ใหม่และทดสอบตรรกะการประมวลผลข้อมูลของคุณได้อย่างมาก ซึ่งมีความสำคัญอย่างยิ่งในระบบที่จัดการรูปแบบข้อมูลที่หลากหลาย หรือต้องการไปป์ไลน์การแปลงที่ยืดหยุ่น ซึ่งมักใช้ในการวิเคราะห์ข้อมูลขนาดใหญ่ หรือกระบวนการ ETL (Extract, Transform, Load) ที่ใช้โดยองค์กรทั่วโลก
ตัวอย่างที่ใช้งานได้จริงเหล่านี้แสดงให้เห็นถึงความหลากหลายและพลังของ Generator Delegation ด้วยการช่วยให้คุณแบ่งงานที่ซับซ้อนออกเป็น Generator Functions ย่อยที่เล็ก จัดการได้ และจัดองค์ประกอบได้ yield* ช่วยให้การสร้างโค้ดที่มีความเป็นโมดูลสูง อ่านง่าย และบำรุงรักษาได้ ซึ่งเป็นคุณสมบัติที่มีคุณค่าสากลในวิศวกรรมซอฟต์แวร์ โดยไม่คำนึงถึงพรมแดนทางภูมิศาสตร์หรือโครงสร้างทีม ทำให้เป็นรูปแบบที่มีคุณค่าสำหรับนักพัฒนา JavaScript มืออาชีพทุกคน
รูปแบบขั้นสูงและข้อควรพิจารณา
นอกเหนือจากกรณีใช้งานพื้นฐานแล้ว การทำความเข้าใจแง่มุมขั้นสูงบางประการของการมอบหมาย Generator สามารถปลดล็อกศักยภาพของมันได้มากขึ้น ช่วยให้คุณจัดการกับสถานการณ์ที่ซับซ้อนยิ่งขึ้น และทำการตัดสินใจในการออกแบบอย่างรอบคอบ
การจัดการข้อผิดพลาดใน Generator ที่มอบหมาย
หนึ่งในคุณสมบัติที่แข็งแกร่งที่สุดของการมอบหมาย Generator คือวิธีการแพร่กระจายข้อผิดพลาดที่ราบรื่น หากมีการโยนข้อผิดพลาดภายใน Generator ที่มอบหมาย มันจะ "ฟองขึ้น" ไปยัง Generator ที่มอบหมาย ซึ่งสามารถจับได้โดยใช้บล็อก try...catch มาตรฐาน หาก Generator ที่มอบหมายไม่ได้จับข้อผิดพลาดนั้น ข้อผิดพลาดจะยังคงแพร่กระจายไปยังผู้เรียกของมัน และอื่นๆ จนกว่าจะได้รับการจัดการ หรือทำให้เกิดข้อยกเว้นที่ไม่ได้จัดการ
พฤติกรรมนี้มีความสำคัญอย่างยิ่งต่อการสร้างระบบที่ยืดหยุ่น เนื่องจากเป็นการรวมการจัดการข้อผิดพลาด และป้องกันความล้มเหลวในส่วนหนึ่งของสายการมอบหมายจากการทำให้แอปพลิเคชันทั้งหมดล้มเหลวโดยไม่มีโอกาสในการกู้คืน
ตัวอย่าง: การแพร่กระจายและจัดการข้อผิดพลาด
function* dataValidator() {
console.log('Validator: Starting validation.');
const data = yield 'VALIDATOR: Provide data to validate';
if (data === null || typeof data === 'undefined') {
throw new Error('Validator: Data cannot be null or undefined!');
}
if (typeof data !== 'string') {
throw new TypeError('Validator: Data must be a string!');
}
console.log(`Validator: Data "${data}" is valid.`);
return true;
}
function* dataProcessor() {
console.log('Processor: Starting processing.');
try {
const isValid = yield* dataValidator(); // Delegate to validator
if (isValid) {
const processed = `Processed: ${yield 'PROCESSOR: Provide value for processing'}`;
console.log(`Processor: Successfully processed: ${processed}`);
return processed;
}
} catch (e) {
console.error(`Processor: Caught error from validator: ${e.message}`);
yield 'PROCESSOR: Error detected, attempting recovery or fallback.';
return 'Processing failed due to validation error.'; // Return a fallback message
}
}
function* mainApplicationFlow() {
console.log('App: Starting application flow.');
try {
const finalResult = yield* dataProcessor(); // Delegate to processor
console.log(`App: Final application result: ${finalResult}`);
return finalResult;
} catch (e) {
console.error(`App: Unhandled error in application flow: ${e.message}`);
return 'Application terminated with an unhandled error.';
}
}
const appFlow = mainApplicationFlow();
console.log('--- Scenario 1: Valid data ---');
console.log(appFlow.next()); // App: Starting application flow. { value: 'VALIDATOR: Provide data to validate', done: false }
console.log(appFlow.next('some string data')); // Validator: Starting validation. { value: 'PROCESSOR: Provide value for processing', done: false }
// Validator: Data "some string data" is valid.
console.log(appFlow.next('final piece')); // Processor: Starting processing. { value: 'Processed: final piece', done: false }
// Processor: Successfully processed: Processed: final piece
console.log(appFlow.next()); // App: Final application result: Processed: final piece { value: 'Processed: final piece', done: true }
const appFlowWithError = mainApplicationFlow();
console.log('\n--- Scenario 2: Invalid data (null) ---');
console.log(appFlowWithError.next()); // App: Starting application flow. { value: 'VALIDATOR: Provide data to validate', done: false }
console.log(appFlowWithError.next(null)); // Validator: Starting validation.
// Processor: Caught error from validator: Validator: Data cannot be null or undefined!
// { value: 'PROCESSOR: Error detected, attempting recovery or fallback.', done: false }
console.log(appFlowWithError.next()); // { value: 'Processing failed due to validation error.', done: false }
// App: Final application result: Processing failed due to validation error.
console.log(appFlowWithError.next()); // { value: 'Processing failed due to validation error.', done: true }
ตัวอย่างนี้แสดงให้เห็นถึงพลังของการจับ try...catch ภายใน Generator ที่มอบหมายอย่างชัดเจน dataProcessor จับข้อผิดพลาดที่โยนโดย dataValidator จัดการกับมันอย่างสง่างาม และ yield ข้อความกู้คืนก่อนที่จะส่งคืนค่าสำรอง mainApplicationFlow ได้รับค่าสำรองนี้ โดยถือว่าเป็นค่าส่งคืนปกติ ซึ่งแสดงให้เห็นว่าการมอบหมายช่วยให้สามารถจัดการข้อผิดพลาดแบบซ้อนที่แข็งแกร่งได้อย่างไร
การส่งคืนค่าจาก Generator ที่มอบหมาย
ดังที่กล่าวไว้ก่อนหน้านี้ คุณสมบัติที่สำคัญของ yield* คือการที่ expression นั้นประเมินเป็น ค่าส่งคืน ของ Generator (หรือ iterable) ที่มอบหมาย สิ่งนี้สำคัญสำหรับงานที่ sub-Generator ทำการคำนวณหรือรวบรวมข้อมูล แล้วส่งคืนผลลัพธ์สุดท้ายกลับไปยังผู้เรียก
ตัวอย่าง: การรวบรวมผลลัพธ์
function* sumRange(start, end) {
let sum = 0;
for (let i = start; i <= end; i++) {
yield i; // Optionally yield intermediate values
sum += i;
}
return sum; // This will be the value of the yield* expression
}
function* calculateAverages() {
console.log('Calculating average of first range...');
const sum1 = yield* sumRange(1, 5); // sum1 will be 15
const count1 = 5;
const avg1 = sum1 / count1;
yield `Average of 1-5: ${avg1}`;
console.log('Calculating average of second range...');
const sum2 = yield* sumRange(6, 10); // sum2 will be 40
const count2 = 5;
const avg2 = sum2 / count2;
yield `Average of 6-10: ${avg2}`;
return { totalSum: sum1 + sum2, overallAverage: (sum1 + sum2) / (count1 + count2) };
}
const calculator = calculateAverages();
console.log('--- Running average calculations ---');
// The yield* sumRange(1,5) yields its individual numbers first
console.log(calculator.next()); // { value: 1, done: false }
console.log(calculator.next()); // { value: 2, done: false }
console.log(calculator.next()); // { value: 3, done: false }
console.log(calculator.next()); // { value: 4, done: false }
console.log(calculator.next()); // { value: 5, done: false }
// Then calculateAverages resumes and yields its own value
console.log(calculator.next()); // Calculating average of first range... { value: 'Average of 1-5: 3', done: false }
// Now yield* sumRange(6,10) yields its individual numbers
console.log(calculator.next()); // Calculating average of second range... { value: 6, done: false }
console.log(calculator.next()); // { value: 7, done: false }
console.log(calculator.next()); // { value: 8, done: false }
console.log(calculator.next()); // { value: 9, done: false }
console.log(calculator.next()); // { value: 10, done: false }
// Then calculateAverages resumes and yields its own value
console.log(calculator.next()); // { value: 'Average of 6-10: 8', done: false }
// Finally, calculateAverages returns its aggregated result
const finalResult = calculator.next();
console.log(`Final result of calculations: ${JSON.stringify(finalResult.value)}`); // { value: { totalSum: 55, overallAverage: 5.5 }, done: true }
กลไกนี้ช่วยให้การคำนวณที่มีโครงสร้างสูง โดยที่ sub-Generators รับผิดชอบการคำนวณเฉพาะ และส่งผลลัพธ์ขึ้นไปตามสายการมอบหมาย สิ่งนี้ส่งเสริมการแยกความกังวลที่ชัดเจน โดยแต่ละ Generator จะมุ่งเน้นไปที่งานเดียว และผลลัพธ์ของพวกมันจะถูกรวบรวมหรือแปลงโดยผู้จัดลำดับที่ระดับสูงกว่า ซึ่งเป็นรูปแบบทั่วไปในสถาปัตยกรรม การประมวลผลข้อมูลที่ซับซ้อนทั่วโลก
การสื่อสารสองทางกับ Generator ที่มอบหมาย
ดังที่แสดงในตัวอย่างก่อนหน้านี้ yield* ให้ช่องทางการสื่อสารสองทาง ค่าที่ส่งเข้าไปในเมธอด next(value) ของ Generator ที่มอบหมายจะถูกส่งต่อไปยังเมธอด next(value) ของ Generator ที่มอบหมายอย่างโปร่งใส สิ่งนี้ช่วยให้รูปแบบการโต้ตอบที่สมบูรณ์ซึ่งผู้เรียกของ Generator หลักสามารถมีอิทธิพลต่อพฤติกรรมหรือให้ข้อมูลแก่ Generator ที่มอบหมายที่ซ้อนอยู่ลึกๆ ได้
ความสามารถนี้มีประโยชน์อย่างยิ่งสำหรับการใช้งานแบบโต้ตอบ เครื่องมือแก้ไขข้อบกพร่อง หรือระบบที่เหตุการณ์ภายนอกจำเป็นต้องเปลี่ยนแปลงกระแสลำดับ Generator ที่ทำงานยาวนานแบบไดนามิก
ผลกระทบด้านประสิทธิภาพ
แม้ว่า Generators และการมอบหมายจะให้ประโยชน์อย่างมากในแง่ของโครงสร้างโค้ดและกระแสควบคุม แต่สิ่งสำคัญคือต้องพิจารณาประสิทธิภาพ
- Overhead: การสร้างและจัดการ Generator object มี overhead เล็กน้อยเมื่อเทียบกับการเรียกฟังก์ชันแบบธรรมดา สำหรับลูปที่สำคัญต่อประสิทธิภาพอย่างยิ่งยวดที่มีการวนซ้ำนับล้านรอบ โดยที่ทุกไมโครวินาทีมีความสำคัญ ลูป
forแบบดั้งเดิมอาจยังคงเร็วกว่าเล็กน้อย - หน่วยความจำ: Generators มีประสิทธิภาพด้านหน่วยความจำเนื่องจากสร้างค่าแบบ lazy พวกเขาไม่ได้สร้างลำดับทั้งหมดลงในหน่วยความจำ เว้นแต่จะถูกบริโภคและรวบรวมเป็นอาร์เรย์อย่างชัดเจน นี่เป็นข้อได้เปรียบอย่างมากสำหรับลำดับอนันต์หรือชุดข้อมูลขนาดใหญ่
- ความสามารถในการอ่านและบำรุงรักษา: ประโยชน์หลักของ
yield*มักจะอยู่ที่ความสามารถในการอ่านโค้ด ความเป็นโมดูล และการบำรุงรักษาที่เพิ่มขึ้น สำหรับแอปพลิเคชันส่วนใหญ่ overhead ด้านประสิทธิภาพนั้นน้อยมากเมื่อเทียบกับประโยชน์ในด้านผลิตภาพของนักพัฒนาและคุณภาพของโค้ด โดยเฉพาะอย่างยิ่งสำหรับตรรกะที่ซับซ้อนซึ่งการจัดการจะยากหากไม่ใช้yield*
ดังนั้น การตัดสินใจใช้ Generator delegation ควรกำหนดโดยการสร้างสมดุลของปัจจัยเหล่านี้ โดยให้ความสำคัญกับความชัดเจนและการบำรุงรักษาสำหรับกระแสที่ซับซ้อน เว้นแต่การทำโปรไฟล์จะเผยให้เห็นคอขวดด้านประสิทธิภาพเฉพาะ
เปรียบเทียบกับ async/await
เป็นธรรมชาติที่จะเปรียบเทียบ Generators และ yield* กับ async/await โดยเฉพาะอย่างยิ่งเนื่องจากทั้งสองวิธีให้วิธีการเขียนโค้ดแบบอะซิงโครนัสที่ดูเหมือนซิงโครนัส
async/await:- วัตถุประสงค์: ออกแบบมาเป็นหลักสำหรับการจัดการการดำเนินการแบบอะซิงโครนัสที่ใช้ Promise เป็นรูปแบบ syntactic sugar ของ Generator ที่ปรับให้เหมาะสมสำหรับ Promises
- ความเรียบง่าย: โดยทั่วไปจะง่ายกว่าสำหรับรูปแบบอะซิงโครนัสทั่วไป (เช่น การดึงข้อมูล การดำเนินการตามลำดับ)
- ข้อจำกัด: เชื่อมโยงอย่างแน่นหนา กับ Promises ไม่สามารถ
yieldค่าใดๆ หรือวนซ้ำ iterable แบบซิงโครนัสได้โดยตรงในลักษณะเดียวกัน ไม่มีช่องทางการสื่อสารสองทางโดยตรงกับnext(value)ที่เทียบเท่าสำหรับวัตถุประสงค์ทั่วไป
- Generators &
yield*:- วัตถุประสงค์: กลไกกระแสควบคุมและสร้าง iterator ทั่วไป สามารถ
yieldค่าใดๆ (Promises, objects, numbers, etc.) และมอบหมายให้กับ iterable ใดๆ - ความยืดหยุ่น: ยืดหยุ่นกว่ามาก สามารถใช้สำหรับการประเมินแบบ lazy แบบซิงโครนัส เครื่องสถานะแบบกำหนดเอง การแยกวิเคราะห์ที่ซับซ้อน และการสร้างกลไกอะซิงโครนัสแบบกำหนดเอง (ดังที่เห็นกับฟังก์ชัน
run) - ความซับซ้อน: อาจจะยาวกว่าสำหรับงานอะซิงโครนัสแบบง่ายๆ กว่า
async/awaitต้องใช้ "runner" หรือการเรียกnext()อย่างชัดเจนสำหรับการดำเนินการ
- วัตถุประสงค์: กลไกกระแสควบคุมและสร้าง iterator ทั่วไป สามารถ
โดยเนื้อแท้แล้ว async/await เหมาะสำหรับเวิร์กโฟลว์อะซิงโครนัส "ทำสิ่งนี้ แล้วทำสิ่งนั้น" ทั่วไป โดยใช้ Promises Generators ที่มี yield* เป็น primitives ระดับล่างที่ทรงพลังกว่าซึ่ง async/await สร้างขึ้นบน ใช้ async/await สำหรับงานอะซิงโครนัส Promise ทั่วไป สงวน Generators ที่มี yield* สำหรับสถานการณ์ที่ต้องการการทำซ้ำแบบกำหนดเอง การประเมินแบบ lazy การจัดการสถานะขั้นสูง หรือเมื่อสร้างกลไกกระแสควบคุมอะซิงโครนัสที่กำหนดเองซึ่งไปไกลกว่า Promises แบบง่ายๆ
ผลกระทบระดับโลกและแนวปฏิบัติที่ดีที่สุด
ในโลกที่ทีมพัฒนาซอฟต์แวร์กระจายตัวอยู่ทั่วเขตเวลา วัฒนธรรม และภูมิหลังทางวิชาชีพเพิ่มมากขึ้น การนำรูปแบบที่ช่วยเพิ่มการทำงานร่วมกันและการบำรุงรักษามาใช้ ไม่ใช่แค่ความชอบ แต่เป็นสิ่งจำเป็น JavaScript Generator Delegation ผ่าน yield* มีส่วนช่วยโดยตรงต่อเป้าหมายเหล่านี้ โดยเสนอประโยชน์ที่สำคัญสำหรับทีมทั่วโลกและระบบนิเวศวิศวกรรมซอฟต์แวร์ที่กว้างขึ้น
ความสามารถในการอ่านโค้ดและการบำรุงรักษา
ตรรกะที่ซับซ้อนมักนำไปสู่โค้ดที่ยุ่งเหยิง ซึ่งเป็นเรื่องยากอย่างไม่น่าเชื่อที่จะทำความเข้าใจและบำรุงรักษา โดยเฉพาะอย่างยิ่งเมื่อนักพัฒนาหลายคนมีส่วนร่วมในฐานข้อมูลโค้ดเดียว yield* ช่วยให้คุณสามารถแบ่งฟังก์ชัน Generator ที่ใหญ่และเป็น monolithic ออกเป็น sub-Generators ที่เล็กกว่าและมุ่งเน้นมากขึ้น แต่ละ sub-Generator สามารถห่อหุ้มส่วนของตรรกะที่แตกต่างกัน หรือขั้นตอนเฉพาะในกระบวนการที่ใหญ่ขึ้น
ความเป็นโมดูลนี้ช่วยเพิ่มความสามารถในการอ่านได้อย่างมาก นักพัฒนาที่พบ yield* expression จะทราบทันทีว่าการควบคุมกำลังถูกมอบหมายไปยัง Generator ลำดับอื่นที่อาจมีความเชี่ยวชาญเฉพาะทาง สิ่งนี้ทำให้ง่ายต่อการติดตามกระแสการควบคุมและข้อมูล ลดภาระทางปัญญา และเร่งการรับเข้าใช้งานสำหรับสมาชิกใหม่ในทีม โดยไม่คำนึงถึงภาษาประจำชาติของพวกเขา หรือประสบการณ์ก่อนหน้านี้กับโครงการเฉพาะ
ความเป็นโมดูลและการนำกลับมาใช้ใหม่
ความสามารถในการมอบหมายงานให้กับ Generators ที่เป็นอิสระส่งเสริมระดับความเป็นโมดูลที่สูง ฟังก์ชัน Generator แต่ละรายการสามารถพัฒนา ทดสอบ และบำรุงรักษาได้อย่างอิสระ ตัวอย่างเช่น Generator ที่รับผิดชอบในการดึงข้อมูลจากจุดสิ้นสุด API ที่เฉพาะเจาะจงสามารถนำกลับมาใช้ใหม่ได้ในหลายส่วนของแอปพลิเคชัน หรือแม้แต่ในโครงการที่แตกต่างกัน Generator ที่ตรวจสอบอินพุตของผู้ใช้สามารถเสียบเข้ากับฟอร์มต่างๆ หรือกระแสปฏิสัมพันธ์ได้
การนำกลับมาใช้ใหม่นี้เป็นเสาหลักของการพัฒนาซอฟต์แวร์ที่มีประสิทธิภาพ มันลดการซ้ำซ้อนของโค้ด ส่งเสริมความสอดคล้อง และช่วยให้ทีมพัฒนา (แม้ว่าทีมจะกระจายตัวอยู่ทั่วทวีป) สามารถมุ่งเน้นไปที่การสร้างส่วนประกอบเฉพาะที่สามารถจัดองค์ประกอบได้อย่างง่ายดาย สิ่งนี้จะเร่งวงจรการพัฒนาและลดโอกาสเกิดข้อผิดพลาด ส่งผลให้แอปพลิเคชันทั่วโลกมีความแข็งแกร่งและปรับขนาดได้มากขึ้น
ความสามารถในการทดสอบที่เพิ่มขึ้น
หน่วยโค้ดที่เล็กกว่าและมุ่งเน้นมากขึ้นโดยเนื้อแท้แล้วจะทดสอบได้ง่ายกว่า เมื่อคุณแบ่ง Generator ที่ซับซ้อนออกเป็นหลาย Generators ที่มอบหมาย คุณสามารถเขียนการทดสอบหน่วยที่ตรงเป้าหมายสำหรับแต่ละ sub-Generator สิ่งนี้จะช่วยให้แน่ใจว่าตรรกะแต่ละส่วนทำงานอย่างถูกต้องโดยอิสระก่อนที่จะรวมเข้ากับระบบที่ใหญ่ขึ้น วิธีการทดสอบแบบละเอียดนี้ส่งผลให้คุณภาพโค้ดสูงขึ้น และทำให้ง่ายต่อการระบุและแก้ไขปัญหา ซึ่งเป็นข้อได้เปรียบที่สำคัญสำหรับทีมที่กระจายตัวทางภูมิศาสตร์ที่ทำงานร่วมกันในแอปพลิเคชันที่สำคัญ
การยอมรับในไลบรารีและเฟรมเวิร์ก
แม้ว่า `async/await` จะเข้ามาแทนที่การดำเนินการอะซิงโครนัสที่ใช้ Promise ทั่วไปแล้ว แต่พลังพื้นฐานของ Generators และความสามารถในการมอบหมายของพวกมันได้มีอิทธิพลและยังคงถูกนำมาใช้ในไลบรารีและเฟรมเวิร์กต่างๆ การทำความเข้าใจ `yield*` สามารถให้ข้อมูลเชิงลึกที่ลึกซึ้งยิ่งขึ้นเกี่ยวกับวิธีการใช้งานกลไกกระแสควบคุมขั้นสูงบางอย่าง แม้ว่าจะไม่ได้เปิดเผยโดยตรงต่อผู้ใช้ปลายทางก็ตาม ตัวอย่างเช่น แนวคิดที่คล้ายคลึงกับกระแสควบคุมที่ใช้ Generator มีความสำคัญในเวอร์ชันแรกๆ ของไลบรารีอย่าง Redux Saga ซึ่งแสดงให้เห็นว่ารูปแบบพื้นฐานเหล่านี้มีความสำคัญต่อการจัดการสถานะที่ซับซ้อนและการจัดการผลข้างเคียงอย่างไร
นอกเหนือจากไลบรารีเฉพาะแล้ว หลักการของการจัดองค์ประกอบ iterables และการมอบหมายการควบคุมแบบ iterative เป็นรากฐานในการสร้างไปป์ไลน์ข้อมูลที่มีประสิทธิภาพและรูปแบบการเขียนโปรแกรมเชิงโต้ตอบ ซึ่งมีความสำคัญอย่างยิ่งในแอปพลิเคชันทั่วโลกที่หลากหลาย ตั้งแต่แดชบอร์ดวิเคราะห์ข้อมูลแบบเรียลไทม์ ไปจนถึงเครือข่ายการจัดส่งเนื้อหาขนาดใหญ่
การเขียนโค้ดร่วมกันข้ามทีมที่หลากหลาย
การทำงานร่วมกันที่มีประสิทธิภาพเป็นเส้นเลือดใหญ่ของการพัฒนาซอฟต์แวร์ทั่วโลก Generator Delegation อำนวยความสะดวกในสิ่งนี้โดยการส่งเสริมขอบเขต API ที่ชัดเจนระหว่างฟังก์ชัน Generator เมื่อนักพัฒนาสร้าง Generator ที่ออกแบบมาเพื่อมอบหมาย พวกเขาจะกำหนดอินพุต เอาต์พุต และค่าที่ yield ได้ วิธีการเขียนโปรแกรมตามสัญญา (contract-based) นี้ทำให้ง่ายขึ้นสำหรับนักพัฒนาหรือทีมที่แตกต่างกัน ซึ่งอาจมีภูมิหลังทางวัฒนธรรมหรือรูปแบบการสื่อสารที่แตกต่างกัน ในการรวมงานของพวกเขาร่วมกันได้อย่างราบรื่น สิ่งนี้จะลดสมมติฐาน และลดความจำเป็นในการสื่อสารแบบซิงโครนัสอย่างละเอียดถี่ถ้วน ซึ่งอาจเป็นเรื่องท้าทายข้ามเขตเวลา
ด้วยการส่งเสริมความเป็นโมดูลและพฤติกรรมที่คาดการณ์ได้ yield* จึงกลายเป็นเครื่องมือสำหรับการส่งเสริมการสื่อสารและการประสานงานที่ดีขึ้นภายในสภาพแวดล้อมวิศวกรรมที่หลากหลาย ทำให้มั่นใจได้ว่าโครงการจะยังคงดำเนินต่อไปและผลลัพธ์จะตรงตามมาตรฐานคุณภาพและประสิทธิภาพทั่วโลก
สรุป: การยอมรับการจัดองค์ประกอบเพื่ออนาคตที่ดีกว่า
JavaScript Generator Delegation ซึ่งขับเคลื่อนโดย yield* expression ที่สง่างาม เป็นกลไกที่ซับซ้อนและมีประสิทธิภาพอย่างยิ่งสำหรับการจัดองค์ประกอบลำดับ iterable ที่ซับซ้อนและการจัดการกระแสควบคุมที่ซับซ้อน มันมีโซลูชันที่แข็งแกร่งสำหรับการทำให้ฟังก์ชัน Generator เป็นโมดูล การอำนวยความสะดวกในการสื่อสารสองทาง การจัดการข้อผิดพลาดอย่างสง่างาม และการจับค่าส่งคืนจากงานที่มอบหมาย
แม้ว่า async/await จะกลายเป็นค่าเริ่มต้นสำหรับรูปแบบการเขียนโปรแกรมแบบอะซิงโครนัสจำนวนมาก แต่การทำความเข้าใจและใช้ yield* ยังคงมีคุณค่าสำหรับสถานการณ์ที่ต้องการการทำซ้ำแบบกำหนดเอง การประเมินแบบ lazy การจัดการสถานะขั้นสูง หรือเมื่อสร้างกลไกอะซิงโครนัสที่ซับซ้อนของคุณเอง ความสามารถในการทำให้การจัดลำดับการดำเนินการตามลำดับง่ายขึ้น การแยกวิเคราะห์สตรีมข้อมูลที่ซับซ้อน และการสร้างเครื่องสถานะ ทำให้เป็นเครื่องมือที่ทรงพลังในการเพิ่มพูนชุดเครื่องมือของนักพัฒนา
ในภูมิทัศน์การพัฒนาทั่วโลกที่เชื่อมโยงถึงกันมากขึ้น ประโยชน์ของ yield* – รวมถึงความสามารถในการอ่านโค้ดที่ดีขึ้น ความเป็นโมดูล ความสามารถในการทดสอบ และการทำงานร่วมกันที่ได้รับการปรับปรุง – มีความเกี่ยวข้องมากกว่าที่เคย ด้วยการยอมรับ Generator delegation นักพัฒนาทั่วโลกสามารถเขียนแอปพลิเคชัน JavaScript ที่สะอาดขึ้น บำรุงรักษาได้ง่ายขึ้น และแข็งแกร่งขึ้น ซึ่งมีความพร้อมที่ดีกว่าในการจัดการความซับซ้อนของระบบซอฟต์แวร์สมัยใหม่
เราขอแนะนำให้คุณทดลองใช้ yield* ในโครงการถัดไปของคุณ สำรวจว่ามันสามารถทำให้เวิร์กโฟลว์แบบอะซิงโครนัสของคุณง่ายขึ้น ปรับปรุงไปป์ไลน์การประมวลผลข้อมูลของคุณให้เหมาะสม หรือช่วยคุณสร้างแบบจำลองการเปลี่ยนสถานะที่ซับซ้อนได้อย่างไร แบ่งปันข้อมูลเชิงลึกและประสบการณ์ของคุณกับชุมชนนักพัฒนาที่กว้างขึ้น ด้วยกัน เราสามารถผลักดันขอบเขตของสิ่งที่สามารถทำได้ด้วย JavaScript ต่อไป!