Node.js Generators & So sánh với lệnh gọi lại

⚡ Tóm tắt thông minh

GeneratorTrong Node.js, `yield` là các hàm mà quá trình thực thi của chúng có thể bị tạm dừng và tiếp tục theo yêu cầu. Từ khóa `yield` tạm dừng hàm, phương thức `next` tiếp tục hàm, và sự kết hợp này giúp đơn giản hóa các hàm gọi lại lồng nhau thành mã tuần tự dễ đọc.

  • ⏸️ Hành vi cốt lõi: Máy phát điện sẽ tạm dừng ở mỗi lần trả về dữ liệu và tiếp tục hoạt động chính xác tại điểm dừng đó khi được gọi lần tiếp theo.
  • ✳️ Tờ khai: Dấu hoa thị sau từ khóa `function` đánh dấu hàm đó là một hàm tạo (generator) chứ không phải là một hàm thông thường.
  • 🔄 Kênh hai chiều: Giá trị được truyền vào hàm next sẽ trở thành kết quả của biểu thức yield đã tạm dừng hàm.
  • 🪆 Địa ngục gọi lại: Các hàm gọi lại lồng nhau trở nên khó đọc khi số bước được thêm vào, đó chính là vấn đề mà các bộ tạo được áp dụng để giải quyết.
  • 🏃 Cần người chạy việc vặt: Trình tạo không tự vận hành; hàm chạy phải gọi hàm next nhiều lần để công việc bất đồng bộ được tiến hành.
  • 🦥 Thực thi lười biếng: Việc tạm dừng và tiếp tục cho phép chương trình chỉ lấy các giá trị vào thời điểm cần thiết.

Node.js GeneratorSo sánh với các hàm gọi lại

Trong hướng dẫn này, chúng ta sẽ xem xét các generator và sự khác biệt giữa chúng với các hàm callback.

Máy phát điện là gì?

Generatortrở nên phổ biến vào năm Node.js Bởi vì những gì họ có khả năng làm được.

  • Generatorlà các hàm thực thi có thể tạm dừng và tiếp tục ở thời điểm sau.
  • GeneratorCác câu lệnh `s` rất hữu ích cho các khái niệm như "thực thi lười biếng". Bằng cách tạm dừng thực thi và tiếp tục khi cần, các giá trị chỉ được lấy khi chúng cần thiết.

GeneratorCó hai phương pháp chính dưới đây.

  1. Phương pháp năng suất — được gọi bên trong một hàm để dừng thực thi tại chính xác dòng lệnh nơi yield xuất hiện.
  2. Phương pháp tiếp theo — được gọi từ ứng dụng chính để tiếp tục một hàm đã tạm dừng. Quá trình thực thi tiếp tục cho đến lần gọi tiếp theo. yieldhoặc cho đến khi kết thúc hàm.

Hãy xem một ví dụ về cách sử dụng generator. Ví dụ này định nghĩa một hàm cộng đơn giản, thực hiện phép cộng hai số, và dừng thực thi tại các điểm khác nhau để minh họa cách generator hoạt động.

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 Giải thích

  1. Bước đầu tiên là định nghĩa hàm sinh. Lưu ý rằng * được thêm vào function từ khóa. Hàm được gọi là Add và nhận một tham số x.
  2. yield Từ khóa này dành riêng cho các hàm tạo và tạm dừng hàm giữa chừng khi đang thực thi. Quá trình thực thi dừng lại ở đây cho đến khi next() Được gọi.
  3. Thứ hai yield vừa tạm dừng hàm, vừa nhận một giá trị: bất cứ giá trị nào được truyền cho hàm tiếp theo. next() cuộc gọi trở thành giá trị của y.
  4. đang gọi gen.next() Lần đầu tiên chạy hàm này, nó sẽ thực thi cho đến lần đầu tiên. yield. Gọi gen.next(6) tiếp tục, giao 6 cho yvà trả lại x + y.

⚠️ Đính chính: Danh sách ban đầu có chứa dòng y = 6 ngay sau đó var y = yield(null);Việc gán giá trị đó sẽ ghi đè lên bất kỳ giá trị nào khác. next() nguồn cung cấp, điều này làm mất đi mục đích của ví dụ và khiến cho ví dụ thứ hai trở nên vô nghĩa. yield Vô nghĩa. Giá trị hiện được truyền vào thông qua gen.next(6)Đây chính là cách mà máy phát điện nhận dữ liệu từ người gọi.

Hàm gọi lại so với hàm tạo

GeneratorGiải pháp này giúp giải quyết vấn đề được gọi là "địa ngục callback". Trong quá trình phát triển ứng dụng Node.js, các hàm callback đôi khi lồng nhau quá sâu đến mức khó theo dõi.

