Як змусити функцію чекати, поки виклик зворотного дзвінка за допомогою node.js


266

У мене є спрощена функція, яка виглядає приблизно так:

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 / event-driven" робити це? Я хочу, щоб моя функція зачекала, поки викликається зворотний виклик, а потім поверне значення, яке було передано йому.


3
Або я збираюся про це зовсім неправильним шляхом, і я повинен викликати інший зворотний дзвінок, а не повертати відповідь?
Кріс

На мій погляд, це найкраще пояснення, чому зайнятий цикл не працює.
bluenote10

Не намагайтеся чекати. Просто зателефонуйте до наступної функції (залежно від зворотного дзвінка) в кінці самого зворотного дзвінка
Atul

Відповіді:


282

Спосіб "хороший node.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
});

5
ОК здорово. Що робити, якщо myApi.exec ніколи не викликав зворотний виклик? Як я можу зробити так, щоб зворотний виклик дзвонив через скажімо 10 секунд зі значенням помилки, кажучи, що він приурочений до нашого чи щось?
Кріс

5
Або ще краще (додано чек, щоб зворотний виклик не можна було викликати двічі): jsfiddle.net/LdaFw/1
Якоб

148
Зрозуміло, що неблокування є стандартом у node / 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
Це так неприємно, коли люди відповідають на запитання "ти цього не повинен робити". Якщо ви хочете бути корисними і відповісти на запитання, це важливо зробити. Але сказати мені однозначно, що я не повинен щось робити, це просто недоброзичливо. Є мільйон різних причин, чому хтось хотів би викликати розпорядок синхронно чи асинхронно. Це було питання про те, як це зробити. Якщо ви надаєте корисні поради щодо природи апі під час надання відповіді, це корисно, але якщо ви не надаєте відповіді, чому б не турбувати відповіді. (Напевно, я дійсно повинен очолити власну пораду.)
Говард Своп

46

Один із способів цього досягти - це ввімкнути виклик 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

Це дуже добре зроблений приклад упаковки функції з зворотним дзвоном, тому ви можете використовувати її з async/await мені часто це не потрібно, тому виникають проблеми з запам'ятовуванням, як впоратися з цією ситуацією, я копіюю це для своїх особистих приміток / довідок.
Роберт Арль


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, тобто мати все асинхронізацію та не блокування. Крім того, це може значно збільшити глибину вашої ставки виклику, так що у вас може виникнути переповнення стека. Якщо ви запускаєте таку функцію рекурсивно, ви, безумовно, зіткнетеся з проблемами.

Дивіться інші відповіді про те, як переробити свій код, щоб зробити це "правильно".

Це рішення, ймовірно, корисне лише тоді, коли ви робите тестування та прослуховування. хочуть синхронізувати і серійний код.


5

З вузла 4.8.0 ви можете використовувати функцію ES6, що називається генератором. Ви можете дотримуватися цієї статті для більш глибоких понять. Але в основному ви можете використовувати генератори та обіцянки виконати цю роботу. Я використовую bluebird для розмежування та управління генератором.

Ваш код повинен бути добре, як на прикладі нижче.

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

-1

Це перешкоджає неблокуванню IO - ви блокуєте його, коли його не потрібно блокувати :)

Ви повинні вкладати зворотні дзвінки замість того, щоб примушувати node.js чекати або викликати інший зворотний дзвінок всередині зворотного дзвінка, де вам потрібен результат r.

Швидше за все, якщо вам потрібно примусити блокувати, ви думаєте про свою архітектуру неправильно.


У мене була підозра, що я мав це навколо назад.
Кріс

31
Швидше за все, я просто хочу написати швидкий скрипт до http.get()якоїсь URL-адреси та console.log()її вмісту. Чому я повинен перестрибувати назад, щоб це зробити в Node?
Дан Даскалеску

6
@DanDascalescu: І чому я повинен оголошувати підписи типу, щоб це робити статичними мовами? І чому я повинен вводити його в основний метод на мовах, подібних С? І чому я повинен складати це мовою, що склалася? Що ви ставите під сумнів - це фундаментальне дизайнерське рішення в Node.js. У цього рішення є плюси і мінуси. Якщо вам це не подобається, ви можете використовувати іншу мову, яка краще відповідає вашому стилю. Ось чому у нас їх більше.
Якоб

@Jakob: рішення, які ви перераховували, справді є неоптимальними. Це не означає, що немає хороших, як-от використання сервера Meteor на Node у волокнах, що усуває пекельну проблему зворотного виклику.
Дан Даскалеску

13
@Jakob: Якщо найкраща відповідь на питання "чому екосистема X робить загальне завдання Y зайвим завданням?" це "якщо вам це не подобається, не використовуйте екосистему X", то це є важливим знаком того, що дизайнери та обслуговуючі екосистеми X надають пріоритет власним его над фактичною зручністю їх екосистеми. З мого досвіду, спільнота Node (на відміну від спільнот Ruby, Elixir і навіть PHP) виходить зі шляху, щоб ускладнити загальні завдання. Дякую ТАКІ МНОГО за те, що ти запропонував себе як живий приклад антипатріану.
Джаз

-1

Використовувати асинхронізацію та чекати - це набагато простіше.

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, який використовується у запитанні, не повертає обіцянки, тому вам потрібно буде прописати його в першу чергу… як ця відповідь була зроблена два роки тому.
Квентін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.