Как заставить функцию ждать, пока не будет вызван обратный вызов, используя node.js

267

У меня есть упрощенная функция, которая выглядит так:

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

В основном, я хочу, чтобы он вызывал myApi.execи возвращал ответ, который дается в лямбде обратного вызова. Однако приведенный выше код не работает и просто сразу возвращается.

Просто для очень хакерской попытки, я попробовал следующее, которое не сработало, но, по крайней мере, вы поняли, чего я пытаюсь достичь:

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

По сути, каков хороший способ управления событиями на основе node.js? Я хочу, чтобы моя функция ожидала обратного вызова, а затем возвращала значение, переданное ей.

Крис
источник
3
Или я делаю это совершенно неправильно, и должен ли я вызывать другой обратный вызов, а не возвращать ответ?
Крис
На мой взгляд, это лучшее SO-объяснение, почему занятый цикл не работает.
bluenote10
Не пытайся ждать. Просто вызовите следующую функцию (зависящую от обратного вызова) в конце самого обратного вызова
Atul

Ответы:

282

«Хороший узел.js / управляемый событиями» способ сделать это - не ждать .

Как и почти все остальное при работе с системами, управляемыми событиями, такими как узел, ваша функция должна принимать параметр обратного вызова, который будет вызван после завершения вычислений. Вызывающая сторона не должна ждать возврата значения в обычном смысле, а должна отправить подпрограмму, которая будет обрабатывать полученное значение:

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

Таким образом, вы не используете это так:

var returnValue = myFunction(query);

А вот так:

myFunction(query, function(returnValue) {
  // use the return value here instead of like a regular (non-evented) return value
});
Jakob
источник
5
Ок, отлично. А что если myApi.exec никогда не вызывал обратный вызов? Как бы я сделал так, чтобы обратный вызов вызывался, скажем, через 10 секунд со значением ошибки, говорящим о том, что он приурочен к нашему или что-то в этом роде?
Крис
5
Или еще лучше (добавлена ​​проверка, чтобы обратный вызов не мог быть вызван дважды): jsfiddle.net/LdaFw/1
Jakob
148
Ясно, что неблокирование является стандартом в узле / js, однако, безусловно, бывают моменты, когда требуется блокировка (например, блокировка на stdin). Четный узел имеет «блокирующие» методы (см. Все fs sync*методы). Таким образом, я думаю, что это все еще актуальный вопрос. Есть хороший способ добиться блокировки в узле, кроме занятого ожидания?
Nategood
7
Поздний ответ на комментарий @nategood: я могу придумать пару способов; слишком много, чтобы объяснить в этом комментарии, но Google их. Помните, что Node не предназначен для блокировки, поэтому он не идеален. Думайте о них как о предложениях. В любом случае, здесь идет речь: (1) Используйте C, чтобы реализовать вашу функцию и опубликовать ее в NPM, чтобы использовать ее. Это то, что syncделают методы. (2) Используйте волокна, github.com/laverdet/node-fibers , (3) Используйте обещания, например, библиотеку Q, (4) Используйте тонкий слой поверх javascript, который выглядит блокирующим, но компилируется в асинхронный, как maxtaco.github.com/coffee-script
Якоб
106
Это так расстраивает, когда люди отвечают на вопрос «ты не должен этого делать». Если кто-то хочет быть полезным и ответить на вопрос, это стоит сделать. Но однозначно сказать мне, что я не должен что-то делать, просто недружелюбно. Есть миллион разных причин, по которым кто-то захочет вызвать процедуру синхронно или асинхронно. Это был вопрос о том, как это сделать. Если вы предоставляете полезные советы о природе API, предоставляя ответ, это полезно, но если вы не предоставляете ответ, зачем отвечать. (Полагаю, мне следовало бы руководствоваться собственным советом.)
Говард Своп
47

Один из способов добиться этого - заключить вызов API в обещание, а затем использовать его awaitдля ожидания результата.

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

Вывод:

Your query was <query all users>
ERROR:problem with the query
Timo
источник
Это очень хорошо сделанный пример обертывания функции с помощью обратного вызова, так что вы можете использовать ее с тем, что async/await мне это часто не нужно, поэтому не могу вспомнить, как справиться с этой ситуацией, я копирую это для моих личных заметок / ссылок.
Роберт Арль
24

проверьте это: https://github.com/luciotato/waitfor-ES6

ваш код с wait.for: (требуются генераторы, флаг --harmony)

