web-dev-qa-db-fra.com

Comment encapsuler les appels de fonctions asynchrones dans une fonction de synchronisation dans Node.js ou Javascript?

Supposons que vous mainteniez une bibliothèque qui expose une fonction getData. Vos utilisateurs l'appellent pour obtenir des données réelles:
var output = getData();
Sous le capot, les données sont enregistrées dans un fichier. Vous avez donc implémenté getData à l'aide de Node.js intégré fs.readFileSync. Il est évident que getData et fs.readFileSync sont des fonctions de synchronisation. Un jour, on vous a demandé de basculer la source de données sous-jacente vers un référentiel tel que MongoDB, auquel vous pouvez uniquement accéder de manière asynchrone. Il vous a également été dit d'éviter de faire chier vos utilisateurs. getData API ne peut pas être modifiée pour renvoyer simplement une promesse ou exiger un paramètre de rappel. Comment répondez-vous à ces deux exigences?

La fonction asynchrone utilisant callback/promise est l’ADN de JavasSript et Node.js. Toute application JS non-triviale est probablement imprégnée de ce style de codage. Mais cette pratique peut facilement conduire à ce qu'on appelle la pyramide de rappel de Doom. Pire encore, si le code d'un appelant dans la chaîne d'appels dépend du résultat de la fonction asynchrone, ce code doit également être encapsulé dans une fonction de rappel, ce qui impose une contrainte de style de codage à l'appelant. De temps en temps, je trouve qu'il est nécessaire d'encapsuler une fonction asynchrone (souvent fournie dans une bibliothèque tierce) dans une fonction de synchronisation afin d'éviter une refactorisation globale massive. La recherche d’une solution sur ce sujet finissait généralement par les paquets Node Fibers ou npm. Mais les fibres ne peuvent tout simplement pas résoudre le problème auquel je suis confronté. Même l'exemple fourni par l'auteur de Fibers illustre cette lacune:

...
Fiber(function() {
    console.log('wait... ' + new Date);
    sleep(1000);
    console.log('ok... ' + new Date);
}).run();
console.log('back in main');

Sortie réelle:

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
back in main
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)

Si la fonction Fibre transforme réellement la fonction asynchrone en veille, le résultat devrait être:

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)
back in main

J'ai créé un autre exemple simple dans JSFiddle et je cherche du code pour obtenir le résultat attendu. J'accepterai une solution qui ne fonctionne que dans Node.js, vous êtes donc libre d'exiger n'importe quel package npm même si vous ne travaillez pas dans JSFiddle.

112
abbr

deasync transforme la fonction asynchrone en synchronisation, implémentée avec un mécanisme de blocage en appelant la boucle d'événement Node.js au niveau de la couche JavaScript. Par conséquent, deasync bloque uniquement l’exécution du code suivant sans bloquer l’ensemble du thread, ni provoquer une attente occupée. Avec ce module, voici la réponse au défi jsFiddle:

function AnticipatedSyncFunction(){
  var ret;
  setTimeout(function(){
      ret = "hello";
  },3000);
  while(ret === undefined) {
    require('deasync').runLoopOnce();
  }
  return ret;    
}


var output = AnticipatedSyncFunction();
//expected: output=hello (after waiting for 3 sec)
console.log("output="+output);
//actual: output=hello (after waiting for 3 sec)

