Appeler une fonction Javascript asynchrone de manière synchrone

222

Tout d'abord, il s'agit d'un cas très spécifique de le faire de la mauvaise façon exprès pour moderniser un appel asynchrone dans une base de code très synchrone qui fait plusieurs milliers de lignes et le temps ne permet pas actuellement de faire les changements pour "faire bien. " Cela fait mal à chaque fibre de mon être, mais la réalité et les idéaux ne s'imbriquent souvent pas. Je sais que ça craint.

OK, cela à l'écart, comment puis-je faire en sorte que je puisse:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

  // block here and return data when the callback is finished
  return data;
}

Les exemples (ou leur absence) utilisent tous des bibliothèques et / ou des compilateurs, tous deux non viables pour cette solution. J'ai besoin d'un exemple concret de la façon de le faire bloquer (par exemple, NE PAS laisser la fonction doSomething jusqu'à ce que le rappel soit appelé) SANS geler l'interface utilisateur. Si une telle chose est possible dans JS.

Robert C. Barth
la source
16
Il n'est tout simplement pas possible de bloquer le navigateur et d'attendre. Ils ne le feront tout simplement pas.
Pointy
2
javascript dosent ayant des mécanismes de blocage sur la plupart des navigateurs ... vous voudrez créer un rappel qui sera appelé lorsque l'appel asynchrone se terminera pour renvoyer les données
Nadir Muzaffar
8
Vous demandez un moyen de dire au navigateur "Je sais que je viens de vous dire d'exécuter cette fonction précédente de manière asynchrone, mais je ne le pensais pas vraiment!". Pourquoi voudriez-vous même que ce soit possible?
Wayne
2
Merci Dan pour la modification. Je n'étais pas strictement impoli, mais votre formulation est meilleure.
Robert C.Barth
2
@ RobertC.Barth C'est désormais possible avec JavaScript aussi. Les fonctions d'attente async n'ont pas encore été ratifiées dans la norme, mais devraient l'être dans ES2017. Voir ma réponse ci-dessous pour plus de détails.
John

Réponses:

135

"ne me dites pas comment je dois le faire" de la bonne façon "ou autre"

D'ACCORD. mais vous devez vraiment le faire de la bonne façon ... ou autre

"J'ai besoin d'un exemple concret de la façon de le bloquer ... SANS geler l'interface utilisateur. Si une telle chose est possible dans JS."

Non, il est impossible de bloquer le JavaScript en cours d'exécution sans bloquer l'interface utilisateur.

Étant donné le manque d'informations, il est difficile de proposer une solution, mais une option peut être de demander à la fonction appelante d'effectuer une interrogation pour vérifier une variable globale, puis de définir le rappel datasur la valeur globale.

function doSomething() {

      // callback sets the received data to a global var
  function callBack(d) {
      window.data = d;
  }
      // start the async
  myAsynchronousCall(param1, callBack);

}

  // start the function
doSomething();

  // make sure the global is clear
window.data = null

  // start polling at an interval until the data is found at the global
var intvl = setInterval(function() {
    if (window.data) { 
        clearInterval(intvl);
        console.log(data);
    }
}, 100);

Tout cela suppose que vous pouvez modifier doSomething(). Je ne sais pas si c'est dans les cartes.

S'il peut être modifié, je ne sais pas pourquoi vous ne passeriez pas simplement un rappel doSomething()pour être appelé de l'autre rappel, mais je ferais mieux de m'arrêter avant d'avoir des ennuis. ;)


Oh, que diable. Vous avez donné un exemple qui suggère que cela peut être fait correctement, donc je vais montrer cette solution ...

function doSomething( func ) {

  function callBack(d) {
    func( d );
  }

  myAsynchronousCall(param1, callBack);

}

doSomething(function(data) {
    console.log(data);
});

Étant donné que votre exemple inclut un rappel qui est passé à l'appel asynchrone, la bonne façon serait de passer une fonction à appeler doSomething()à partir du rappel.

Bien sûr, si c'est la seule chose que le rappel fait, vous passeriez simplement funcdirectement ...

