278 votes

Appeler une fonction Javascript asynchrone de manière synchrone

Tout d'abord, il s'agit d'un cas très spécifique où l'on s'est trompé de méthode dans le but d'intégrer un appel asynchrone dans une base de code très synchrone qui compte plusieurs milliers de lignes et pour laquelle le temps ne permet pas actuellement d'effectuer les changements nécessaires pour "bien faire les choses". Cela fait mal à chaque fibre de mon être, mais la réalité et les idéaux ne s'accordent pas toujours. Je sais que ça craint.

OK, ceci étant dit, comment faire pour 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, qui ne sont pas viables pour cette solution. J'ai besoin d'un exemple concret de la façon dont on peut le faire bloquer (par exemple, ne PAS laisser la fonction doSomething jusqu'à ce que le callback soit appelé) SANS geler l'interface utilisateur. Si une telle chose est possible en JS.

17 votes

Il n'est tout simplement pas possible de bloquer un navigateur et d'attendre. Ils ne le feront tout simplement pas.

2 votes

Javascript n'a pas de mécanisme de blocage sur la plupart des navigateurs... vous devrez créer un callback qui sera appelé à la fin de l'appel asynchrone pour retourner les données.

0 votes

Je ne pense pas que cela puisse être fait. Javascript (malgré les web workers) est monofilaire - le callback ne peut pas être appelé avant le retour du gestionnaire d'événement actuel. Cela nécessiterait l'équivalent de la fonction async/await de C# en Javascript et je doute qu'une telle bête existe. Je crains que vous ne soyez obligé de réécrire votre code en CPS, aussi ennuyeux que cela puisse être.

155voto

squint Points 28293

"Ne me dis pas comment je dois faire "de la bonne façon" ou autre".

OK. <sup>mais tu devrais vraiment le faire de la bonne façon... ou autre.</sup>

" J'ai besoin d'un exemple concret de comment le faire bloquer .... SANS geler l'interface utilisateur. Si une telle chose est possible en 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 pourrait être de demander à la fonction appelante de vérifier une variable globale, puis de faire en sorte que la fonction de rappel définisse les paramètres suivants data au niveau mondial.

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 ceci suppose que vous pouvez modifier doSomething() . Je ne sais pas si c'est dans les cartes.

S'il peut être modifié, alors je ne vois pas pourquoi vous ne passeriez pas simplement un callback à doSomething() pour être appelé depuis l'autre rappel, mais je ferais mieux d'arrêter avant d'avoir des problèmes ;)


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

function doSomething( func ) {

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

  myAsynchronousCall(param1, callBack);

}

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

Comme votre exemple comprend une fonction de rappel qui est transmise à l'appel asynchrone, la bonne façon de procéder serait de transmettre une fonction à doSomething() à invoquer à partir du callback.

Bien sûr, si c'est la seule chose que le callback fait, vous pouvez simplement passer func directement...

myAsynchronousCall(param1, func);

28 votes

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. L'essentiel est que je ne veux pas quitter doSomething() avant que myAsynchronousCall ait terminé l'appel à la fonction callback. Bleh, ce n'est pas possible, comme je le soupçonnais, j'avais juste besoin de la sagesse rassemblée des Internets pour me soutenir. Merci. :-)

2 votes

@RobertC.Barth : Oui, vos soupçons étaient corrects malheureusement.

0 votes

C'est moi ou seule la version "faite correctement" fonctionne ? La question incluait un appel de retour, avant lequel il devrait y avoir quelque chose qui attende la fin de l'appel asynchrone, ce que la première partie de cette réponse ne couvre pas...

76voto

John Points 4028

Fonctions asynchrones une fonction dans ES2017 pour faire en sorte que le code asynchrone ait l'air synchrone en utilisant promesses (une forme particulière de code asynchrone) et l'option await mot-clé. Remarquez également dans les exemples de code ci-dessous le mot-clé async devant le function qui signifie une fonction async/await. L'adresse await ne fonctionnera pas s'il ne se trouve pas dans une fonction préfixée par le mot-clé async mot-clé. Comme il n'y a actuellement aucune exception à cette règle, cela signifie qu'aucune attente de niveau supérieur ne fonctionnera (une attente de niveau supérieur signifie une attente en dehors de toute fonction). Bien qu'il existe un proposition de niveau supérieur await .

L'ES2017 a été ratifiée (c'est-à-dire finalisée) comme norme pour JavaScript le 27 juin 2017. Async await fonctionne peut-être déjà dans votre navigateur, mais si ce n'est pas le cas, vous pouvez toujours utiliser la fonctionnalité en utilisant un transpilateur javascript comme babel o traceur . Chrome 55 a un support complet des fonctions asynchrones. Donc si vous avez un navigateur plus récent, vous pouvez essayer le code ci-dessous.