function* (query) {
  var r = yield wait.for( myApi.exec, 'SomeCommand');
  return r;
}
Лусио М. Тато
источник
10

Если вы не хотите использовать обратный вызов, вы можете использовать модуль «Q».

Например:

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. 

});

Для получения дополнительной информации обратитесь к этому: https://github.com/kriskowal/q

Вишал Патель
источник
9

Если вы хотите, чтобы было очень просто и легко, без каких-либо необычных библиотек, ожидать выполнения функций обратного вызова в узле, прежде чем выполнять какой-либо другой код, это выглядит так:

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

Примечание. Этот ответ, вероятно, не следует использовать в рабочем коде. Это взлом, и вы должны знать о последствиях.

Существует модуль uvrun (обновлен для новых версий Nodejs здесь ), где вы можете выполнить один цикл цикла основного события libuv (который является основным циклом Nodejs).

Ваш код будет выглядеть так:

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

(Вы можете использовать альтернативу uvrun.runNoWait(). Это может избежать некоторых проблем с блокировкой, но занимает 100% ЦП.)

Обратите внимание, что этот подход делает недействительным всю цель Nodejs, то есть иметь все асинхронное и неблокирующее. Кроме того, это может значительно увеличить глубину стека вызовов, что может привести к переполнению стека. Если вы запустите такую ​​функцию рекурсивно, у вас наверняка возникнут проблемы.

Посмотрите другие ответы о том, как изменить дизайн своего кода, чтобы сделать его «правильным».

Это решение, вероятно, полезно только тогда, когда вы проводите тестирование и ESP. хочу иметь синхронизированный и серийный код.

Альберт
источник
5

Начиная с узла 4.8.0 вы можете использовать функцию ES6, называемую генератором. Вы можете следовать этой статье для более глубоких понятий. Но в основном вы можете использовать генераторы и обещания, чтобы выполнить эту работу. Я использую синюю птицу, чтобы обещать и управлять генератором.

Ваш код должен быть в порядке, как в примере ниже.

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

Предположим, у вас есть функция:

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


};

Вы можете использовать обратные вызовы, как это:

fetchPage(1, x = function(next, page) {
if (next) {
    console.log("^^^ CALLBACK -->  fetchPage: " + page);
    fetchPage(page, x);
}
});
Z0LtaR
источник
-1

Это побеждает цель неблокирования ввода-вывода - вы блокируете его, когда он не нуждается в блокировке :)

Вы должны вкладывать свои обратные вызовы вместо того, чтобы заставлять node.js ждать, или вызывать другой обратный вызов внутри обратного вызова, где вам нужен результат r.

Скорее всего, если вам нужно принудительно блокировать, вы думаете о своей архитектуре неправильно.


источник
У меня было подозрение, что это было задом наперед.
Крис
31
Скорее всего, я просто хочу написать быстрый скрипт для http.get()некоторого URL и console.log()его содержимого. Почему я должен перепрыгнуть назад, чтобы сделать это в Node?
Дан Даскалеску
6
@DanDascalescu: И почему я должен объявлять сигнатуры типов, чтобы делать это на статических языках? И почему я должен поместить это в метод main в C-подобных языках? И почему я должен скомпилировать его на скомпилированном языке? То, что вы спрашиваете, является фундаментальным дизайнерским решением в Node.js. Это решение имеет плюсы и минусы. Если вам это не нравится, вы можете использовать другой язык, который больше подходит вашему стилю. Вот почему у нас больше, чем один.
Якоб
@Jakob: решения, которые вы перечислили, действительно неоптимальны. Это не значит, что не существует хороших, таких как использование Mete в волокнах на стороне сервера Meteor, что устраняет проблему ада обратного вызова.
Дан Даскалеску
13
@Jakob: Если лучший ответ на вопрос «почему экосистема X делает общую задачу Y ненужно трудной?» «если вам это не нравится, не используйте экосистему X», то это явный признак того, что разработчики и разработчики экосистемы X отдают приоритет своим собственным эго выше фактического удобства использования их экосистемы. По моему опыту, сообщество Node (в отличие от сообществ Ruby, Elixir и даже PHP) делает все возможное, чтобы затруднять выполнение общих задач. Большое вам спасибо за предложение себя в качестве живого примера этого antipattern.
Джаз
-1

Использование async и await намного проще.

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

  }

}
SaiSurya
источник
API, использованный в вопросе, не возвращает обещание, поэтому вам нужно сначала обернуть его в один… как этот ответ сделал два года назад.
Квентин