Typescript async/await не обновляет просмотр AngularJS
Я использую Typescript 2.1 (версия разработчика) для пересылки async/await на ES5.
Я заметил, что после того, как я изменил какое-либо свойство, которое связано с моей функцией async, представление не обновляется текущим значением, поэтому каждый раз, когда мне приходится вызывать $scope. $apply() в конце функция.
Пример асинхронного кода:
async testAsync() {
await this.$timeout(2000);
this.text = "Changed";
//$scope.$apply(); <-- would like to omit this
}
И новое значение text
после этого не отображается.
Есть ли какое-либо обходное решение, поэтому мне не нужно вручную вызывать $scope. $apply() каждый раз?
Ответы
Ответ 1
Это удобно сделать с помощью angular-async-await
extension:
class SomeController {
constructor($async) {
this.testAsync = $async(this.testAsync);
}
async testAsync() { ... }
}
Как можно видеть, все, что он делает, это обертывание обещающей функции с оболочкой, которая вызывает $rootScope.$apply()
впоследствии.
Нет надежного способа автоматического переключения дайджеста на функцию async, так как это приведет к взлому как структуры, так и реализации Promise
. Что еще более важно, этот способ был бы неприемлемым, потому что это не поведение, которое ожидается по умолчанию. Разработчик должен иметь полный контроль над ним и явно назначать это поведение.
Учитывая, что testAsync
вызывается несколько раз, и единственным местом, где он вызывается, является testsAsync
, автоматический дайджест в конце testAsync
приведет к спаму спама. Хотя правильным способом было бы запустить дайджест один раз, после testsAsync
.
В этом случае $async
будет применяться только к testsAsync
, а не к testAsync
:
class SomeController {
constructor($async) {
this.testsAsync = $async(this.testsAsync);
}
private async testAsync() { ... }
async testsAsync() {
await Promise.all([this.testAsync(1), this.testAsync(2), ...]);
...
}
}
Ответ 2
Как @basarat сказал, что родной ES6 Promise
не знает о цикле дайджеста.
Что вы можете сделать, это позволить Typescript использовать обещание службы $q
вместо обычного ES6-обещания.
Таким образом вам не нужно вызывать $scope.$apply()
angular.module('myApp')
.run(['$window', '$q', ($window, $q) => {
$window.Promise = $q;
}]);
Ответ 3
Ответы здесь верны в том, что AngularJS не знает о методе, поэтому вам нужно "рассказать" Angular о любых обновленных значениях.
Лично я использовал бы $q
для асинхронного поведения вместо того, чтобы использовать await
как его "способ Angular".
Вы можете легко обернуть методы Angular с помощью $q. [Обратите внимание, что я обертываю все функции Google Maps, поскольку все они следуют этому шаблону передачи в обратном вызове, чтобы получать уведомление о завершении]
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;
}
Затем вы можете использовать его так
this.doAThing().then(someValue => {
this.memberValue = someValue;
});
Однако, если вы хотите продолжить с помощью await
, в этом случае лучше использовать $apply
и использовать $digest
. Таким образом
async testAsync() {
await this.$timeout(2000);
this.text = "Changed";
$scope.$digest(); <-- This is now much faster :)
}
$scope.$digest
лучше в этом случае, потому что $scope.$apply
будет выполнять грязную проверку (метод углов для обнаружения изменений) для всех связанных значений во всех областях, это может быть дорогостоящим качеством, особенно если у вас много привязок. Тем не менее, $scope.$digest
выполняет только проверку связанных значений в текущем $scope
, делая его намного более эффективным.
Ответ 4
Я создал скрипку, демонстрирующую желаемое поведение. Это можно увидеть здесь: Promises с помощью AngularJS.
Обратите внимание, что он использует кучу Promises, которые разрешают после 1000 мс, асинхронную функцию и Promise.race, и она по-прежнему требует только 4 цикла дайджеста (откройте консоль).
Я еще раз повторю, что это за желание:
- разрешить использование асинхронных функций, как и в родном JavaScript; это означает, что никакие другие сторонние библиотеки, например
$async
- для автоматического запуска минимального количества циклов дайджеста
Как это было достигнуто?
В ES6 мы получили потрясающий признак Proxy. Этот объект используется для определения пользовательского поведения для основных операций (например, поиск свойств, назначение, перечисление, вызов функций и т.д.).
Это означает, что мы можем обернуть Promise в прокси-сервер, который, когда обещание будет разрешено или отклонено, запускает цикл дайджеста, только при необходимости. Поскольку нам нужен способ запуска цикла дайджеста, это изменение добавляется в время работы 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;
})();
}
});
}
Так как async functions
полагается на Promises для работы, желаемое поведение было достигнуто всего несколькими строками кода. В качестве дополнительной функции можно использовать native Promises в AngularJS!
Редактировать позже: Не нужно использовать прокси, так как это поведение может быть реплицировано с помощью простого JS. Вот он:
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;
Ответ 5
Есть ли какое-либо обходное решение, поэтому мне не нужно вручную вызывать $scope. $apply() каждый раз?
Это связано с тем, что TypeScript использует встроенную реализацию браузера Promise
, и это не то, о чем знает Angular 1.x. Чтобы выполнить свою грязную проверку всех асинхронных функций, которые она не контролирует, должен запускаться цикл дайджеста.
Ответ 6
Как сказал @basarat, родной ES6 Promise не знает о цикле дайджеста. Вы должны обещать
async testAsync() {
await this.$timeout(2000).toPromise()
.then(response => this.text = "Changed");
}
Ответ 7
Я бы написал функцию конвертера в некотором родовом factory (не тестировал этот код, но должен работать)
function toNgPromise(promise)
{
var defer = $q.defer();
promise.then((data) => {
$q.resolve(data);
}).catch(response)=> {
$q.reject(response);
});
return defer.promise;
}
Это просто, чтобы вы начали, хотя я предполагаю, что преобразование в конце не будет таким простым, как это...
Ответ 8
Как уже было описано, angular не знает, когда нативный Promise закончен. Все функции async
создают новый Promise
.
Возможное решение может быть следующим:
window.Promise = $q;
Таким образом TypeScript/Babel будет использовать angular promises.
Это безопасно? Честно говоря, я не уверен - все еще проверяю это решение.