Mecanografía asincrónica / espera no actualiza la vista de AngularJS

Estoy usando Typescript 2.1 (versión de desarrollador) para transstackr async / await a ES5.

Me he dado cuenta de que después de cambiar cualquier propiedad que está destinada a ver en mi función asíncrona, la vista no se actualiza con el valor actual, por lo que cada vez que tengo que llamar $ scope. $ Apply () al final de la función.

Ejemplo de código asíncrono:

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

Y el nuevo valor de text no se muestra a la vista después de esto.

¿Hay alguna solución, así que no tengo que llamar manualmente $ scope. $ Apply () cada vez?

Las respuestas aquí son correctas, ya que AngularJS no conoce el método, por lo que debe ‘decirle’ a Angular sobre cualquier valor que se haya actualizado.

Personalmente, usaría $q para el comportamiento asincrónico en lugar de usar await como su “forma angular”.

Puede ajustar métodos no angulares con $ q con bastante facilidad, es decir [Tenga en cuenta que así es como envuelvo todas las funciones de Google Maps ya que todos siguen este patrón de pasar en una callback para recibir notificación de finalización]

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

Puedes usarlo así

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

Sin embargo, si desea continuar con la await hay una manera mejor que usar $apply , en este caso, y que use $digest . Al igual que

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

$scope.$digest es mejor en este caso porque $scope.$apply realizará una comprobación sucia (método de Angulars para la detección de cambios) para todos los valores encuadernados en todos los ámbitos, esto puede ser costoso en cuanto a rendimiento, especialmente si tiene muchas consolidaciones. $scope.$digest , sin embargo, solo realizará la comprobación de valores encuadernados dentro del $scope actual, por lo que es mucho más eficiente.

Esto se puede hacer convenientemente con angular-async-await extensión angular-async-await :

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

Como se puede ver, todo lo que hace es envolver la función de promesa de retorno con un contenedor que llama $rootScope.$apply() después .

No existe una forma confiable de desencadenar automáticamente la función de async , ya que esto podría provocar el pirateo tanto del marco como de la implementación de Promise . No hay forma de hacer esto para la función async nativa ( es2017 TypeScript es2017 ), ya que depende de la implementación de promesa interna y no de Promise global. Más importante aún, de esta manera sería inaceptable porque este no es un comportamiento que se espera por defecto. Un desarrollador debe tener control total sobre él y asignar este comportamiento explícitamente.

Dado que se llama a testAsync varias veces, y el único lugar donde se llama es testsAsync , el resumen automático en el extremo testAsync daría como resultado el resumen de spam. Si bien una forma adecuada sería activar un resumen una vez, después de las testsAsync .

En este caso, $async se aplicaría solo a testsAsync y no a testAsync :

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

Como @basarat dijo que el ES6 Promise nativo no sabe sobre el ciclo de digestión.

Lo que podría hacer es dejar que Typescript use la promesa del servicio $q lugar de la promesa original de ES6.

De esta forma, no necesitará invocar $scope.$apply()

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

He creado un violín que muestra el comportamiento deseado. Se puede ver aquí: Promesas con AngularJS . Tenga en cuenta que está utilizando un montón de promesas que se resuelven después de 1000 ms, una función asíncrona y una Promise.race y aún así solo requiere 4 ciclos de resumen (abra la consola).

Reitero cuál fue el comportamiento deseado:

  • para permitir el uso de funciones asíncronas al igual que en JavaScript nativo; esto significa que no hay otras bibliotecas de terceros, como $async
  • para activar automáticamente el número mínimo de ciclos de resumen

¿Cómo se logró esto?

En ES6, hemos recibido una excelente función llamada Proxy . Este objeto se usa para definir un comportamiento personalizado para operaciones fundamentales (por ejemplo, búsqueda de propiedades, asignación, enumeración, invocación de funciones, etc.).

Esto significa que podemos ajustar la promesa en un proxy que, cuando la promesa se resuelve o rechaza, desencadena un ciclo de resumen, solo si es necesario. Como necesitamos una forma de activar el ciclo de resumen, este cambio se agrega en el tiempo de ejecución de 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; })(); } }); } 

Dado que las async functions dependen de que Promises funcione, el comportamiento deseado se logró con solo unas pocas líneas de código. Como característica adicional, ¡uno puede usar Promises nativos en AngularJS!

Edición posterior: no es necesario usar Proxy, ya que este comportamiento se puede replicar con JS simple. Aquí está:

 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; 

¿Hay alguna solución, así que no tengo que llamar manualmente $ scope. $ Apply () cada vez?

Esto se debe a que TypeScript utiliza la implementación Promise nativa del navegador y eso no es lo que Angular 1.x conoce. Para hacer su comprobación sucia todas las funciones asíncronas que no controla deben desencadenar un ciclo de resumen.

Como @basarat dijo que el ES6 Promise nativo no sabe sobre el ciclo de digestión. Deberías prometer

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

Como ya se ha descrito, angular no sabe cuándo finaliza la Promesa nativa. Todas las funciones async crean una nueva Promise .

La posible solución puede ser esta:

window.Promise = $q;

De esta forma, TypeScript / Babel usará promesas angulosas en su lugar. ¿Es seguro? Honestamente, no estoy seguro, todavía estoy probando esta solución.

Escribiría una función de conversión, en una fábrica genérica (no probó este código, pero debería funcionar)

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

Esto es solo para comenzar, aunque supongo que la conversión al final no será tan simple como esto …