Cómo hacer que una función espere hasta que se haya llamado una devolución de llamada usando node.js

267

Tengo una función simplificada que se ve así:

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

Básicamente, quiero que llame myApi.execy devuelva la respuesta que se da en la devolución de llamada lambda. Sin embargo, el código anterior no funciona y simplemente regresa de inmediato.

Solo por un intento muy hostil, probé lo siguiente, que no funcionó, pero al menos te haces una idea de lo que estoy tratando de lograr:

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

Básicamente, ¿cuál es una buena forma de 'nodo.js / evento impulsado' de hacer esto? Quiero que mi función espere hasta que se llame la devolución de llamada y luego devuelva el valor que se le pasó.

Chris
fuente
3
¿O lo estoy haciendo por el camino equivocado aquí, y debería llamar a otra devolución de llamada, en lugar de devolver una respuesta?
Chris
Esta es, en mi opinión, la mejor explicación de SO por qué el ciclo ocupado no funciona.
bluenote10
No trates de esperar. Simplemente llame a la siguiente función (dependiente de devolución de llamada) al final de la devolución de llamada en sí
Atul

Respuestas:

282

La forma de "hacer un buen nodo.js / evento" es no esperar .

Como casi todo lo demás cuando se trabaja con sistemas controlados por eventos como el nodo, su función debe aceptar un parámetro de devolución de llamada que se invocará cuando se complete el cálculo. La persona que llama no debe esperar a que el valor sea "devuelto" en el sentido normal, sino que debe enviar la rutina que manejará el valor resultante:

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

Entonces no lo usas así:

var returnValue = myFunction(query);

Pero así:

myFunction(query, function(returnValue) {
  // use the return value here instead of like a regular (non-evented) return value
});
Jakob
fuente
55
Vale genial. ¿Qué pasa si myApi.exec nunca llamó a la devolución de llamada? ¿Cómo lo haría para que se llame a la devolución de llamada después de unos 10 segundos con un valor de error que dice que cronometró nuestro o algo así?
Chris
55
O mejor aún (se agregó un cheque para que no se pueda invocar la devolución de llamada dos veces): jsfiddle.net/LdaFw/1
Jakob
148
Está claro que el no bloqueo es el estándar en node / js, sin embargo, hay momentos en los que se desea el bloqueo (por ejemplo, bloqueo en stdin). Incluso el nodo tiene métodos de "bloqueo" (ver todos los fs sync*métodos). Como tal, creo que esta sigue siendo una pregunta válida. ¿Hay una buena manera de lograr el bloqueo en el nodo aparte de la espera ocupada?
nategood
77
Una respuesta tardía al comentario de @nategood: puedo pensar en un par de formas; demasiado para explicar en este comentario, pero googlealos. Recuerde que Node no está hecho para ser bloqueado, por lo que estos no son perfectos. Piense en ellos como sugerencias. De todos modos, aquí va: (1) Use C para implementar su función y publíquela en NPM para usarla. Eso es lo syncque hacen los métodos. (2) Use fibras, github.com/laverdet/node-fibers , (3) Use promesas, por ejemplo, la biblioteca Q, (4) Use una capa delgada en la parte superior de javascript, que parece bloqueante, pero se compila de forma asíncrona, like maxtaco.github.com/coffee-script
Jakob
106
Es muy frustrante cuando las personas responden una pregunta con "no deberías hacer eso". Si uno quiere ser útil y responder una pregunta, eso es algo que debe hacer. Pero decirme inequívocamente que no debería hacer algo es simplemente hostil. Hay un millón de razones diferentes por las que alguien querría llamar a una rutina de forma síncrona o asíncrona. Esta fue una pregunta sobre cómo hacerlo. Si proporciona consejos útiles sobre la naturaleza de la API mientras proporciona la respuesta, eso es útil, pero si no proporciona una respuesta, ¿por qué molestarse en responder? (Creo que realmente debería seguir mi propio consejo).
Howard Swope
46

Una forma de lograr esto es envolver la llamada API en una promesa y luego usarla awaitpara esperar el resultado.

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

Salida:

Your query was <query all users>
ERROR:problem with the query
Timo
fuente
Este es un ejemplo muy bien hecho de envolver una función con una devolución de llamada para que pueda usarla. async/await A menudo no lo necesito, así que tenga problemas para recordar cómo manejar esta situación, estoy copiando esto para mis notas / referencias personales.
Robert
10

Si no desea utilizar la devolución de llamada, puede utilizar el módulo "Q".

Por ejemplo:

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. 

});

Para obtener más información, consulte esto: https://github.com/kriskowal/q

vishal patel
fuente
9

Si desea que sea muy simple y fácil, sin bibliotecas sofisticadas, esperar a que las funciones de devolución de llamada se ejecuten en el nodo, antes de ejecutar otro código, es así:

//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();
}
Marquinho Peli
fuente
5

Nota: Esta respuesta probablemente no debería usarse en el código de producción. Es un truco y debes saber sobre las implicaciones.

Existe el módulo uvrun (actualizado para las versiones más recientes de Nodejs aquí ) donde puede ejecutar una ronda de bucle único del bucle de evento principal de libuv (que es el bucle principal de Nodejs).

Su código se vería así:

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

(Puede utilizarlo de forma alternativa uvrun.runNoWait(). Eso podría evitar algunos problemas con el bloqueo, pero requiere un 100% de CPU).

Tenga en cuenta que este enfoque invalida el propósito de Nodejs, es decir, tener todo asíncrono y sin bloqueo. Además, podría aumentar mucho la profundidad de su pila de llamadas, por lo que podría terminar con desbordamientos de pila. Si ejecuta dicha función de forma recursiva, definitivamente tendrá problemas.

Vea las otras respuestas sobre cómo rediseñar su código para hacerlo "bien".

Esta solución aquí probablemente solo sea útil cuando realice pruebas y esp. desea tener un código sincronizado y en serie.

Albert
fuente
5

Desde el nodo 4.8.0 puede usar la función de ES6 llamada generador. Puede seguir este artículo para conceptos más profundos. Pero básicamente puedes usar generadores y promesas para hacer este trabajo. Estoy usando bluebird para promisificar y administrar el generador.

Su código debería estar bien como en el ejemplo a continuación.

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));
Douglas Soares
fuente
1

suponiendo que tenga una función:

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


};

puede utilizar devoluciones de llamada como esta:

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

Eso anula el propósito de no bloquear IO: lo está bloqueando cuando no necesita bloqueo :)

Debe anidar sus devoluciones de llamada en lugar de obligar a node.js a esperar, o llamar a otra devolución de llamada dentro de la devolución de llamada donde necesita el resultado r.

Lo más probable es que, si necesita forzar el bloqueo, esté pensando en que su arquitectura es incorrecta.


fuente
Tenía la sospecha de que tenía esto al revés.
Chris
31
Lo más probable es que solo quiera escribir un script rápido para http.get()alguna URL y console.log()su contenido. ¿Por qué tengo que saltar hacia atrás para hacer eso en Node?
Dan Dascalescu
66
@DanDascalescu: ¿Y por qué tengo que declarar firmas de tipo para hacerlo en lenguajes estáticos? ¿Y por qué tengo que ponerlo en un método principal en lenguajes tipo C? ¿Y por qué tengo que compilarlo en un lenguaje compilado? Lo que cuestionas es una decisión de diseño fundamental en Node.js. Esa decisión tiene pros y contras. Si no te gusta, puedes usar otro idioma que se adapte mejor a tu estilo. Por eso tenemos más de uno.
Jakob
@Jakob: las soluciones que ha enumerado son realmente subóptimas. Eso no significa que no haya buenas, como el uso de Nodo en el servidor por parte de Meteor en fibras, lo que elimina el problema del infierno de devolución de llamada.
Dan Dascalescu
13
@Jakob: Si la mejor respuesta a "¿por qué el ecosistema X hace que la tarea común Y sea innecesariamente difícil?" es "si no te gusta, no uses el ecosistema X", entonces esa es una buena señal de que los diseñadores y mantenedores del ecosistema X están priorizando sus propios egos por encima de la usabilidad real de su ecosistema. Según mi experiencia, la comunidad Node (en contraste con las comunidades Ruby, Elixir e incluso PHP) hace todo lo posible para dificultar las tareas comunes. Muchas gracias por ofrecerte como un ejemplo vivo de ese antipatrón.
Jazz
-1

Usar async y esperar es mucho más fácil.

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
fuente
La API utilizada en la pregunta no devuelve una promesa, por lo que necesitaría incluirla primero ... como hizo esta respuesta hace dos años.
Quentin