myAsynchronousCall(param1, func);
1106925
la source
22
Oui, je sais comment le faire correctement, j'ai besoin de savoir comment / si cela peut être fait incorrectement pour la raison spécifique indiquée. Le nœud est que je ne veux pas quitter doSomething () jusqu'à ce que myAsynchronousCall termine l'appel à la fonction de rappel. Bleh, cela ne peut pas être fait, comme je le soupçonnais, j'avais juste besoin de la sagesse recueillie des Internets pour me soutenir. Je vous remercie. :-)
Robert C. Barth
2
@ RobertC.Barth: Oui, vos soupçons étaient malheureusement corrects.
Est-ce moi ou seulement la version "fait correctement" qui fonctionne? La question comprenait un appel de retour, avant lequel il devrait y avoir quelque chose qui attend la fin de l'appel asynchrone, que cette première partie de cette réponse ne couvre pas ...
ravemir
@ravemir: La réponse indique qu'il n'est pas possible de faire ce qu'il veut. C'est la partie importante à comprendre. En d'autres termes, vous ne pouvez pas effectuer un appel asynchrone et renvoyer une valeur sans bloquer l'interface utilisateur. Donc, la première solution est un hack laid utilisant une variable globale et une interrogation pour voir si cette variable a été modifiée. La deuxième version est la bonne façon.
1
@Leonardo: C'est la fonction mystérieuse qui est appelée dans la question. Fondamentalement, il représente tout ce qui exécute le code de manière asynchrone et produit un résultat qui doit être reçu. Cela pourrait donc être comme une demande AJAX. Vous passez la callbackfonction à la myAsynchronousCallfonction, qui fait son travail asynchrone et appelle le rappel une fois terminé. Voici une démo.
60

Les fonctions asynchrones , une fonctionnalité d'ES2017 , permettent de synchroniser le code asynchrone en utilisant des promesses (une forme particulière de code asynchrone) et le awaitmot clé. Notez également dans les exemples de code ci-dessous le mot-clé asyncdevant le functionmot-clé qui signifie une fonction asynchrone / attente. Le awaitmot-clé ne fonctionnera pas sans être dans une fonction pré-fixée avec le asyncmot - clé. Comme il n'y a actuellement aucune exception à cela, cela signifie qu'aucune attente de niveau supérieur ne fonctionnera (le niveau supérieur attend, ce qui signifie une attente en dehors de toute fonction). Bien qu'il existe une proposition de haut niveauawait .

ES2017 a été ratifié (c'est-à-dire finalisé) en tant que norme pour JavaScript le 27 juin 2017. L'attente asynchrone peut déjà fonctionner dans votre navigateur, mais sinon, vous pouvez toujours utiliser la fonctionnalité à l'aide d'un transpilateur javascript comme babel ou traceur . Chrome 55 prend entièrement en charge les fonctions asynchrones. Donc, si vous avez un navigateur plus récent, vous pourrez peut-être essayer le code ci-dessous.

Voir le tableau de compatibilité es2017 de kangax pour la compatibilité du navigateur.

Voici un exemple de fonction d'attente asynchrone appelée doAsyncqui prend trois pauses d'une seconde et imprime le décalage horaire après chaque pause à partir de l'heure de début:

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

async function doAsync () {
  var start = Date.now(), time;
  console.log(0);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
}

doAsync();

Lorsque le mot-clé wait est placé avant une valeur promise (dans ce cas, la valeur promise est la valeur retournée par la fonction doSomethingAsync), le mot-clé wait suspendra l'exécution de l'appel de fonction, mais ne suspendra aucune autre fonction et continuera exécuter un autre code jusqu'à ce que la promesse soit résolue. Une fois la promesse résolue, elle déballera la valeur de la promesse et vous pouvez penser que l'expression attendre et promettre est désormais remplacée par cette valeur non emballée.

Ainsi, puisque wait s'arrête juste attend puis décompresse une valeur avant d'exécuter le reste de la ligne, vous pouvez l'utiliser dans les boucles et les appels de fonction internes comme dans l'exemple ci-dessous qui collecte les différences de temps attendues dans un tableau et imprime le tableau.

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this calls each promise returning function one after the other
async function doAsync () {
  var response = [];
  var start = Date.now();
  // each index is a promise returning function
  var promiseFuncs= [doSomethingAsync, doSomethingAsync, doSomethingAsync];
  for(var i = 0; i < promiseFuncs.length; ++i) {
    var promiseFunc = promiseFuncs[i];
    response.push(await promiseFunc() - start);
    console.log(response);
  }
  // do something with response which is an array of values that were from resolved promises.
  return response
}

doAsync().then(function (response) {
  console.log(response)
})

La fonction asynchrone elle-même renvoie une promesse afin que vous puissiez l'utiliser comme une promesse avec un chaînage comme je le fais ci-dessus ou dans une autre fonction d'attente asynchrone.

La fonction ci-dessus attendrait chaque réponse avant d'envoyer une autre demande si vous souhaitez envoyer les demandes simultanément, vous pouvez utiliser Promise.all .

// no change
function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

// no change
function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this function calls the async promise returning functions all at around the same time
async function doAsync () {
  var start = Date.now();
  // we are now using promise all to await all promises to settle
  var responses = await Promise.all([doSomethingAsync(), doSomethingAsync(), doSomethingAsync()]);
  return responses.map(x=>x-start);
}

// no change
doAsync().then(function (response) {
  console.log(response)
})

Si la promesse est éventuellement rejetée, vous pouvez l'encapsuler dans un catch catch ou ignorer le try catch et laisser l'erreur se propager à l'appel des fonctions async / wait. Vous devez faire attention à ne pas laisser les erreurs de promesse non gérées, en particulier dans Node.js. Voici quelques exemples qui montrent comment fonctionnent les erreurs.

function timeoutReject (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      reject(new Error("OOPS well you got an error at TIMESTAMP: " + Date.now()));
    }, time)
  })
}

