Cách tạo một hàm chờ cho đến khi một cuộc gọi lại được gọi bằng node.js


266

Tôi có một chức năng đơn giản hóa trông như thế này:

function(query) {
  myApi.exec('SomeCommand', function(response) {
    return response;
  });
}

Về cơ bản tôi muốn nó gọi myApi.execvà trả về phản hồi được đưa ra trong lambda gọi lại. Tuy nhiên, đoạn mã trên không hoạt động và chỉ cần trả về ngay lập tức.

Chỉ với một nỗ lực rất hóc búa, tôi đã thử những cách dưới đây không hiệu quả, nhưng ít nhất bạn cũng hiểu được những gì tôi đang cố gắng đạt được:

function(query) {
  var r;
  myApi.exec('SomeCommand', function(response) {
    r = response;
  });
  while (!r) {}
  return r;
}

Về cơ bản, cách tốt nhất "node.js / hướng sự kiện" sẽ diễn ra như thế nào? Tôi muốn chức năng của mình đợi cho đến khi gọi lại được gọi, sau đó trả về giá trị được truyền cho nó.


3
Hoặc tôi đang đi về nó hoàn toàn sai ở đây, và tôi có nên gọi lại một cuộc gọi lại, thay vì trả lời?
Chris

Theo ý kiến ​​của tôi, đây là lời giải thích SO tốt nhất tại sao vòng lặp bận không hoạt động.
bluenote10

Đừng cố chờ đợi. Chỉ cần gọi chức năng tiếp theo (phụ thuộc vào cuộc gọi lại) trong khi kết thúc cuộc gọi lại
Atul

Câu trả lời:


282

Cách "tốt node.js / hướng sự kiện" để làm điều này là không chờ đợi .

Giống như hầu hết mọi thứ khác khi làm việc với các hệ thống điều khiển sự kiện như nút, hàm của bạn sẽ chấp nhận tham số gọi lại sẽ được gọi khi hoàn thành tính toán. Người gọi không nên đợi giá trị được "trả về" theo nghĩa thông thường, mà nên gửi thói quen sẽ xử lý giá trị kết quả:

function(query, callback) {
  myApi.exec('SomeCommand', function(response) {
    // other stuff here...
    // bla bla..
    callback(response); // this will "return" your value to the original caller
  });
}

Vì vậy, bạn không sử dụng nó như thế này:

var returnValue = myFunction(query);

Nhưng như thế này:

myFunction(query, function(returnValue) {
  // use the return value here instead of like a regular (non-evented) return value
});

5
Tuyệt. Nếu myApi.exec không bao giờ gọi lại thì sao? Làm thế nào tôi có thể làm cho nó để cuộc gọi lại được gọi sau khi nói 10 giây với giá trị lỗi cho biết nó đã hẹn giờ với chúng tôi hoặc một cái gì đó?
Chris

5
Hoặc tốt hơn nữa (đã thêm một kiểm tra để không thể gọi lại hai lần): jsfiddle.net/LdaFw/1
Jakob

148
Rõ ràng việc không chặn là tiêu chuẩn trong nút / js, tuy nhiên chắc chắn sẽ có những lúc muốn chặn (ví dụ: chặn trên stdin). Ngay cả nút có các phương thức "chặn" (xem tất cả các fs sync*phương thức). Như vậy, tôi nghĩ đây vẫn là một câu hỏi hợp lệ. Có một cách hay để đạt được việc chặn trong nút ngoài việc chờ đợi bận rộn?
chiến lược

7
Một câu trả lời muộn cho nhận xét của @nargetood: Tôi có thể nghĩ ra một vài cách; quá nhiều để giải thích trong bình luận này, nhưng google chúng. Hãy nhớ rằng Node không được tạo ra để bị chặn, vì vậy những thứ này không hoàn hảo. Hãy nghĩ về họ như những lời đề nghị. Dù sao, ở đây đi: (1) Sử dụng C để thực hiện chức năng của bạn và xuất bản nó lên NPM để sử dụng nó. Đó là những gì các syncphương pháp làm. (2) Sử dụng các sợi, github.com/laverdet/node-fibers , (3) Sử dụng các lời hứa, ví dụ như thư viện Q, (4) Sử dụng một lớp mỏng trên đầu javascript, trông có vẻ chặn, nhưng biên dịch thành async, như maxtaco.github.com/coffee-script
Jakob