(disclaimer: je suis co-auteur de deasync. Le module a été créé après la publication de cette question et n'a pas trouvé de proposition réalisable.)

99
abbr

Il existe également un module de synchronisation npm. qui est utilisé pour synchroniser le processus d’exécution de la requête.

Lorsque vous souhaitez exécuter des requêtes parallèles de manière synchrone, la restriction de nœud est exécutée car elle n'attend jamais la réponse. Le module Sync est parfaitement adapté à ce type de solution.

Exemple de code

/*require sync module*/
var Sync = require('sync');
    app.get('/',function(req,res,next){
      story.find().exec(function(err,data){
        var sync_function_data = find_user.sync(null, {name: "sanjeev"});
          res.send({story:data,user:sync_function_data});
        });
    });


    /*****sync function defined here *******/
    function find_user(req_json, callback) {
        process.nextTick(function () {

            users.find(req_json,function (err,data)
            {
                if (!err) {
                    callback(null, data);
                } else {
                    callback(null, err);
                }
            });
        });
    }

lien de référence: https://www.npmjs.com/package/sync

5
sanjeev kumar

Si la fonction Fibre transforme vraiment la fonction async en veille en synchronisation

Oui. À l’intérieur de la fibre, la fonction attend avant d’enregistrer ok. Les fibres ne rendent pas les fonctions asynchrones synchrones, mais permettent d'écrire du code d'aspect synchrone utilisant des fonctions asynchrones, puis s'exécutant de manière asynchrone dans un Fiber.

De temps en temps, je trouve nécessaire d'incorporer une fonction asynchrone dans une fonction de synchronisation afin d'éviter une refactorisation globale massive.

Vous ne pouvez pas. Il est impossible de rendre le code asynchrone synchrone. Vous devrez anticiper cela dans votre code global et l'écrire dès le début dans un style asynchrone. Que vous intégriez le code global dans une fibre, utilisiez des promesses, des générateurs de promesses ou de simples rappels, cela dépend de vos préférences.

Mon objectif est de minimiser l'impact sur l'appelant lorsque la méthode d'acquisition des données est passée de la synchronisation à l'async.

Les promesses et les fibres peuvent faire cela.

4
Bergi

Vous devez utiliser les promesses:

const asyncOperation = () => {
    return new Promise((resolve, reject) => {
        setTimeout(()=>{resolve("hi")}, 3000)
    })
}

const asyncFunction = async () => {
    return await asyncOperation();
}

const topDog = () => {
    asyncFunction().then((res) => {
        console.log(res);
    });
}

J'aime plus les définitions de fonction de flèche. Mais toute chaîne de la forme "() => {...}" pourrait également être écrite sous la forme "function () {...}"

Donc topDog n'est pas asynchrone malgré l'appel d'une fonction asynchrone.

enter image description here

EDIT: Je réalise souvent que vous devez insérer une fonction asynchrone dans une fonction de synchronisation dans un contrôleur. Voici un tour de passe-passe pour ces situations:

const getDemSweetDataz = (req, res) => {
    (async () => {
        try{
            res.status(200).json(
                await asyncOperation()
            );
        }
        catch(e){
            res.status(500).json(serviceResponse); //or whatever
        }
    })() //So we defined and immediately called this async function.
}

En utilisant ceci avec des rappels, vous pouvez faire un wrapping qui n'utilise pas les promesses:

const asyncOperation = () => {
    return new Promise((resolve, reject) => {
        setTimeout(()=>{resolve("hi")}, 3000)
    })
}

const asyncFunction = async (callback) => {
    let res = await asyncOperation();
    callback(res);
}

const topDog = () => {
    let callback = (res) => {
        console.log(res);
    };

    (async () => {
        await asyncFunction(callback)
    })()
}

En appliquant cette astuce à un EventEmitter, vous pouvez obtenir les mêmes résultats. Définissez le programme d'écoute de EventEmitter dans lequel j'ai défini le rappel et émettez l'événement où j'ai appelé le rappel.

2
user2485309

La synchronisation de code Node.js est essentielle dans quelques aspects tels que la base de données. Mais l’avantage réel de Node.js réside dans le code async. Comme il s'agit d'un thread unique non bloquant.

nous pouvons le synchroniser en utilisant une fonctionnalité importante. Fiber () Utilisez wait () et defer (), nous appelons toutes les méthodes en utilisant wait (). puis remplacez les fonctions de rappel par defer ().

Code Async Normal. Ceci utilise les fonctions CallBack.

function add (var a, var b, function(err,res){
       console.log(res);
});

 function sub (var res2, var b, function(err,res1){
           console.log(res);
    });

 function div (var res2, var b, function(err,res3){
           console.log(res3);
    });

Synchronisez le code ci-dessus à l'aide de Fiber (), wait () et defer ()

fiber(function(){
     var obj1 = await(function add(var a, var b,defer()));
     var obj2 = await(function sub(var obj1, var b, defer()));
     var obj3 = await(function sub(var obj2, var b, defer()));

});

J'espère que cela aidera. Merci

1
Mohan Ramakrishna

Je ne trouve pas de scénario qui ne puisse être résolu en utilisant des fibres de nœud. L'exemple que vous avez fourni à l'aide de nœud-fibres se comporte comme prévu. La clé consiste à exécuter tout le code pertinent à l'intérieur d'une fibre afin d'éviter de démarrer une nouvelle fibre dans des positions aléatoires.

Voyons un exemple: Disons que vous utilisez un framework, qui est le point d’entrée de votre application (vous ne pouvez pas modifier ce framework). Ce framework charge les modules de nodejs en tant que plugins et appelle certaines méthodes sur les plugins. Disons que ce cadre n'accepte que les fonctions synchrones et n'utilise pas de fibres par lui-même.

Il existe une bibliothèque que vous souhaitez utiliser dans l'un de vos plugins, mais cette bibliothèque est asynchrone et vous ne souhaitez pas la modifier non plus.

Le fil principal ne peut pas être cédé si aucune fibre n'est en cours d'exécution, mais vous pouvez toujours créer des plugins à l'aide de fibres! Il suffit de créer une entrée de wrapper qui démarre l'ensemble de la structure à l'intérieur d'une fibre afin de pouvoir exécuter l'exécution à partir des plugins.

Inconvénient: si le cadre utilise setTimeout ou Promises en interne, il échappera au contexte de la fibre. Vous pouvez contourner ce problème en moquant setTimeout, Promise.then et tous les gestionnaires d'événements.

C’est donc comment vous pouvez donner une fibre jusqu’à ce que Promise soit résolu. Ce code prend une fonction asynchrone (retour de la promesse) et reprend la fibre une fois la promesse résolue:

framework-entry.js

console.log(require("./my-plugin").run());

async-lib.js

exports.getValueAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Async Value");
    }, 100);
  });
};

my-plugin.js

const Fiber = require("fibers");

function fiberWaitFor(promiseOrValue) {
  var fiber = Fiber.current, error, value;
  Promise.resolve(promiseOrValue).then(v => {
    error = false;
    value = v;
    fiber.run();
  }, e => {
    error = true;
    value = e;
    fiber.run();
  });
  Fiber.yield();
  if (error) {
    throw value;
  } else {
    return value;
  }
}

const asyncLib = require("./async-lib");

exports.run = () => {
  return fiberWaitFor(asyncLib.getValueAsync());
};

my-entry.js

require("fibers")(() => {
  require("./framework-entry");
}).run();

Lorsque vous exécutez node framework-entry.js, une erreur se produira: Error: yield() called with no fiber running. Si vous exécutez node my-entry.js, cela fonctionne comme prévu.

0
Tamas Hegedus

Vous ne devriez pas regarder ce qui se passe autour de l'appel qui crée la fibre mais plutôt ce qui se passe à l'intérieur la fibre. Une fois que vous êtes à l'intérieur de la fibre, vous pouvez programmer en style de synchronisation. Par exemple:

 function f1 () {
 console.log ('wait ...' + new Date); 
 sleep (1000); 
 console.log (' ok ... '+ nouvelle date); 
} 
 
 fonction f2 () {
 f1 (); 
 f1 (); 
} 
 
 Fibre (fonction () {
 F2 (); 
}) Run (); 

À l'intérieur de la fibre, vous appelez f1, f2 et sleep comme s'ils étaient synchronisés.

Dans une application Web typique, vous créerez la fibre dans votre répartiteur de requêtes HTTP. Une fois que vous avez terminé, vous pouvez écrire toute la logique de traitement de votre demande dans un style synchronisé, même si elle appelle des fonctions asynchrones (fs, bases de données, etc.).

0
Bruno Jouhier