function doErrorAsync () {
  return timeoutReject(1000);
}

var log = (...args)=>console.log(...args);
var logErr = (...args)=>console.error(...args);

async function unpropogatedError () {
  // promise is not awaited or returned so it does not propogate the error
  doErrorAsync();
  return "finished unpropogatedError successfully";
}

unpropogatedError().then(log).catch(logErr)

async function handledError () {
  var start = Date.now();
  try {
    console.log((await doErrorAsync()) - start);
    console.log("past error");
  } catch (e) {
    console.log("in catch we handled the error");
  }
  
  return "finished handledError successfully";
}

handledError().then(log).catch(logErr)

// example of how error propogates to chained catch method
async function propogatedError () {
  var start = Date.now();
  var time = await doErrorAsync() - start;
  console.log(time - start);
  return "finished propogatedError successfully";
}

// this is what prints propogatedError's error.
propogatedError().then(log).catch(logErr)

Si vous allez ici, vous pouvez voir les propositions finies pour les prochaines versions d'ECMAScript.

Une alternative à cela qui peut être utilisée avec seulement ES2015 (ES6) est d'utiliser une fonction spéciale qui encapsule une fonction de générateur. Les fonctions génératrices ont un mot-clé yield qui peut être utilisé pour répliquer le mot-clé wait avec une fonction environnante. Le mot-clé yield et la fonction de générateur sont beaucoup plus généraux et peuvent faire beaucoup plus de choses que ce que fait la fonction d'attente asynchrone. Si vous voulez une enveloppe de fonction de générateur qui peut être utilisé pour async répliquées attendre que je vérifierais co.js . Par ailleurs, la fonction de co, tout comme les fonctions d'attente asynchrone, renvoie une promesse. Honnêtement, à ce stade, la compatibilité du navigateur est à peu près la même pour les fonctions de générateur et les fonctions asynchrones, donc si vous voulez juste la fonctionnalité d'attente asynchrone, vous devez utiliser les fonctions Async sans co.js.

La prise en charge du navigateur est en fait assez bonne maintenant pour les fonctions Async (à partir de 2017) dans tous les principaux navigateurs actuels (Chrome, Safari et Edge) sauf IE.

John
la source
2
J'aime cette réponse
ycomp
1
jusqu'où nous sommes arrivés :)
Derek
3
C'est une excellente réponse, mais pour le problème des affiches originales, je pense que cela ne fait que déplacer le problème d'un niveau. Disons qu'il transforme doSomething en une fonction asynchrone avec une attente à l'intérieur. Cette fonction renvoie maintenant une promesse et est asynchrone, il devra donc faire face au même problème à nouveau dans tous les appels de cette fonction.
dpwrussell
1
@dpwrussell c'est vrai, il y a un fluage de fonctions asynchrones et des promesses dans la base de code. La meilleure façon de résoudre les promesses de ramper dans tout est d'écrire des rappels synchrones, il n'y a aucun moyen de retourner une valeur asynchrone de manière synchrone à moins que vous ne fassiez quelque chose d'extrêmement bizarre et controversé comme ce twitter.com/sebmarkbage/status/941214259505119232 que je ne fais pas recommander. Je vais ajouter une modification à la fin de la question pour répondre plus complètement à la question telle qu'elle a été posée et pas seulement répondre au titre.
John
C'est une excellente réponse +1 et tout, mais écrit tel quel, je ne vois pas comment cela est moins compliqué que d'utiliser des rappels.
Altimus Prime
47

Jetez un œil aux promesses JQuery:

http://api.jquery.com/promise/

http://api.jquery.com/jQuery.when/

http://api.jquery.com/deferred.promise/

Refactoriser le code:

    var dfd = new jQuery.Deferred ();


    fonction callBack (données) {
       dfd.notify (données);
    }

    // effectue l'appel asynchrone.
    myAsynchronousCall (param1, callBack);

    fonction doSomething (données) {
     // faire des trucs avec les données ...
    }

    $ .when (dfd) .then (doSomething);