106
Thật là bực bội khi mọi người trả lời một câu hỏi với "bạn không nên làm điều đó." Nếu một người muốn trở nên hữu ích và trả lời một câu hỏi, đó là một việc cần làm. Nhưng nói với tôi một cách dứt khoát rằng tôi không nên làm điều gì đó chỉ là không thân thiện. Có một triệu lý do khác nhau khiến ai đó muốn gọi một thói quen đồng bộ hoặc không đồng bộ. Đây là một câu hỏi về cách làm điều đó. Nếu bạn cung cấp lời khuyên hữu ích về bản chất của api trong khi cung cấp câu trả lời, điều đó rất hữu ích, nhưng nếu bạn không cung cấp câu trả lời, tại sao phải trả lời. (Tôi đoán tôi nên thực sự đứng đầu lời khuyên của chính mình.)
Howard Swope

46

Một cách để đạt được điều này là kết thúc lệnh gọi API thành một lời hứa và sau đó sử dụng awaitđể chờ kết quả.

// let's say this is the API function with two callbacks,
// one for success and the other for error
function apiFunction(query, successCallback, errorCallback) {
    if (query == "bad query") {
        errorCallback("problem with the query");
    }
    successCallback("Your query was <" + query + ">");
}

// myFunction wraps the above API call into a Promise
// and handles the callbacks with resolve and reject
function apiFunctionWrapper(query) {
    return new Promise((resolve, reject) => {
        apiFunction(query,(successResponse) => {
            resolve(successResponse);
        }, (errorResponse) => {
            reject(errorResponse)
        });
    });
}

// now you can use await to get the result from the wrapped api function
// and you can use standard try-catch to handle the errors
async function businessLogic() {
    try {
        const result = await apiFunctionWrapper("query all users");
        console.log(result);

        // the next line will fail
        const result2 = await apiFunctionWrapper("bad query");
    } catch(error) {
        console.error("ERROR:" + error);
    }
}

// call the main function
businessLogic();

Đầu ra:

Your query was <query all users>
ERROR:problem with the query

Đây là một ví dụ được thực hiện rất tốt khi gói một hàm với một cuộc gọi lại để bạn có thể sử dụng nó với async/await tôi không thường xuyên cần điều này, vì vậy, hãy nhớ cách xử lý tình huống này, tôi đang sao chép điều này cho các ghi chú / tài liệu tham khảo cá nhân của tôi.
robert arles


10

Nếu bạn không muốn sử dụng gọi lại thì bạn có thể sử dụng mô-đun "Q".

Ví dụ:

function getdb() {
    var deferred = Q.defer();
    MongoClient.connect(databaseUrl, function(err, db) {
        if (err) {
            console.log("Problem connecting database");
            deferred.reject(new Error(err));
        } else {
            var collection = db.collection("url");
            deferred.resolve(collection);
        }
    });
    return deferred.promise;
}


getdb().then(function(collection) {
   // This function will be called afte getdb() will be executed. 

}).fail(function(err){
    // If Error accrued. 

});

Để biết thêm thông tin tham khảo điều này: https://github.com/kriskowal/q


9

Nếu bạn muốn nó rất đơn giản và dễ dàng, không có thư viện ưa thích nào, hãy đợi các hàm gọi lại được thực thi trong nút, trước khi thực thi một số mã khác, giống như sau:

//initialize a global var to control the callback state
var callbackCount = 0;
//call the function that has a callback
someObj.executeCallback(function () {
    callbackCount++;
    runOtherCode();
});
someObj2.executeCallback(function () {
    callbackCount++;
    runOtherCode();
});

//call function that has to wait
continueExec();

function continueExec() {
    //here is the trick, wait until var callbackCount is set number of callback functions
    if (callbackCount < 2) {
        setTimeout(continueExec, 1000);
        return;
    }
    //Finally, do what you need
    doSomeThing();
}

5

Lưu ý: Câu trả lời này có lẽ không nên được sử dụng trong mã sản xuất. Đó là một hack và bạn nên biết về những tác động.

Có mô-đun uvrun (được cập nhật cho các phiên bản Nodejs mới hơn ở đây ) nơi bạn có thể thực hiện một vòng lặp đơn của vòng lặp sự kiện chính libuv (là vòng lặp chính của Nodejs).

Mã của bạn sẽ trông như thế này:

function(query) {
  var r;
  myApi.exec('SomeCommand', function(response) {
    r = response;
  });
  var uvrun = require("uvrun");
  while (!r)
    uvrun.runOnce();
  return r;
}

(Bạn có thể sử dụng thay thế uvrun.runNoWait(). Điều đó có thể tránh một số vấn đề với việc chặn, nhưng mất 100% CPU.)

Lưu ý rằng cách tiếp cận này làm mất hiệu lực toàn bộ mục đích của Nodejs, tức là có mọi thứ không đồng bộ và không chặn. Ngoài ra, nó có thể tăng độ sâu của nút gọi của bạn rất nhiều, vì vậy bạn có thể kết thúc với tràn ngăn xếp. Nếu bạn chạy chức năng như vậy một cách đệ quy, bạn chắc chắn sẽ gặp rắc rối.

