web-dev-qa-db-fra.com

Typescript async/wait ne met pas à jour la vue AngularJS

J'utilise TypeScript 2.1 (version développeur) pour transpiler async/wait en ES5.

J'ai remarqué qu'après avoir changé une propriété liée à la vue dans ma fonction asynchrone, la vue n'est pas mise à jour avec la valeur actuelle, donc chaque fois que je dois appeler $ scope. $ Apply () à la fin de la fonction.

Exemple de code async:

async testAsync() {
     await this.$timeout(2000);
     this.text = "Changed";
     //$scope.$apply(); <-- would like to omit this
}

Et la nouvelle valeur text n'est plus affichée dans cette vue.

Existe-t-il une solution de contournement qui évite de devoir appeler manuellement $ scope. $ Apply () à chaque fois?

21
monoh_

Les réponses ici sont correctes car AngularJS ne connaît pas la méthode. Vous devez donc "informer" Angular des valeurs mises à jour.

Personnellement, j'utiliserais $q pour le comportement asynchrone au lieu d'utiliser await en tant que "La manière Angular".

Vous pouvez encapsuler des méthodes non Angular avec $ q assez facilement, c.-à-d.

function doAThing()
{
    var defer = $q.defer();
    // Note that this method takes a `parameter` and a callback function
    someMethod(parameter, (someValue) => {
        $q.resolve(someValue)
    });

    return defer.promise;
}

Vous pouvez ensuite l'utiliser comme si

this.doAThing().then(someValue => {
    this.memberValue = someValue;
});

Toutefois, si vous souhaitez continuer avec await, il existe un meilleur moyen que d'utiliser $apply, dans ce cas, et d'utiliser $digest. Ainsi

async testAsync() {
   await this.$timeout(2000);
   this.text = "Changed";
   $scope.$digest(); <-- This is now much faster :)
}

$scope.$digest est préférable dans ce cas car $scope.$apply effectuera une vérification en blanc (méthode angulaire de détection des modifications) pour toutes les valeurs liées sur toutes les étendues. Cela peut coûter cher en performances, surtout si vous avez plusieurs liaisons. $scope.$digest n'effectuera cependant qu'un contrôle sur les valeurs liées dans le $scope actuel, ce qui le rendra beaucoup plus performant.

8
Chris

Ceci peut être fait facilement avec angular-async-await extension:

class SomeController {
  constructor($async) {
    this.testAsync = $async(this.testAsync.bind(this));
  }

  async testAsync() { ... }
}

Comme on peut le constater, tout ce qu'il fait est encapsulant la fonction de retour de promesse avec un wrapper qui appelle $rootScope.$apply() ensuite .

Il n’existe aucun moyen fiable de déclencher automatiquement Digest sur la fonction async; cela aurait pour conséquence de pirater à la fois le framework et l’implémentation Promise. Il n'existe aucun moyen de le faire pour la fonction native async (TypeScript es2017 target), car elle repose sur une implémentation de promesse interne et non sur Promise global. Plus important encore, cette méthode serait inacceptable car il ne s'agit pas d'un comportement attendu par défaut. Un développeur doit en avoir le plein contrôle et attribuer ce comportement explicitement.

Étant donné que testAsync est appelé plusieurs fois et que le seul endroit où il est appelé est testsAsync, la synthèse automatique dans testAsync end entraînerait la synthèse du courrier indésirable. Bien qu’une manière appropriée serait de déclencher un résumé une fois, après testsAsync.

Dans ce cas, $async serait appliqué uniquement à testsAsync et non à testAsync lui-même:

class SomeController {
  constructor($async) {
    this.testsAsync = $async(this.testsAsync.bind(this));
  }

  private async testAsync() { ... }

  async testsAsync() {
    await Promise.all([this.testAsync(1), this.testAsync(2), ...]);
    ...
  }
}
7
estus

Comme @basarat , l’ES6 natif Promise ne connaît pas le cycle de résumé.

Ce que vous pourriez faire est de laisser TypeScript utiliser la promesse de service $q au lieu de la promesse native ES6. 

De cette façon, vous n'aurez pas besoin d'appeler $scope.$apply()

angular.module('myApp')
    .run(['$window', '$q', ($window, $q) =>  {
        $window.Promise = $q;
    }]);
3
Joel Raju

J'ai mis en place un violon mettant en valeur le comportement souhaité. On peut le voir ici: Promises with AngularJS . Veuillez noter qu’il utilise une série de promesses résolues après 1000 ms, une fonction asynchrone et un Promise.race et qu’il n’exige toujours que 4 cycles de résumé (ouvre la console).

Je vais réitérer quel était le comportement souhaité:

  • pour permettre l'utilisation de fonctions asynchrones comme dans JavaScript natif; cela signifie qu'aucune autre bibliothèque tierce, comme $async
  • pour déclencher automatiquement le nombre minimum de cycles de digestion