Đây là lúc các trình tạo (generator) phát huy tác dụng. Một trong những ví dụ phổ biến nhất là tạo các hàm hẹn giờ. Ví dụ dưới đây xây dựng một hàm trễ thời gian đơn giản và sau đó gọi nó với các độ trễ là 1000, 2000 và 3000 ms.

Bước 1) Hãy định nghĩa hàm gọi lại với mã xử lý độ trễ thời gian cần thiết.

Cuộc gọi lại so với Generators

function Timedelay(ptime, callback) {

    setTimeout(function() {

        callback("Pausing for " + ptime);

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

Code Giải thích

  1. Một chức năng được gọi là Timedelay được tạo với một tham số ptime, trong đó ghi rõ số mili giây cần tạm dừng.
  2. setTimeout Chờ bấy nhiêu mili giây rồi gọi hàm callback được cung cấp.

⚠️ Đính chính: mã gốc đã được thông qua time như lập luận thứ hai cho setTimeout, nhưng không có biến nào được đặt tên time tồn tại — tham số là ptimeNhư đã viết, nó ném ra ReferenceError: time is not defined.

Bước 2) Bây giờ hãy xem xét đoạn mã sử dụng hàm gọi lại (callback). Để tạo chuỗi độ trễ 1000, 2000 và 3000 mili giây, các hàm gọi lại sẽ lồng vào nhau.

Cuộc gọi lại so với Generators

Timedelay(1000, function(message) {

    console.log(message);

    Timedelay(2000, function(message) {

        console.log(message);

        Timedelay(3000, function(message) {

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

Code Giải thích

  1. Timedelay được gọi như một hàm callback với giá trị là 1000.
  2. Bên trong hàm gọi lại đó, Timedelay được gọi lại với số 2000.
  3. Bên trong cái đó, Timedelay được gọi lần thứ ba với số 3000.

⚠️ Đính chính: mỗi lần gọi lại trong nhật ký gốc msg, nhưng tham số được đặt tên là messageCả ba tuyến đều ném bóng. ReferenceError: msg is not defined.

Từ đoạn mã trên, bạn có thể thấy nó trở nên phức tạp hơn với mỗi lần gọi thêm — sự lồng ghép này chính là điều mà "địa ngục callback" mô tả.

Bước 3) Bây giờ hãy xem hành vi tương tự được viết bằng các hàm tạo. Chuỗi này trông giống như ba dòng thẳng chứ không phải ba cấp độ lồng nhau.

Cuộc gọi lại so với Generators

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

Code Giải thích

  1. Một hàm tạo được định nghĩa để gọi Timedelay chức năng.
  2. yield được sử dụng với Timedelay và 1000 là giá trị tham số.
  3. yield được sử dụng lại với 2000, và một lần nữa với 3000.

⚠️ Đính chính: người viết ban đầu đã viết console,log(...) Thay vì dấu chấm, cả ba dòng đều có dấu phẩy, đây là lỗi cú pháp và khiến tệp không thể được phân tích cú pháp.

Làm thế nào để chạy một Generator với chức năng Runner

Đoạn mã máy phát ở trên đọc rất hay nhưng sẽ không tự hoạt động. Đây là chi tiết mà hầu hết các lời giải thích đều bỏ qua, và đó là lý do tại sao phiên bản đơn giản có thể trông giống như phép thuật. Máy phát là một cỗ máy tạm dừng, và phải có thứ gì đó liên tục nhấn nút. Gọi Messages() Nó chỉ đơn giản tạo ra đối tượng generator và trả về ngay lập tức; không có bộ đếm thời gian nào được bắt đầu và không có gì được in ra.

Mảnh ghép còn thiếu là một hàm chạy: một hàm nhỏ gọi next(), chờ cho công việc bất đồng bộ hoàn thành, rồi gọi next() Lại một lần nữa với kết quả.

// 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);

Có ba điểm làm cho điều này hiệu quả. Thứ nhất, Timedelay Nó không còn nhận trực tiếp một hàm gọi lại nữa; nó trả về một hàm mà trình chạy có thể gọi, trả lại quyền điều khiển khi bộ hẹn giờ kích hoạt. Thứ hai, it.next(value) đưa kết quả trước đó vào máy phát đang tạm dừng, đó là điều cho phép yield Thứ ba, người chạy dừng lại ngay khi result.done Khi điều đó trở thành sự thật, quá trình đệ quy sẽ tự nhiên kết thúc. Mô hình runner này chính xác là những gì các thư viện như co đã cung cấp, và đó là cơ chế giúp... async/await sau đó được hấp thụ vào chính ngôn ngữ.

GeneratorNên sử dụng `s` hay `async/await`: Nên dùng cái nào?

GeneratorTrước đây, viết mã bất đồng bộ dạng phẳng là cách viết tiêu chuẩn. async/await đã tồn tại, và cả hai đều có chung ý tưởng cơ bản là tạm dừng một hàm ở giữa chừng. Node đã hỗ trợ async/await Tính năng này được hỗ trợ mặc định từ phiên bản 7.6 trở đi, điều này làm thay đổi khuyến nghị dành cho bất kỳ ai bắt đầu công việc mới ngày hôm nay.

Yếu tố Generators không đồng bộ/đang chờ
Cần người chạy việc vặt Vâng, do bạn hoặc thư viện viết. Không, nó được tích hợp sẵn trong ngôn ngữ.
Từ khóa tạm dừng yield await
Xử lý lỗi Người chạy phải chuyển tiếp lỗi Cố gắng bắt giữ thông thường
Có thể tạm dừng giữa chừng Vâng, và có thể tiếp tục một cách chọn lọc. Không, chạy đến khi hoàn thành
Tốt nhất cho Chuỗi lười biếng và dòng vô hạn Công việc bất đồng bộ thông thường

Sử dụng async/await Đối với luồng bất đồng bộ trong mã mới, vì nó loại bỏ bộ chạy và khôi phục lại khối try và catch thông thường. Giữ lại các generator cho những việc mà chỉ chúng mới làm tốt: tạo ra các giá trị một cách lười biếng, lặp lại các chuỗi lớn hoặc vô hạn, và bất kỳ trường hợp nào mà người tiêu dùng quyết định chính xác khi nào giá trị tiếp theo được tính toán.

Chung Generator Các lỗi và cách khắc phục chúng

Hầu hết các sự cố của hàm tạo xuất phát từ việc quên rằng hàm này không hoạt động cho đến khi có lệnh gọi. next()hoặc do những lỗi cú pháp nhỏ xung quanh dấu sao. Mỗi triệu chứng dưới đây đều nêu rõ nguyên nhân và cách khắc phục.

  • Không có gì xảy ra khi trình tạo được gọi: Việc gọi hàm này trả về một iterator, chứ không phải kết quả. Hãy gọi next()hoặc chuyền bóng cho người chạy.
  • Lỗi cú pháp khi thực hiện lệnh yield: Dấu hoa thị bị thiếu. yield chỉ có hiệu lực bên trong một function*.
  • Giá trị từ next() nhận được là undefined: đầu tiên next() Cuộc gọi không thể trả về giá trị vì trình tạo chưa đạt đến một giá trị nào. yield Tuy nhiên. Hãy gửi dữ liệu từ cuộc gọi thứ hai trở đi.
  • Vòng lặp vô tận trong trò chơi chạy: result.done không bao giờ được kiểm tra, vì vậy next() vẫn tiếp tục được gọi sau khi kết thúc. Luôn trả về khi hoàn thành là đúng.
  • Các lỗi tự biến mất một cách lặng lẽ: Một lỗi từ chối bên trong bước bất đồng bộ sẽ không bao giờ được truyền ngược trở lại trình tạo. Hãy chuyển tiếp nó bằng it.throw(err) để khối try-catch được viết bên trong hàm tạo có thể thực sự nhìn thấy và xử lý nó.

Câu Hỏi Thường Gặp

Một đối tượng có hai thuộc tính: value, giữ lại tất cả những gì đã được trao đổi, và done, một biến boolean sẽ chuyển thành true khi trình tạo hoàn tất. Trình chạy kiểm tra `done` để biết khi nào cần dừng lại.

Đúng vậy. Một generator chứa vòng lặp vô hạn là an toàn vì không có gì được tính toán cho đến khi hàm next() được gọi. Điều này làm cho generator rất phù hợp với các luồng định danh hoặc kết quả được phân trang.

Vâng. AI Công cụ hoán đổi lệnh yield thành await và loại bỏ hoàn toàn tiến trình chạy. RevHãy xem xét kỹ các chuỗi lười biếng, bởi vì một generator không bao giờ hoàn thành thì không có async await tương đương trực tiếp.

Trợ lý AI tracLiệu trình chạy có ngừng gọi hàm tiếp theo hay không, liệu hàm gọi lại tiếp tục có bao giờ được gọi hay không, hoặc liệu quá trình hoàn thành có bao giờ được thực hiện hay không. Ba điều đó chiếm gần như toàn bộ nguyên nhân gây ra hiện tượng kẹt máy phát điện.

Không. Một trình tạo bất đồng bộ đã được khai báo. async function* và bị tiêu thụ với for await...ofNó kết hợp cả hai ý tưởng và phù hợp với việc truyền dữ liệu bất đồng bộ, chẳng hạn như con trỏ cơ sở dữ liệu.

Tóm tắt bài viết này với: