Comment faire attendre une fonction jusqu'à ce qu'un rappel soit appelé à l'aide de node.js


267

J'ai une fonction simplifiée qui ressemble à ceci:

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

Fondamentalement, je veux qu'il appelle myApi.execet renvoie la réponse qui est donnée dans le lambda de rappel. Cependant, le code ci-dessus ne fonctionne pas et revient simplement immédiatement.

Juste pour une tentative très hackish, j'ai essayé ce qui n'a pas fonctionné, mais au moins vous avez une idée de ce que j'essaie de réaliser:

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

Fondamentalement, quelle est la bonne façon de procéder à cet égard? Node.js / event driven? Je veux que ma fonction attende que le rappel soit appelé, puis renvoie la valeur qui lui a été transmise.


3
Ou suis-je en train de m'y prendre complètement dans le mauvais sens, et devrais-je appeler un autre rappel, plutôt que de renvoyer une réponse?
Chris

C'est à mon avis la meilleure explication SO pourquoi la boucle occupée ne fonctionne pas.
bluenote10

N'essayez pas d'attendre. Il suffit d'appeler la fonction suivante (dépendante du rappel) à la fin du rappel lui
Atul

Réponses:


282

La bonne façon de faire ceci: node.js / event driven est de ne pas attendre .

Comme presque tout le reste lorsque vous travaillez avec des systèmes pilotés par des événements comme le nœud, votre fonction doit accepter un paramètre de rappel qui sera invoqué une fois le calcul terminé. L'appelant ne doit pas attendre que la valeur soit "renvoyée" dans le sens normal, mais plutôt envoyer la routine qui gérera la valeur résultante:

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

Donc, vous ne l'utilisez pas comme ceci:

var returnValue = myFunction(query);

Mais comme ça:

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

5
D'accord, super. Et si myApi.exec n'a jamais appelé le rappel? Comment pourrais-je faire en sorte que le rappel soit appelé après disons 10 secondes avec une valeur d'erreur disant qu'il a chronométré notre ou quelque chose?
Chris

5
Ou mieux encore (ajout d'un chèque pour que le rappel ne puisse pas être appelé
Jakob

148
Il est clair que le non-blocage est la norme dans node / js, mais il y a certainement des moments où le blocage est souhaité (par exemple le blocage sur stdin). Même le nœud a des méthodes de "blocage" (voir toutes les fs sync*méthodes). En tant que tel, je pense que c'est toujours une question valable. Existe-t-il un bon moyen de bloquer le nœud en dehors de l'attente occupée?
nategood

7
Une réponse tardive au commentaire de @nategood: je peux penser à deux façons; trop à expliquer dans ce commentaire, mais google eux. N'oubliez pas que Node n'est pas fait pour être bloqué, donc ce n'est pas parfait. Considérez-les comme des suggestions. Quoi qu'il en soit, voici: (1) Utilisez C pour implémenter votre fonction et la publier sur NPM afin de l'utiliser. Voilà ce que font les syncméthodes. (2) Utilisez des fibres, github.com/laverdet/node-fibers , (3) Utilisez des promesses, par exemple la bibliothèque Q, (4) Utilisez une couche mince au-dessus de javascript, qui semble bloquante, mais se compile en async, comme maxtaco.github.com/coffee-script
Jakob

106
C'est tellement frustrant quand les gens répondent à une question par «vous ne devriez pas faire ça». Si l'on veut être utile et répondre à une question, c'est une chose à faire. Mais me dire sans équivoque que je ne devrais pas faire quelque chose est tout simplement hostile. Il y a un million de raisons différentes pour lesquelles quelqu'un voudrait appeler une routine de manière synchrone ou asynchrone. C'était une question sur la façon de le faire. Si vous fournissez des conseils utiles sur la nature de l'API tout en fournissant la réponse, cela est utile, mais si vous ne fournissez pas de réponse, pourquoi vous embêter à répondre. (Je suppose que je devrais vraiment diriger mes propres conseils.)
Howard Swope

47

Une façon d'y parvenir consiste à encapsuler l'appel d'API dans une promesse, puis à utiliser awaitpour attendre le résultat.

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

Production:

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

Ceci est un exemple très bien fait d'encapsuler une fonction avec un rappel afin que vous puissiez l'utiliser avec async/await Je n'en ai pas souvent besoin, alors ayez du mal à vous souvenir comment gérer cette situation, je le copie pour mes notes / références personnelles.
robert arles


10

Si vous ne souhaitez pas utiliser le rappel, vous pouvez utiliser le module "Q".

Par exemple:

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. 

});

Pour plus d'informations, référez-vous à ceci: https://github.com/kriskowal/q


9

Si vous le voulez très simple et facile, pas de bibliothèques fantaisies, attendre que les fonctions de rappel soient exécutées dans le nœud, avant d'exécuter un autre code, c'est comme ceci:

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

Remarque: Cette réponse ne devrait probablement pas être utilisée dans le code de production. C'est un hack et vous devez en connaître les implications.

Il y a le module uvrun (mis à jour pour les nouvelles versions de Nodejs ici ) où vous pouvez exécuter un seul tour de boucle de la boucle d'événement principal libuv (qui est la boucle principale de Nodejs).

Votre code ressemblerait à ceci:

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

(Vous pouvez utiliser une alternative uvrun.runNoWait(). Cela pourrait éviter certains problèmes de blocage, mais prend 100% de CPU.)

Notez que cette approche invalide en quelque sorte le but de Nodejs, c'est-à-dire que tout est asynchrone et non bloquant. En outre, cela pourrait augmenter considérablement la profondeur de votre pile d'appels, vous pourriez donc vous retrouver avec des débordements de pile. Si vous exécutez une telle fonction récursivement, vous rencontrerez certainement des problèmes.

Voir les autres réponses sur la façon de reconcevoir votre code pour le faire "bien".

Cette solution ici n'est probablement utile que lorsque vous effectuez des tests et esp. voulez avoir synchronisé et code série.


5

Depuis le nœud 4.8.0, vous pouvez utiliser la fonctionnalité d'ES6 appelée générateur. Vous pouvez suivre cet article pour des concepts plus approfondis. Mais en gros, vous pouvez utiliser des générateurs et promets de faire ce travail. J'utilise Bluebird à promisify et gérer le générateur.

Votre code devrait être correct comme dans l'exemple ci-dessous.

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

en supposant que vous ayez une fonction:

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


};

vous pouvez utiliser des rappels comme celui-ci:

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

-1

Cela va à l'encontre du but des E / S non bloquantes - vous le bloquez quand il n'a pas besoin de le bloquer :)

Vous devez imbriquer vos rappels au lieu de forcer node.js à attendre, ou appeler un autre rappel à l'intérieur du rappel où vous avez besoin du résultat r.

Si vous devez forcer le blocage, il y a de fortes chances que vous pensiez mal à votre architecture.


J'avais un soupçon que j'avais ça à l'envers.
Chris

31
Les chances sont, je veux juste écrire un script rapide sur http.get()une URL et console.log()son contenu. Pourquoi dois-je sauter en arrière pour le faire dans Node?
Dan Dascalescu

6
@DanDascalescu: Et pourquoi dois-je déclarer des signatures de type pour le faire dans des langages statiques? Et pourquoi dois-je le mettre dans une méthode principale dans des langages de type C? Et pourquoi dois-je le compiler dans un langage compilé? Ce que vous remettez en question est une décision de conception fondamentale dans Node.js. Cette décision a des avantages et des inconvénients. Si vous ne l'aimez pas, vous pouvez utiliser une autre langue qui correspond mieux à votre style. C'est pourquoi nous en avons plus d'un.
Jakob

@Jakob: les solutions que vous avez répertoriées sont en effet sous-optimales. Cela ne signifie pas qu'il n'y en a pas de bonnes, comme l'utilisation côté serveur de Node dans les fibres par Meteor, ce qui élimine le problème de l'enfer de rappel.
Dan Dascalescu

13
@Jakob: Si la meilleure réponse à "pourquoi l'écosystème X rend la tâche commune Y inutilement difficile?" est "si vous ne l'aimez pas, n'utilisez pas l'écosystème X", alors c'est un signe fort que les concepteurs et les responsables de l'écosystème X accordent la priorité à leurs propres egos au-dessus de l'utilisabilité réelle de leur écosystème. D'après mon expérience, la communauté Node (contrairement aux communautés Ruby, Elixir et même PHP) fait tout son possible pour rendre les tâches courantes difficiles. Merci BEAUCOUP de vous offrir comme un exemple vivant de cet antipattern.
Jazz

-1

Utiliser async et attendre est beaucoup plus facile.

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

  }

}

L'API utilisée dans la question ne renvoie aucune promesse, vous devez donc la boucler en une première… comme cette réponse l'a fait il y a deux ans.
Quentin
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.