topromise - Typesyn async/await no actualiza la vista AngularJS
await subscribe angular 6 (9)
Estoy usando Typescript 2.1 (versión de desarrollador) para hacer una transcripción de async / await a ES5.
Me he dado cuenta de que después de cambiar cualquier propiedad que está obligada a ver en mi función asíncrona, la vista no se actualiza con el valor actual, por lo que cada vez 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 para que no tenga que llamar manualmente $ scope. $ Apply () todas las veces?
¿Hay alguna solución para que no tenga que llamar manualmente $ scope. $ Apply () todas las veces?
Esto se debe a que TypeScript utiliza la implementación de 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 dijo @basarat , la Promise
ES6 nativa no conoce el ciclo del resumen.
Lo que podría hacer es permitir que Typescript use la promesa de servicio $q
lugar de la promesa nativa de ES6.
De esa manera no necesitarás invocar $scope.$apply()
angular.module(''myApp'')
.run([''$window'', ''$q'', ($window, $q) => {
$window.Promise = $q;
}]);
Como dijo @basarat, la promesa ES6 nativa no conoce el ciclo del resumen. 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 manera, TypeScript / Babel usará promesas angulares en su lugar. ¿Es seguro? Sinceramente, no estoy seguro, todavía estoy probando esta solución.
Escribiría una función de conversión, en alguna 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 asumo que la conversión al final no será tan simple como esto ...
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 puede verse, todo lo que hace es envolver la función de promesa de devolución con un envoltorio que llama a $rootScope.$apply()
después .
No hay una forma confiable de activar el resumen automáticamente en la función async
, ya que esto podría resultar en la piratería tanto del marco como de la implementación Promise
. No hay forma de hacer esto para la función async
nativa (objetivo de TypeScript es2017
), ya que se basa en la implementación de promesa interna y no en Promise
global. Más importante aún, esta forma sería inaceptable porque este no es un comportamiento que se espera de forma predeterminada. Un desarrollador debe tener control total sobre él y asignar este comportamiento explícitamente.
Dado que testAsync
se llama varias veces, y el único lugar donde se llama es testsAsync
, el resumen automático en el final de testAsync
daría como resultado el spam de digestión. Si bien una forma adecuada sería desencadenar un resumen una vez, después de testsAsync
.
En este caso, $async
se aplicaría solo a testsAsync
y no a testAsync
sí:
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), ...]);
...
}
}
He configurado un violín mostrando 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 todavía solo requiere 4 ciclos de resumen (abra la consola).
Reitero cual fue el comportamiento deseado:
- para permitir el uso de funciones asíncronas como 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 digestión
¿Cómo se logró esto?
En ES6 hemos recibido una impresionante característica 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 incluir la Promise en un Proxy que, cuando la promesa se resuelve o rechaza, activa un ciclo de resumen, solo si es necesario. Ya que necesitamos una manera 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
basan en Promesas para funcionar, 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;
He examinado el código de angular-async-await y parece que están usando $rootScope.$apply()
para digerir la expresión después de que se resuelva la promesa de async.
Este no es un buen método. Puede usar $q
original de AngularJS y con un pequeño truco, puede lograr el mejor rendimiento.
Primero, crea una función (por ejemplo, fábrica, método)
// inject $q ...
const resolver=(asyncFunc)=>{
const deferred = $q.defer();
asyncFunc()
.then(deferred.resolve)
.catch(deferred.reject);
return deferred.promise;
}
Ahora, puedes usarlo en tus servicios de ejemplo.
getUserInfo=()=>{
return resolver(async()=>{
const userInfo=await fetch(...);
const userAddress= await fetch (...);
return {userInfo,userAddress};
});
};
Esto es tan eficiente como usar AngularJS $q
y con un código mínimo.
Las respuestas aquí son correctas, ya que AngularJS no conoce el método, por lo que debe "informar" a Angular sobre los valores que se han actualizado.
Personalmente, usaría $q
para el comportamiento asíncrono en lugar de usar await
como su "Forma Angular".
Puede envolver los 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 todas siguen este patrón de transferencia en una devolución de llamada para recibir una 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;
}
Entonces puedes usarlo así
this.doAThing().then(someValue => {
this.memberValue = someValue;
});
Sin embargo, si desea continuar con la await
hay una mejor manera que usar $apply
, en este caso, y usar $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 enlazados en todos los ámbitos, esto puede ser costoso en cuanto al rendimiento, especialmente si tiene muchos enlaces. Sin embargo, $scope.$digest
solo realizará la comprobación de los valores enlazados dentro de $scope
actual, lo que lo hace mucho más eficaz.