Typescript async/await doesnt update AngularJS view

前端 未结 10 468
小鲜肉
小鲜肉 2020-12-01 09:17

I\'m using Typescript 2.1(developer version) to transpile async/await to ES5.

I\'ve noticed that after I change any property which is bound to view in my async funct

相关标签:
10条回答
  • 2020-12-01 09:43

    The answers here are correct in that AngularJS does not know about the method so you need to 'tell' Angular about any values that have been updated.

    Personally I'd use $q for asynchronous behaviour instead of using await as its "The Angular way".

    You can wrap non Angular methods with $q quite easily i.e. [Note this is how I wrap all Google Maps functions as they all follow this pattern of passing in a callback to be notified of completion]

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

    You can then use it like so

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

    However if you do wish to continue with await there is a better way than using $apply, in this case, and that it to use $digest. Like so

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

    $scope.$digest is better in this case because $scope.$apply will perform dirty checking (Angulars method for change detection) for all bound values on all scopes, this can be expensive performance wise - especially if you have many bindings. $scope.$digest will, however, only perform checking on bound values within the current $scope making it much more performant.

    0 讨论(0)
  • 2020-12-01 09:43

    I've set up a fiddle showcasing the desired behavior. It can be seen here: Promises with AngularJS. Please note that it's using a bunch of Promises which resolve after 1000ms, an async function, and a Promise.race and it still only requires 4 digest cycles (open the console).

    I'll reiterate what the desired behavior was:

    • to allow the usage of async functions just like in native JavaScript; this means no other 3rd party libraries, like $async
    • to automatically trigger the minimum number of digest cycles

    How was this achieved?

    In ES6 we've received an awesome featured called Proxy. This object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

    This means that we can wrap the Promise into a Proxy which, when the promise gets resolved or rejected, triggers a digest cycle, only if needed. Since we need a way to trigger the digest cycle, this change is added at AngularJS run time.

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

    Since async functions rely on Promises to work, the desired behavior was achieved with just a few lines of code. As an additional feature, one can use native Promises into AngularJS!

    Later edit: It's not necessary to use Proxy as this behavior can be replicated with plain JS. Here it is:

    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;

    0 讨论(0)
  • 2020-12-01 09:50

    As @basarat said the native ES6 Promise doesn't know about the digest cycle. You should to promise

    async testAsync() {
     await this.$timeout(2000).toPromise()
          .then(response => this.text = "Changed");
     }
    
    0 讨论(0)
  • 2020-12-01 09:53

    This can be conveniently done with angular-async-await extension:

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

    As it can be seen, all it does is wrapping promise-returning function with a wrapper that calls $rootScope.$apply() afterwards.

    There is no reliable way to trigger digest automatically on async function, doing this would result in hacking both the framework and Promise implementation. There is no way to do this for native async function (TypeScript es2017 target), because it relies on internal promise implementation and not Promise global. More importantly, this way would be unacceptable because this is not a behaviour that is expected by default. A developer should have full control over it and assign this behaviour explicitly.

    Given that testAsync is being called multiple times, and the only place where it is called is testsAsync, automatic digest in testAsync end would result in digest spam. While a proper way would be to trigger a digest once, after testsAsync.

    In this case $async would be applied only to testsAsync and not to testAsync itself:

    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), ...]);
        ...
      }
    }
    
    0 讨论(0)
  • 2020-12-01 09:53

    I would write a converter function, in some generic factory (didnt tested this code, but should be work)

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

    This is just to get you started, though I assume conversion in the end will not be as simple as this...

    0 讨论(0)
  • 2020-12-01 09:54

    I have examined the code of angular-async-await and It seems like they are using $rootScope.$apply() to digest the expression after the async promise is resolved.

    This is not a good method. You can use AngularJS original $q and with a little trick, you can achieve the best performance.

    First, create a function ( e.g., factory, method)

    // inject $q ...
    const resolver=(asyncFunc)=>{
        const deferred = $q.defer();
        asyncFunc()
          .then(deferred.resolve)
          .catch(deferred.reject);
        return deferred.promise;
    }
    

    Now, you can use it in your for instance services.

    getUserInfo=()=>{
    
      return resolver(async()=>{
    
        const userInfo=await fetch(...);
        const userAddress= await fetch (...);
    
        return {userInfo,userAddress};
      });
    };
    

    This is as efficient as using AngularJS $q and with minimal code.

    0 讨论(0)
提交回复
热议问题