Il typescript asincrono / atteso non aggiorna la vista AngularJS

Sto usando Typescript 2.1 (versione per sviluppatori) per traspondere async / attendere a ES5.

Ho notato che dopo aver modificato qualsiasi proprietà che è destinata alla visualizzazione nella mia funzione asincrona, la vista non viene aggiornata con il valore corrente, quindi ogni volta che devo chiamare $ scope. $ Apply () alla fine della funzione.

Esempio di codice asincrono:

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

E il nuovo valore del text non viene mostrato in vista dopo questo.

C’è qualche soluzione, quindi non devo chiamare manualmente $ scope. $ Apply () ogni volta?

Le risposte qui sono corrette in quanto AngularJS non conosce il metodo, quindi è necessario ‘dire’ Angular su qualsiasi valore che è stato aggiornato.

Personalmente userò $q per comportamento asincrono invece di usare await come “The Angular way”.

Puoi avvolgere abbastanza facilmente i metodi non angolari con $ q [Nota: questo è il modo in cui avvolgo tutte le funzioni di Google Maps poiché tutti seguono questo schema di trasmissione di un callback per essere avvisati del completamento]

 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; } 

Puoi quindi usarlo in questo modo

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

Tuttavia, se si desidera continuare con l’ await esiste un modo migliore rispetto all’utilizzo di $apply , in questo caso, e l’utilizzo di $digest . Così

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

$scope.$digest è meglio in questo caso perché $scope.$apply eseguirà un controllo sporco (metodo di Angulars per il rilevamento delle modifiche) per tutti i valori associati su tutti gli ambiti, questo può essere costoso, specialmente se si hanno molti binding. $scope.$digest , tuttavia, eseguirà solo il controllo dei valori associati all'interno dell'attuale $scope rendendolo molto più performante.

Questo può essere fatto comodamente con un’estensione angular-async-await :

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

Come si può vedere, tutto ciò che fa è racchiudere la funzione promise di ritorno con un wrapper che chiama $rootScope.$apply() seguito .

Non esiste un modo affidabile per triggersre automaticamente il digest su una funzione async , in quanto ciò comporterebbe l’hacking sia Promise dell’implementazione di Promise . Non c’è modo di farlo per la funzione async nativa (objective es2017 ), perché si basa sull’implementazione di una promise interna e non su Promise globale. Ancora più importante, in questo modo sarebbe inaccettabile perché questo non è un comportamento previsto per impostazione predefinita. Uno sviluppatore dovrebbe avere il pieno controllo su di esso e assegnare questo comportamento in modo esplicito.

Dato che testAsync viene chiamato più volte e l’unico posto in cui viene chiamato è testsAsync , il digest automatico in end testAsync risulterebbe in digest spam. Mentre un modo corretto sarebbe quello di innescare un digest una volta, dopo testsAsync .

In questo caso $async verrebbe applicato solo a testsAsync e non a testAsync stesso:

 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), ...]); ... } } 

Come @basarat ha detto che la Promise ES6 nativa non conosce il ciclo di digestione.

Quello che potresti fare è lasciare che Typescript usi la promise del servizio $q invece della promise nativa ES6.

In questo modo non avrai bisogno di invocare $scope.$apply()

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

Ho creato un violino che mostra il comportamento desiderato. Può essere visto qui: Promises with AngularJS . Tieni presente che sta utilizzando un gruppo di Promises che risolvono dopo 1000 ms, una funzione asincrona e un Promise.race e richiede solo 4 cicli digest (apri la console).

Ripeterò quale fosse il comportamento desiderato:

  • per consentire l’uso delle funzioni asincrone proprio come nel JavaScript nativo; questo significa che nessun’altra libreria di terze parti, come $async
  • per triggersre automaticamente il numero minimo di cicli digest

Come è stato realizzato?

In ES6 abbiamo ricevuto un fantastico dispositivo chiamato Proxy . Questo object viene utilizzato per definire il comportamento personalizzato per le operazioni fondamentali (ad esempio ricerca di proprietà, assegnazione, enumerazione, invocazione di funzione, ecc.).

Ciò significa che possiamo trasformare la Promessa in un Proxy che, quando la promise viene risolta o respinta, innesca un ciclo di digestione, solo se necessario. Poiché abbiamo bisogno di un modo per triggersre il ciclo di digest, questa modifica viene aggiunta al tempo di esecuzione di 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; })(); } }); } 

Poiché le async functions dipendono dal fatto che Promises funzioni, il comportamento desiderato è stato ottenuto con poche righe di codice. Come caratteristica aggiuntiva, è ansible utilizzare Promises native in AngularJS!

Modifica successiva: non è necessario utilizzare Proxy poiché questo comportamento può essere replicato con JS normale. Ecco qui:

 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; 

C’è qualche soluzione, quindi non devo chiamare manualmente $ scope. $ Apply () ogni volta?

Ciò è dovuto al fatto che TypeScript utilizza l’implementazione nativa Promise del browser e che non è ciò che conosce Angular 1.x. Per fare il suo controllo sporco tutte le funzioni asincrone che non controlla devono triggersre un ciclo di digestione.

Come @basarat ha detto che la Promessa ES6 nativa non conosce il ciclo di digestione. Dovresti prometterti

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

Come è già stato descritto, l’angular non sa quando è finita la Promessa nativa. Tutte le funzioni async creano una nuova Promise .

La ansible soluzione può essere questa:

window.Promise = $q;

In questo modo TypeScript / Babel userà invece promesse angolari. È sicuro? Onestamente non ne sono sicuro – sto ancora testando questa soluzione.

Scriverei una funzione di conversione, in qualche fabbrica generica (non ho testato questo codice, ma dovrebbe funzionare)

 function toNgPromise(promise) { var defer = $q.defer(); promise.then((data) => { $q.resolve(data); }).catch(response)=> { $q.reject(response); }); return defer.promise; } 

Questo è solo per iniziare, anche se presumo che la conversione alla fine non sarà così semplice …