Matt Taylor
la source
3
+1 pour cette réponse, c'est correct. Cependant, je voudrais mettre à jour la ligne avec dfd.notify(data)àdfd.resolve(data)
Jason
7
S'agit-il d'un cas où le code donne l'illusion d'être synchrone, sans en réalité PAS être asynchrone?
saurshaz
2
les promesses sont des rappels IMO juste bien organisés :) si vous avez besoin d'un appel asynchrone dans disons une initialisation d'objet, alors les promesses font une petite différence.
webduvet
10
Les promesses ne sont pas synchronisées.
Vans S
6

Il existe une solution de contournement intéressante sur http://taskjs.org/

Il utilise des générateurs qui sont nouveaux en javascript. Il n'est donc actuellement pas implémenté par la plupart des navigateurs. Je l'ai testé dans Firefox, et pour moi, c'est une bonne façon d'envelopper la fonction asynchrone.

Voici un exemple de code du projet GitHub

var { Deferred } = task;

spawn(function() {
    out.innerHTML = "reading...\n";
    try {
        var d = yield read("read.html");
        alert(d.responseText.length);
    } catch (e) {
        e.stack.split(/\n/).forEach(function(line) { console.log(line) });
        console.log("");
        out.innerHTML = "error: " + e;
    }

});

function read(url, method) {
    method = method || "GET";
    var xhr = new XMLHttpRequest();
    var deferred = new Deferred();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status >= 400) {
                var e = new Error(xhr.statusText);
                e.status = xhr.status;
                deferred.reject(e);
            } else {
                deferred.resolve({
                    responseText: xhr.responseText
                });
            }
        }
    };
    xhr.open(method, url, true);
    xhr.send();
    return deferred.promise;
}
George Vinokhodov
la source
3

Vous pouvez forcer le JavaScript asynchrone dans NodeJS à être synchrone avec sync-rpc .

Cependant, cela gèlera certainement votre interface utilisateur, donc je suis toujours un naysayer pour savoir s'il est possible de prendre le raccourci dont vous avez besoin. Il n'est pas possible de suspendre le One And Only Thread en JavaScript, même si NodeJS vous permet parfois de le bloquer. Aucun rappel, événement, rien d'asynchrone ne pourra être traité jusqu'à ce que votre promesse soit résolue. Donc, à moins que vous, le lecteur, n'ayez une situation inévitable comme l'OP (ou, dans mon cas, écrivez un script shell glorifié sans rappels, événements, etc.), NE FAITES PAS CECI!

Mais voici comment procéder:

./calling-file.js

var createClient = require('sync-rpc');
var mySynchronousCall = createClient(require.resolve('./my-asynchronous-call'), 'init data');

var param1 = 'test data'
var data = mySynchronousCall(param1);
console.log(data); // prints: received "test data" after "init data"

./my-asynchronous-call.js

function init(initData) {
  return function(param1) {
    // Return a promise here and the resulting rpc client will be synchronous
    return Promise.resolve('received "' + param1 + '" after "' + initData + '"');
  };
}
module.exports = init;

LIMITES:

Ce sont à la fois une conséquence de la façon dont sync-rpcest mis en œuvre, qui est en abusant require('child_process').spawnSync:

  1. Cela ne fonctionnera pas dans le navigateur.
  2. Les arguments de votre fonction doivent être sérialisables. Vos arguments passeront dans et hors JSON.stringify, donc les fonctions et les propriétés non énumérables comme les chaînes de prototypes seront perdues.
meustrus
la source
1

Vous pouvez également le convertir en rappels.

function thirdPartyFoo(callback) {    
  callback("Hello World");    
}

function foo() {    
  var fooVariable;

  thirdPartyFoo(function(data) {
    fooVariable = data;
  });

  return fooVariable;
}

var temp = foo();  
console.log(temp);
Nikhil
la source
0

Ce que vous voulez est maintenant possible. Si vous pouvez exécuter le code asynchrone dans un travailleur de service et le code synchrone dans un travailleur Web, vous pouvez demander au travailleur Web d'envoyer un XHR synchrone au travailleur de service, et pendant que le travailleur de service fait les choses asynchrones, le travailleur Web le fil attendra. Ce n'est pas une excellente approche, mais cela pourrait fonctionner.

Vous voyez peut-être ce nom.
la source
-4

L'idée que vous espérez réaliser peut être rendue possible si vous modifiez un peu l'exigence

Le code ci-dessous est possible si votre runtime prend en charge la spécification ES6.

En savoir plus sur les fonctions asynchrones

async function myAsynchronousCall(param1) {
    // logic for myAsynchronous call
    return d;
}

function doSomething() {

  var data = await myAsynchronousCall(param1); //'blocks' here until the async call is finished
  return data;
}
eragon512
la source
4
Firefox donne l'erreur: SyntaxError: await is only valid in async functions and async generators. Sans oublier que param1 n'est pas défini (et même pas utilisé).
Harvey