web-dev-qa-db-fra.com

fonctions du constructeur async dans TypeScript?

J'ai quelques réglages à faire lors d'un constructeur, mais il semble que ce ne soit pas autorisé

 no async const

Ce qui signifie que je ne peux pas utiliser:

 await

Sinon, comment devrais-je faire cela?

Actuellement, j'ai quelque chose d'extérieur comme ça, mais ce n'est pas garanti de fonctionner dans l'ordre que je veux?

async function run() {
  let topic;
  debug("new TopicsModel");
  try {
    topic = new TopicsModel();
  } catch (err) {
    debug("err", err);
  }

  await topic.setup();
30
dcsan

Le constructeur doit renvoyer une instance de la classe qu'il "construit"; il est donc impossible de renvoyer Promise <...> et de l'attendre.

Vous pouvez:

  1. Rendre votre configuration publique async
  2. Ne l'appelez pas du constructeur. 
  3. Appelez-le chaque fois que vous souhaitez "finaliser" la construction d'un objet

    async function run() 
    {
        let topic;
        debug("new TopicsModel");
        try 
        {
            topic = new TopicsModel();
            await topic.setup();
        } 
        catch (err) 
        {
            debug("err", err);
        }
    }
    
23
Amid

Modèle de conception de constructeur asynchrone

Si vous ne pouvez pas mettre l'objet dans une promesse, mettez une promesse dans l'objet.

Exposer la promesse d'achèvement de la construction en tant que propriété de l'objet construit. Lorsque la partie asynchrone de votre construction se termine, cela devrait résoudre la promesse.

Peu importe que .then(...) soit exécuté avant ou après la résolution de la promesse. La spécification de la promesse indique que l'invocation de then sur une promesse déjà résolue exécute simplement le gestionnaire immédiatement.

class Foo {
  public Ready: Promise.IThenable<any>;
  constructor() {
    ...
    this.Ready = new Promise((resolve, reject) => {
      $.ajax(...).then(result => {
        // use result
        resolve(undefined);
      }).fail(reject);
    });
  }
}

var foo = new Foo();
foo.Ready.then(() => {
  //do stuff that needs foo to be ready, eg apply bindings
});

Pourquoi resolve(undefined); au lieu de resolve();? Parce que ES6. Ajustez au besoin pour convenir à votre cible.

Dans un commentaire, il a été suggéré que j’aurais dû cadrer cette solution avec await afin de répondre plus directement à la question posée. 

Cette solution est médiocre car elle autorise uniquement le code dans l'étendue qui suit immédiatement l'instruction wait à attendre la fin. Exposer un objet de promesse en tant que propriété d'un objet initialisé de manière asynchrone signifie que tout code, n'importe où, peut garantir que l'initialisation est complète, car la promesse est étendue partout où l'objet le est, de sorte qu'il est garanti disponible partout où le risque existe.

En outre, il est peu probable que l’utilisation du mot-clé wait soit un produit livrable pour tout projet qui n’est pas une mission universitaire démontrant l’utilisation du mot-clé wait.


C'est un travail original de moi. J'ai conçu ce modèle parce que je n'étais pas satisfait des usines externes et d'autres solutions de ce type. Malgré des recherches pendant un certain temps, je n’ai pas trouvé d’état de la technique pour ma solution. Je revendique donc le mérite en tant que créateur de ce schéma jusqu’à ce que ce soit contesté.

Dans un commentaire, @suhas suggère d'utiliser await plutôt que .then et cela fonctionnerait, mais il serait moins compatible. En ce qui concerne la compatibilité, TypeScript a changé depuis que j'ai écrit ceci et vous devez maintenant déclarer public Ready: Promise<any>

12
Peter Wone

Je connais son calme ancien mais une autre option est d'avoir une usine qui va créer l'objet et attendre son initialisation:

// Declare the class
class A {

  // Declare class constructor
  constructor() {

    // We didn't finish the async job yet
    this.initialized = false;

    // Simulates async job, it takes 5 seconds to have it done
    setTimeout(() => {
      this.initialized = true;
    }, 5000);
  }

  // do something usefull here - thats a normal method
  usefull() {
    // but only if initialization was OK
    if (this.initialized) {
      console.log("I am doing something usefull here")

    // otherwise throw error which will be catched by the promise catch
    } else {
      throw new Error("I am not initialized!");
    }
  }

}

// factory for common, extensible class - thats the reason of the constructor parameter
// it can be more sophisticated and accept also params for constructor and pass them there
// also, the timeout is just example, it will wait about 10s (1000 x 10ms iterations
function factory(construct) {

  // create a promise
  var aPromise = new Promise(
    function(resolve, reject) {

      // construct the object here
      var a = new construct();

      // setup simple timeout
      var timeout = 1000;

      // called in 10ms intervals to check if the object is initialized
      function waiter() {

        if (a.initialized) {
          // if initialized, resolve the promise
          resolve(a);
        } else {

          // check for timeout - do another iteration after 10ms or throw exception
          if (timeout > 0) {     
            timeout--;
            setTimeout(waiter, 10);            
          } else {            
            throw new Error("Timeout!");            
          }

        }
      }

      // call the waiter, it will return almost immediately
      waiter();
    }
  );

  // return promise of object being created and initialized
  return aPromise;
}


// this is some async function to create object of A class and do something with it
async function createObjectAndDoSomethingUsefull() {

  // try/catch to capture exceptions during async execution
  try {
    // create object and wait until its initialized (promise resolved)
    var a = await factory(A);
    // then do something usefull
    a.usefull();
  } catch(e) {
    // if class instantiation failed from whatever reason, timeout occured or usefull was called before the object finished its initialization
    console.error(e);
  }

}

// now, perform the action we want
createObjectAndDoSomethingUsefull();

// spagetti code is done here, but async probably still runs
7
Fis

Utilisez plutôt une méthode d'usine asynchrone.

class MyClass {
   private mMember: Something;

   constructor() {
      this.mMember = await SomeFunctionAsync(); // error
   }
}

Devient:

class MyClass {
   private mMember: Something;

   // make private if possible; I can't in TS 1.8
   constructor() {
   }

   public static CreateAsync = async () => {
      const me = new MyClass();

      me.mMember = await SomeFunctionAsync();

      return me;
   };
}

Cela signifiera que vous devrez attendre la construction de ce type d'objets, mais cela devrait déjà être implicite du fait que vous êtes dans la situation où vous devez attendre quelque chose pour les construire de toute façon.

Vous pouvez faire autre chose, mais je suppose que ce n’est pas une bonne idée:

// probably BAD
class MyClass {
   private mMember: Something;

   constructor() {
      this.LoadAsync();
   }

   private LoadAsync = async () => {
      this.mMember = await SomeFunctionAsync();
   };
}

Cela peut fonctionner et je n'ai jamais eu de problème réel auparavant, mais cela semble être dangereux pour moi, car votre objet ne sera pas complètement initialisé lorsque vous commencerez à l'utiliser.

3
Dave Cousineau

Vous pouvez choisir de laisser complètement l'attente de l'équation. Vous pouvez l'appeler depuis le constructeur si vous en avez besoin. L'avertissement est que vous devez traiter toutes les valeurs de retour dans la fonction setup/initialise, pas dans le constructeur.

cela fonctionne pour moi, en utilisant angular 1.6.3.

import { module } from "angular";
import * as R from "ramda";
import cs = require("./checkListService");

export class CheckListController {

    static $inject = ["$log", "$location", "ICheckListService"];
    checkListId: string;

    constructor(
        public $log: ng.ILogService,
        public $loc: ng.ILocationService,
        public checkListService: cs.ICheckListService) {
        this.initialise();
    }

    /**
     * initialise the controller component.
     */
    async initialise() {
        try {
            var list = await this.checkListService.loadCheckLists();
            this.checkListId = R.head(list).id.toString();
            this.$log.info(`set check list id to ${this.checkListId}`);
         } catch (error) {
            // deal with problems here.
         }
    }
}

module("app").controller("checkListController", CheckListController)
2
Jim

Utilisez une méthode d'installation asynchrone qui renvoie l'instance

J'ai eu un problème similaire dans le cas suivant: comment instancier une classe 'Foo' avec une instance d'une classe 'FooSession' ou avec un objet 'fooSessionParams', sachant que créer une fooSession à partir d'un objet fooSessionParams est une fonction asynchrone? Je voulais instancier soit en faisant:

let foo = new Foo(fooSession);

ou

let foo = await new Foo(fooSessionParams);

et je ne voulais pas d’usine parce que les deux usages auraient été trop différents. Mais comme nous le savons, nous ne pouvons pas retourner une promesse d’un constructeur (et la signature de retour est différente). Je l'ai résolu de cette façon:

class Foo {
    private fooSession: FooSession;

    constructor(fooSession?: FooSession) {
        if (fooSession) {
            this.fooSession = fooSession;
        }
    }

    async setup(fooSessionParams: FooSessionParams): Promise<Foo> {
        this.fooSession = await getAFooSession(fooSessionParams);
        return this;
    }
}

La partie intéressante est où la méthode async de configuration retourne l'instance elle-même . Ensuite, si j'ai une instance 'FooSession', je peux l'utiliser de cette façon:

let foo = new Foo(fooSession);

Et si je n'ai pas d'instance 'FooSession', je peux configurer 'foo' de l'une des manières suivantes:

let foo = await new Foo().setup(fooSessionParams);

(La sorcière est ma façon préférée car elle est proche de ce que je voulais en premier).

let foo = new Foo();
await foo.setup(fooSessionParams);

Comme alternative, je pourrais aussi ajouter la méthode statique:

    static async getASession(fooSessionParams: FooSessionParams): FooSession {
        let fooSession: FooSession = await getAFooSession(fooSessionParams);
        return fooSession;
    }

et instancier de cette façon:

let foo = new Foo(await Foo.getASession(fooSessionParams));

C'est principalement une question de style…

2
Abakhan

J'ai trouvé une solution qui ressemble à

export class SomeClass {
  private initialization;

  // Implement async constructor
  constructor() {
    this.initialization = this.init();
  }

  async init() {
    await someAsyncCall();
  }

  async fooMethod() {
    await this.initialization();
    // ...some other stuff
  }

  async barMethod() {
    await this.initialization();
    // ...some other stuff
  }

Cela fonctionne car les promesses qui async/attendent peuvent être résolues plusieurs fois avec la même valeur.

0
Paul Flame

Ou vous pouvez simplement vous en tenir au vrai modèle ASYNC sans trop compliquer la configuration. 9 fois sur 10, cela revient à une conception asynchrone ou synchrone. Par exemple, j'ai un composant React qui avait besoin de la même chose si j'étais en train d'initialiser les variables d'état dans un rappel de promesse dans le constructeur. Il s'avère que tout ce que j'avais à faire pour contourner l'exception de données null était simplement de configurer un objet d'état vide, puis de le définir dans le rappel asynchrone. Par exemple, voici une lecture Firebase avec une promesse retournée et un rappel:

        this._firebaseService = new FirebaseService();
        this.state = {data: [], latestAuthor: '', latestComment: ''};

        this._firebaseService.read("/comments")
        .then((data) => {
            const dataObj = data.val();
            const fetchedComments = dataObj.map((e: any) => {
                return {author: e.author, text: e.text}
            });

            this.state = {data: fetchedComments, latestAuthor: '', latestComment: ''};

        });

En adoptant cette approche, mon code conserve son comportement AJAX sans compromettre le composant avec une exception null, car l'état est configuré avec les valeurs par défaut (objet vide et chaînes vides) avant le rappel. L'utilisateur peut voir une liste vide pendant une seconde mais ensuite, elle est rapidement remplie. Mieux encore serait d’appliquer un spinner pendant le chargement des données. J'entends souvent des personnes suggérer des solutions trop compliquées, comme c'est le cas ici, mais le flux initial doit être réexaminé.

0
Jason Rice