Comment cela a-t-il été réalisé?

Dans ES6, nous avons reçu une superbe fonctionnalité appelée Proxy . Cet objet permet de définir un comportement personnalisé pour les opérations fondamentales (recherche de propriétés, affectation, énumération, appel de fonction, etc.).

Cela signifie que nous pouvons envelopper la Promise dans un proxy qui, lorsque la promesse est résolue ou rejetée, déclenche un cycle de résumé, uniquement si nécessaire. Comme nous avons besoin d’un moyen de déclencher le cycle de résumé, ce changement est ajouté au moment de l’exécution d’AngularJS.

function($rootScope) {
  function triggerDigestIfNeeded() {
    // $applyAsync acts as a debounced funciton which is exactly what we need in this case
    // in order to get the minimum number of digest cycles fired.
    $rootScope.$applyAsync();
  };

  // This principle can be used with other native JS "features" when we want to integrate 
  // then with AngularJS; for example, fetch.
  Promise = new Proxy(Promise, {
    // We are interested only in the constructor function
    construct(target, argumentsList) {
      return (() => {
        const promise = new target(...argumentsList);

        // The first thing a promise does when it gets resolved or rejected, 
        // is to trigger a digest cycle if needed
        promise.then((value) => {
          triggerDigestIfNeeded();

          return value;
        }, (reason) => {
          triggerDigestIfNeeded();

          return reason;
        });

        return promise;
      })();
    }
  });
}

Puisque async functions s’appuie sur Promises pour fonctionner, le comportement souhaité a été obtenu avec seulement quelques lignes de code. En tant que fonctionnalité supplémentaire, vous pouvez utiliser les promesses natives dans AngularJS!

Édition ultérieure: Il n'est pas nécessaire d'utiliser un proxy car ce comportement peut être répliqué avec JS simple. C'est ici:

Promise = ((Promise) => {
  const NewPromise = function(fn) {
    const promise = new Promise(fn);

    promise.then((value) => {
      triggerDigestIfNeeded();

      return value;
    }, (reason) => {
      triggerDigestIfNeeded();

      return reason;
    });

    return promise;
  };

  // Clone the prototype
  NewPromise.prototype = Promise.prototype;

  // Clone all writable instance properties
  for (const propertyName of Object.getOwnPropertyNames(Promise)) {
    const propertyDescription = Object.getOwnPropertyDescriptor(Promise, propertyName);

    if (propertyDescription.writable) {
      NewPromise[propertyName] = Promise[propertyName];
    }
  }

  return NewPromise;
})(Promise) as any;

1
Cosmin Ababei

Comme @basarat a déclaré que la ES6 Promise native ne connaissait pas le cycle de digestion. Vous devriez promettre 

async testAsync() {
 await this.$timeout(2000).toPromise()
      .then(response => this.text = "Changed");
 }
0
Mehmet Otkun

Comme cela a déjà été décrit, angular ne sait pas quand la promesse native est terminée. Toutes les fonctions async créent une nouvelle Promise.

La solution possible peut être la suivante:

window.Promise = $q;

De cette façon, TypeScript/Babel utilisera des promesses angulaires à la place. Est-ce sûr? Honnêtement, je ne suis pas sûr - toujours tester cette solution.

0
Szymon Wygnański

Existe-t-il une solution de contournement qui évite de devoir appeler manuellement $ scope. $ Apply () à chaque fois?

En effet, TypeScript utilise l'implémentation du navigateur native Promise et ce n'est pas ce que Angular 1.x connaît. Pour faire sa sale vérification, toutes les fonctions asynchrones qu'il ne contrôle pas doivent déclencher un cycle de résumé. 

0
basarat

J'ai examiné le code de angular-async-wait et il semble qu'ils utilisent $rootScope.$apply() pour digérer l'expression après la résolution de la promesse asynchrone.

Ce n'est pas une bonne méthode. Vous pouvez utiliser les originaux $q et d’AngularJS avec un petit truc pour obtenir les meilleures performances.

Tout d’abord, créez une fonction (par exemple, une usine, une méthode)

// inject $q ...
const resolver=(asyncFunc)=>{
    const deferred = $q.defer();
    asyncFunc()
      .then(deferred.resolve)
      .catch(deferred.reject);
    return deferred.promise;
}

Maintenant, vous pouvez l’utiliser dans vos services par exemple.

getUserInfo=()=>{

  return resolver(async()=>{

    const userInfo=await fetch(...);
    const userAddress= await fetch (...);

    return {userInfo,userAddress};
  });
};

Ceci est aussi efficace que d’utiliser AngularJS $q et avec un code minimal.

0
imana97