Xem các câu trả lời khác về cách thiết kế lại mã của bạn để thực hiện "đúng".

Giải pháp này ở đây có lẽ chỉ hữu ích khi bạn thực hiện thử nghiệm và đặc biệt. muốn có mã đồng bộ và nối tiếp.


5

Vì nút 4.8.0, bạn có thể sử dụng tính năng của ES6 được gọi là trình tạo. Bạn có thể theo dõi bài viết này cho các khái niệm sâu hơn. Nhưng về cơ bản, bạn có thể sử dụng máy phát điện và hứa sẽ hoàn thành công việc này. Tôi đang sử dụng bluebird để quảng bá và quản lý trình tạo.

Mã của bạn sẽ ổn như ví dụ dưới đây.

const Promise = require('bluebird');

function* getResponse(query) {
  const r = yield new Promise(resolve => myApi.exec('SomeCommand', resolve);
  return r;
}

Promise.coroutine(getResponse)()
  .then(response => console.log(response));

1

giả sử bạn có một chức năng:

var fetchPage(page, callback) {
   ....
   request(uri, function (error, response, body) {
        ....
        if (something_good) {
          callback(true, page+1);
        } else {
          callback(false);
        }
        .....
   });


};

bạn có thể sử dụng các cuộc gọi lại như thế này:

fetchPage(1, x = function(next, page) {
if (next) {
    console.log("^^^ CALLBACK -->  fetchPage: " + page);
    fetchPage(page, x);
}
});

-1

Điều đó đánh bại mục đích của việc không chặn IO - bạn đang chặn nó khi không cần chặn :)

Bạn nên lồng các cuộc gọi lại của mình thay vì buộc node.js phải chờ hoặc gọi một cuộc gọi lại khác bên trong cuộc gọi lại nơi bạn cần kết quả r.

Rất có thể, nếu bạn cần buộc chặn, bạn đang nghĩ về kiến ​​trúc của mình sai.


Tôi đã có một sự nghi ngờ tôi đã có điều này xung quanh ngược.
Chris

31
Rất có thể, tôi chỉ muốn viết một đoạn script nhanh cho http.get()một số URL và console.log()nội dung của nó. Tại sao tôi phải nhảy qua lại để làm điều đó trong Node?
Dan Dascalescu

6
@DanDascalescu: Và tại sao tôi phải khai báo chữ ký loại để làm điều đó bằng ngôn ngữ tĩnh? Và tại sao tôi phải đặt nó trong một phương thức chính trong các ngôn ngữ giống như C? Và tại sao tôi phải biên dịch nó bằng ngôn ngữ biên dịch? Những gì bạn đang đặt câu hỏi là một quyết định thiết kế cơ bản trong Node.js. Quyết định đó có ưu và nhược điểm. Nếu bạn không thích nó, bạn có thể sử dụng ngôn ngữ khác phù hợp với phong cách của bạn hơn. Đó là lý do tại sao chúng ta có nhiều hơn một.
Jakob

@Jakob: các giải pháp bạn đã liệt kê thực sự là tối ưu. Điều đó không có nghĩa là không có những thứ tốt, chẳng hạn như việc sử dụng Node trong các máy chủ của Meteor, giúp loại bỏ vấn đề địa ngục gọi lại.
Dan Dascalescu

13
@Jakob: Nếu câu trả lời tốt nhất cho "tại sao hệ sinh thái X làm cho nhiệm vụ chung Y trở nên khó khăn không cần thiết?" là "nếu bạn không thích nó, đừng sử dụng hệ sinh thái X", thì đó là một dấu hiệu mạnh mẽ cho thấy các nhà thiết kế và bảo trì hệ sinh thái X đang ưu tiên các bản ngã của họ trên khả năng sử dụng thực tế của hệ sinh thái của họ. Đó là kinh nghiệm của tôi khi cộng đồng Node (trái ngược với cộng đồng Ruby, Elixir và thậm chí cả PHP) vượt ra ngoài để làm cho các nhiệm vụ chung trở nên khó khăn. Cảm ơn bạn RẤT NHIỀU đã cung cấp cho mình như một ví dụ sống động của antipotype đó.
Jazz

-1

Sử dụng async và chờ đợi nó dễ dàng hơn nhiều.

router.post('/login',async (req, res, next) => {
i = await queries.checkUser(req.body);
console.log('i: '+JSON.stringify(i));
});

//User Available Check
async function checkUser(request) {
try {
    let response = await sql.query('select * from login where email = ?', 
    [request.email]);
    return response[0];

    } catch (err) {
    console.log(err);

  }

}

API được sử dụng trong câu hỏi không trả lại lời hứa, vì vậy bạn sẽ cần phải bọc nó trong một câu hỏi đầu tiên giống như câu trả lời này đã làm cách đây hai năm.
Quentin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.