Node.js Generators & เปรียบเทียบกับการโทรกลับ

⚡ สรุปอย่างชาญฉลาด

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

  • ⏸️ พฤติกรรมหลัก: เครื่องกำเนิดไฟฟ้าจะหยุดทำงานชั่วคราวเมื่อถึงระดับสัญญาณเอาต์พุตแต่ละครั้ง และจะกลับมาทำงานต่อจากจุดที่หยุดไปเมื่อมีการเรียกใช้งานครั้งต่อไป
  • ✳️ ประกาศ: เครื่องหมายดอกจันหลังคำหลักของฟังก์ชันจะบ่งบอกว่าฟังก์ชันนั้นเป็นตัวสร้าง (generator) ไม่ใช่ฟังก์ชันธรรมดา
  • 🔄 ช่องสัญญาณสองทาง: ค่าที่ส่งผ่านไปยังฟังก์ชัน next จะกลายเป็นผลลัพธ์ของนิพจน์ yield ที่หยุดการทำงานของฟังก์ชันไว้ชั่วคราว
  • 🪆 นรกแห่งการเรียกกลับ: การเรียกกลับแบบซ้อนกันจะอ่านยากขึ้นเมื่อมีการเพิ่มขั้นตอน ซึ่งเป็นปัญหาที่ตัวสร้างถูกนำมาใช้เพื่อแก้ไข
  • 🏃 ต้องการผู้ส่งสาร: ตัวสร้างสัญญาณไม่ได้ทำงานด้วยตัวเอง ฟังก์ชันตัวเรียกใช้งานจะต้องเรียกฟังก์ชัน next ซ้ำ ๆ เพื่อให้การทำงานแบบอะซิงโครนัสดำเนินต่อไปได้
  • 🦥 การดำเนินการที่ไม่เอาใจใส่: การระงับและเริ่มทำงานต่อช่วยให้โปรแกรมดึงค่าเฉพาะในช่วงเวลาที่จำเป็นเท่านั้น

Node.js Generatorเมื่อเปรียบเทียบกับ Callback

ในบทแนะนำนี้ เราจะมาดูตัวสร้าง (generator) และความแตกต่างระหว่างตัวสร้างกับฟังก์ชันเรียกกลับ (callback)

เครื่องกำเนิดไฟฟ้าคืออะไร?

Generatorกลายเป็นที่นิยมใน Node.js เพราะสิ่งที่พวกเขาสามารถทำได้

  • Generatorคือการดำเนินการฟังก์ชันที่สามารถระงับและดำเนินการต่อในภายหลังได้
  • Generatorมีประโยชน์สำหรับแนวคิดต่างๆ เช่น "การประมวลผลแบบไม่เร่งรีบ" (lazy execution) โดยการระงับการประมวลผลและกลับมาทำงานต่อตามต้องการ ค่าต่างๆ จะถูกดึงมาใช้ก็ต่อเมื่อจำเป็นเท่านั้น

Generatorมีสองวิธีหลักดังต่อไปนี้

  1. วิธีการให้ผลผลิต — เรียกใช้ภายในฟังก์ชันเพื่อหยุดการทำงาน ณ บรรทัดที่ระบุ yield ปรากฏ
  2. วิธีการต่อไป — เรียกใช้จากแอปพลิเคชันหลักเพื่อดำเนินการต่อฟังก์ชันที่หยุดชั่วคราว การทำงานจะดำเนินต่อไปจนกว่าจะมีการเรียกใช้ครั้งถัดไป yieldหรือจนกว่าฟังก์ชันจะสิ้นสุด

เรามาดูตัวอย่างการใช้งานตัวสร้าง (generator) กัน ตัวอย่างนี้กำหนดฟังก์ชัน Add อย่างง่ายที่บวกตัวเลขสองตัว โดยหยุดการทำงานที่จุดต่างๆ เพื่อแสดงให้เห็นว่าตัวสร้างทำงานอย่างไร

Node.js Generators

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 คำอธิบาย

  1. ขั้นตอนแรกคือการกำหนดฟังก์ชันตัวสร้าง โปรดสังเกต * เพิ่มลงในไฟล์ function คำหลัก ฟังก์ชันนี้ถูกเรียกใช้ Add และรับพารามิเตอร์ x.
  2. การขอ yield คีย์เวิร์ดนี้ใช้เฉพาะกับตัวสร้าง (generator) และจะหยุดการทำงานของฟังก์ชันกลางคัน การทำงานจะหยุดลงตรงนี้จนกว่าจะ... next() ถูกเรียก
  3. ที่สอง yield ทั้งหยุดการทำงานของฟังก์ชันชั่วคราวและรับค่า: ไม่ว่าค่าใดจะถูกส่งต่อไปยังส่วนถัดไป next() การโทรกลายเป็นค่าของ y.
  4. การเรียกร้อง 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) พร้อมโค้ดหน่วงเวลาที่จำเป็น

โทรกลับเทียบกับ Generators

function Timedelay(ptime, callback) {

    setTimeout(function() {

        callback("Pausing for " + ptime);

    }, ptime);   // the delay must use ptime, the parameter name
}