Voir table de compatibilité es2017 de kangax pour la compatibilité avec les navigateurs.

Voici un exemple de fonction async await appelée doAsync qui prend trois pauses d'une seconde et imprime la différence de temps après chaque pause par rapport au temps de départ :

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é await est placé devant une valeur de promesse (dans ce cas, la valeur de la promesse est la valeur renvoyée par la fonction doSomethingAsync), le mot-clé await met en pause l'exécution de l'appel de fonction, mais il ne met en pause aucune autre fonction et continue à exécuter d'autres codes jusqu'à ce que la promesse soit résolue. Une fois la promesse résolue, la valeur de la promesse sera déballée et vous pouvez considérer que les expressions await et promise sont maintenant remplacées par cette valeur déballée.

Ainsi, puisque await se contente de mettre en pause, d'attendre puis de déballer une valeur avant d'exécuter le reste de la ligne, vous pouvez l'utiliser dans des boucles for et à l'intérieur d'appels de fonction 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, vous pouvez donc l'utiliser comme une promesse avec un chaînage comme je le fais ci-dessus ou dans une autre fonction asynchrone await.

La fonction ci-dessus attendrait chaque réponse avant d'envoyer une autre requête ; si vous souhaitez envoyer les requêtes 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'envelopper dans un try catch ou sauter le try catch et laisser l'erreur se propager jusqu'à l'appel catch des fonctions async/await. Vous devez faire attention à ne pas laisser les erreurs de promesse non gérées, en particulier dans Node.js. Voici quelques exemples qui illustrent le fonctionnement des 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 aquí vous pouvez voir les propositions finalisées pour les prochaines versions d'ECMAScript.

Une alternative à cela, qui peut être utilisée uniquement avec ES2015 (ES6), consiste à utiliser une fonction spéciale qui enveloppe une fonction de générateur. Les fonctions génératrices ont un mot-clé yield qui peut être utilisé pour reproduire le mot-clé await 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 que ce que fait la fonction async await. Si vous voulez une enveloppe de fonction de générateur qui peut être utilisée pour répliquer la fonction async await, je vous conseille de consulter les sites suivants co.js . Au fait, les fonctions de co, tout comme les fonctions async await, renvoient une promesse. Honnêtement, à ce stade, la compatibilité avec les navigateurs est à peu près la même pour les fonctions génératrices et les fonctions asynchrones, donc si vous voulez seulement la fonctionnalité async await, vous devriez utiliser les fonctions asynchrones sans co.js.

La prise en charge des navigateurs est en fait assez bonne maintenant pour les fonctions asynchrones (depuis 2017) dans tous les principaux navigateurs actuels (Chrome, Safari et Edge) sauf IE.

45voto

Matt Taylor Points 624

Jetez un coup d'œil à JQuery Promises :

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

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

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

Refaire le code :

    var dfd = new jQuery.Deferred();

    function callBack(data) {
       dfd.notify(data);
    }

    // do the async call.
    myAsynchronousCall(param1, callBack);

    function doSomething(data) {
     // do stuff with data...
    }

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

3 votes

+1 pour cette réponse, c'est correct. cependant, je mettrais à jour la ligne avec dfd.notify(data) a dfd.resolve(data)

7 votes

Est-ce un cas où le code donne l'illusion d'être synchrone, sans être réellement asynchrone ?

2 votes

Les promesses ne sont que des callbacks bien organisés :) si vous avez besoin d'un appel asynchrone dans l'initialisation d'un objet, par exemple, les promesses font une petite différence.

10voto

meustrus Points 856

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

Il va certainement geler votre interface utilisateur, donc je suis toujours un opposant quand il s'agit de savoir si ce qu'il est possible de prendre le raccourci que vous devez prendre. Il n'est pas possible de suspendre le One And Only Thread en JavaScript, même si NodeJS vous permet de le bloquer parfois. Aucun callback, événement ou autre élément asynchrone ne pourra être traité jusqu'à la résolution de votre promesse. Donc, à moins que vous, le lecteur, ayez une situation inévitable comme l'OP (ou, dans mon cas, que vous écriviez un shell glorifié script sans callbacks, événements, etc.

Mais voici comment vous pouvez le faire :

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

Ces deux éléments sont une conséquence de la façon dont sync-rpc est mis en œuvre, c'est-à-dire en abusant de require('child_process').spawnSync :

  1. Cela ne fonctionnera pas dans le navigateur.
  2. Les arguments de votre fonction doit être sérialisable. Vos arguments passeront dans et hors de JSON.stringify Les fonctions et les propriétés non dénombrables comme les chaînes de prototypes seront donc perdues.

6voto

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

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

Voici un exemple de code tiré 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;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X