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. Это безопасно? Честно говоря, я не уверен - все еще проверяю это решение.