Code คำอธิบาย

  1. ฟังก์ชันที่เรียกว่า Timedelay ถูกสร้างขึ้นด้วยพารามิเตอร์ ptimeซึ่งระบุจำนวนมิลลิวินาทีที่จะหยุดชั่วคราว
  2. setTimeout รอเป็นเวลาตามจำนวนมิลลิวินาทีที่ระบุ แล้วจึงเรียกใช้ฟังก์ชันเรียกกลับที่กำหนดไว้

⚠️ แก้ไข: รหัสเดิมผ่านแล้ว time เป็นข้อโต้แย้งที่สองต่อ setTimeoutแต่ไม่มีตัวแปรชื่อนั้น time มีอยู่จริง — พารามิเตอร์คือ ptimeตามที่เขียนไว้ มันจะโยน ReferenceError: time is not defined.

ขั้นตอน 2) ทีนี้ลองพิจารณาโค้ดที่ใช้ Callback ดู ในการหน่วงเวลา 1000, 2000 และ 3000 มิลลิวินาที Callback จะต้องซ้อนกันอยู่ภายในกันและกัน

โทรกลับเทียบกับ Generators

Timedelay(1000, function(message) {

    console.log(message);

    Timedelay(2000, function(message) {

        console.log(message);

        Timedelay(3000, function(message) {

            console.log(message);
        });
    });
});

Code คำอธิบาย

  1. Timedelay เรียกใช้เป็นฟังก์ชันเรียกกลับ (callback) โดยมีค่าเป็น 1000
  2. ภายในฟังก์ชันเรียกกลับนั้น Timedelay ถูกเรียกอีกครั้งในปี 2000
  3. ภายในนั้น Timedelay ถูกเรียกเป็นครั้งที่สามด้วย 3000

⚠️ แก้ไข: แต่ละฟังก์ชันเรียกกลับในบันทึกเดิม msgแต่พารามิเตอร์นั้นมีชื่อว่า messageทั้งสามสายโยน ReferenceError: msg is not defined.

จากโค้ดด้านบน คุณจะเห็นว่ามันยิ่งซับซ้อนขึ้นเรื่อยๆ เมื่อมีการเรียกใช้ฟังก์ชันเพิ่มขึ้น — การซ้อนกันแบบนี้คือสิ่งที่เรียกว่า "callback hell"

ขั้นตอน 3) ทีนี้ลองดูพฤติกรรมเดียวกันนี้ที่เขียนด้วยตัวสร้าง (generator) ลำดับที่ได้จะอ่านได้เป็นเส้นตรงสามเส้นแทนที่จะเป็นการซ้อนกันสามระดับ

โทรกลับเทียบกับ Generators

function* Messages() {
    console.log(yield Timedelay(1000));
    console.log(yield Timedelay(2000));
    console.log(yield Timedelay(3000));
}

Code คำอธิบาย

  1. มีการกำหนดฟังก์ชันตัวสร้างเพื่อเรียกใช้ Timedelay ฟังก์ชัน
  2. yield ใช้กับ Timedelay และกำหนดค่าพารามิเตอร์เป็น 1000
  3. yield นำมาใช้อีกครั้งกับ 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 ที่เขียนไว้ภายในตัวสร้างโค้ดสามารถมองเห็นและจัดการกับข้อผิดพลาดได้จริง

คำถามที่พบบ่อย

วัตถุที่มีคุณสมบัติสองอย่าง: valueโดยยึดถือสิ่งที่ยอมจำนนเอาไว้ และ doneค่าบูลีนที่จะเป็นจริงเมื่อตัวสร้างทำงานเสร็จสิ้น การทดสอบนี้ทำขึ้นเพื่อให้ทราบว่าควรหยุดเมื่อใด

ใช่แล้ว ตัวสร้างข้อมูลที่มีลูปไม่สิ้นสุดนั้นปลอดภัย เพราะจะไม่มีการคำนวณใดๆ จนกว่าจะมีการเรียกใช้เมธอด next() ทำให้ตัวสร้างข้อมูลเหมาะอย่างยิ่งสำหรับสตรีมของตัวระบุหรือผลลัพธ์ที่มีการแบ่งหน้า

ใช่. AI เครื่องมือต่างๆ สลับ yield เป็น await และตัดการทำงานของ runner ออกไปโดยสิ้นเชิง Revโปรดตรวจสอบลำดับการทำงานแบบ lazy อย่างระมัดระวัง เนื่องจาก generator ที่ไม่เสร็จสิ้นการทำงานจะไม่มีฟังก์ชันเทียบเท่า async await โดยตรง

ผู้ช่วย AI tracไม่ว่าตัวรันจะหยุดเรียกฟังก์ชันถัดไปหรือไม่ ไม่ว่าฟังก์ชันเรียกกลับเพื่อดำเนินการต่อจะไม่ถูกเรียกใช้หรือไม่ หรือไม่ว่าฟังก์ชันเสร็จสิ้นจะไม่ถูกเรียกใช้เลย ปัจจัยทั้งสามนี้เป็นสาเหตุหลักที่ทำให้เครื่องกำเนิดไฟฟ้าหยุดชะงักเกือบทุกเครื่อง

ไม่ มีการประกาศตัวสร้างแบบอะซิงโครนัสไว้แล้ว async function* และถูกบริโภคด้วย for await...ofมันเป็นการผสมผสานทั้งสองแนวคิดและเหมาะสำหรับการประมวลผลข้อมูลแบบอะซิงโครนัส เช่น เคอร์เซอร์ฐานข้อมูล

สรุปโพสต์นี้ด้วย: