web-dev-qa-db-fra.com

Constructeur asynchrone

Comment puis-je gérer au mieux une situation comme celle-ci? 

J'ai un constructeur qui prend un certain temps à compléter.

var Element = function Element(name){
   this.name = name;
   this.nucleus = {};

   this.load_nucleus(name); // This might take a second.
}

var oxygen = new Element('oxygen');
console.log(oxygen.nucleus); // Returns {}, because load_nucleus hasn't finished.

Je vois trois options, chacune d’entre elles sortant de l’ordinaire.

One , ajoute un rappel au constructeur.

var Element = function Element(name, fn){
   this.name = name;
   this.nucleus = {};

   this.load_nucleus(name, function(){
      fn(); // Now continue.
   });
}

Element.prototype.load_nucleus(name, fn){
   fs.readFile(name+'.json', function(err, data) {
      this.nucleus = JSON.parse(data); 
      fn();
   });
}

var oxygen = new Element('oxygen', function(){  
   console.log(oxygen.nucleus);
});

Two , utilisez EventEmitter pour émettre un événement "chargé".

var Element = function Element(name){
   this.name = name;
   this.nucleus = {};

   this.load_nucleus(name); // This might take a second.
}

Element.prototype.load_nucleus(name){
   var self = this;
   fs.readFile(name+'.json', function(err, data) {
      self.nucleus = JSON.parse(data); 
      self.emit('loaded');
   });
}

util.inherits(Element, events.EventEmitter);

var oxygen = new Element('oxygen');
oxygen.once('loaded', function(){
   console.log(this.nucleus);
});

Ou trois , bloque le constructeur.

var Element = function Element(name){
   this.name = name;
   this.nucleus = {};

   this.load_nucleus(name); // This might take a second.
}

Element.prototype.load_nucleus(name, fn){
   this.nucleus = JSON.parse(fs.readFileSync(name+'.json'));
}

var oxygen = new Element('oxygen');
console.log(oxygen.nucleus)

Mais je n'ai jamais rien vu de tel auparavant. 

Quelles autres options ai-je?

50
Luke Burns

Compte tenu de la nécessité d'éviter le blocage dans le nœud, l'utilisation d'événements ou de rappels n'est pas si étrange(1).

Avec une légère modification de Two, vous pouvez le fusionner avec One:

var Element = function Element(name, fn){
    this.name = name;
    this.nucleus = {};

    if (fn) this.on('loaded', fn);

    this.load_nucleus(name); // This might take a second.
}

...

Cependant, comme le fs.readFile dans votre exemple, les API de nœud de base (ou au moins) suivent souvent le modèle de fonctions statiques qui expose l'instance lorsque les données sont prêtes:

var Element = function Element(name, nucleus) {
    this.name = name;
    this.nucleus = nucleus;
};

Element.create = function (name, fn) {
    fs.readFile(name+'.json', function(err, data) {
        var nucleus = err ? null : JSON.parse(data);
        fn(err, new Element(name, nucleus));
    });
};

Element.create('oxygen', function (err, elem) {
    if (!err) {
        console.log(elem.name, elem.nucleus);
    }
});

(1) La lecture d’un fichier JSON ne devrait pas être très longue. Si c'est le cas, un changement de système de stockage est peut-être nécessaire pour les données.

11
Jonathan Lonowski

Mise à jour 2: Voici un exemple mis à jour utilisant une méthode de fabrique asynchrone. N.B. cela nécessite le nœud 8 ou Babel s'il est exécuté dans un navigateur.

class Element {
    constructor(nucleus){
        this.nucleus = nucleus;
    }

    static async createElement(){
        const nucleus = await this.loadNucleus();
        return new Element(nucleus);
    }

    static async loadNucleus(){
        // do something async here and return it
        return 10;
    }
}

async function main(){
    const element = await Element.createElement();
    // use your element
}

main();

Mise à jour: Le code ci-dessous a été voté plusieurs fois. Cependant, je trouve cette approche beaucoup mieux d'utiliser une méthode statique: https://stackoverflow.com/a/24686979/2124586

Version ES6 utilisant des promesses

class Element{
    constructor(){
        this.some_property = 5;
        this.nucleus;

        return new Promise((resolve) => {
            this.load_nucleus().then((nucleus) => {
                this.nucleus = nucleus;
                resolve(this);
            });
        });
    }

    load_nucleus(){
        return new Promise((resolve) => {
            setTimeout(() => resolve(10), 1000)
        });
    }
}

//Usage
new Element().then(function(instance){
    // do stuff with your instance
});
25
tim

Une chose que vous pouvez faire est de précharger tous les noyaux (peut-être inefficace; je ne sais pas combien de données il contient). L'autre, que je recommanderais si le préchargement n'est pas une option, impliquerait un rappel avec un cache pour sauvegarder les noyaux chargés. Voici cette approche:

Element.nuclei = {};

Element.prototype.load_nucleus = function(name, fn){
   if ( name in Element.nuclei ) {
       this.nucleus = Element.nuclei[name];
       return fn();
   }
   fs.readFile(name+'.json', function(err, data) {
      this.nucleus = Element.nuclei[name] = JSON.parse(data); 
      fn();
   });
}
3
Will

Ceci est une mauvaise conception de code.

Le problème principal réside dans le rappel de votre instance qui n’exécute pas toujours le "retour", c’est ce que je veux dire.

var MyClass = function(cb) {
  doAsync(function(err) {
    cb(err)
  }

  return {
    method1: function() { },
    method2: function() { }
  }
}

var _my = new MyClass(function(err) {
  console.log('instance', _my) // < _my is still undefined
  // _my.method1() can't run any methods from _my instance
})
_my.method1() // < it run the function, but it's not yet inited

Ainsi, la bonne conception de code consiste à appeler explicitement la méthode "init" (ou dans votre cas, "load_nucleus") après instance de la classe

var MyClass = function() {
  return {
    init: function(cb) {
      doAsync(function(err) {
        cb(err)
      }
    },
    method1: function() { },
    method2: function() { }
  }
}

var _my = new MyClass()
_my.init(function(err) { 
   if(err) {
     console.error('init error', err)
     return
   } 
   console.log('inited')
  // _my.method1()
})
2
Simone Sanfratello

J'ai développé un constructeur asynchrone:

function Myclass(){
 return (async () => {
     ... code here ...
     return this;
 })();
}

(async function() { 
 let s=await new Myclass();
 console.log("s",s)
})();
  • async retourne une promesse
  • Les fonctions de flèche passent 'this' telles quelles
  • il est possible de renvoyer quelque chose d’autre lorsqu’on en crée un autre (vous obtenez toujours un nouvel objet vide dans cette variable. Si vous appelez la fonction sans nouvelle, vous obtenez l’original, comme peut-être window ou global ou son objet de maintien).
  • il est possible de renvoyer la valeur de retour de la fonction asynchrone appelée en utilisant wait.
  • pour utiliser wait dans le code normal, vous devez encapsuler les appels avec une fonction anonyme asynchrone, appelée instantanément. (la fonction appelée retourne promesse et le code continue)

ma 1ère itération était:

peut-être juste ajouter un rappel

appeler une fonction asynchrone anonyme, puis appeler le rappel.

function Myclass(cb){
 var asynccode=(async () => { 
     await this.something1();
     console.log(this.result)
 })();

 if(cb)
    asynccode.then(cb.bind(this))
}

ma 2ème itération était:

essayons avec une promesse au lieu d'un rappel. Je me suis dit: une promesse étrange qui rendait une promesse, et cela a fonctionné. .. alors la prochaine version est juste une promesse.

function Myclass(){
 this.result=false;
 var asynccode=(async () => {
     await new Promise (resolve => setTimeout (()=>{this.result="ok";resolve()}, 1000))
     console.log(this.result)
     return this;
 })();
 return asynccode;
}


(async function() { 
 let s=await new Myclass();
 console.log("s",s)
})();

callback basé sur javascript ancien

function Myclass(cb){
 var that=this;
 var cb_wrap=function(data){that.data=data;cb(that)}
 getdata(cb_wrap)
}

new Myclass(function(s){

});
2
Shimon Doodkin

J'extrais les portions asynchrones dans une méthode fluide. Par convention, je les appelle ensemble.

class FooBar {
  constructor() {
    this.foo = "foo";
  }
  
  async create() {
    this.bar = await bar();

    return this;
  }
}

async function bar() {
  return "bar";
}

async function main() {
  const foobar = await new FooBar().create(); // two-part constructor
  console.log(foobar.foo, foobar.bar);
}

main(); // foo bar

J'ai essayé une approche d'usine statique en enveloppant new FooBar(), par exemple. FooBar.create(), mais cela ne fonctionnait pas bien avec l'héritage. Si vous étendez FooBar en FooBarChild, FooBarChild.create() renverra toujours un FooBar. Alors qu'avec mon approche, new FooBarChild().create() renverra une FooBarChild et il est facile de configurer une chaîne d'héritage avec create().

0
Bill Barnes