【问题标题】:Typescript async/await doesnt update AngularJS viewTypescript async/await 不更新 AngularJS 视图
【发布时间】:2017-02-18 00:41:27
【问题描述】:

我正在使用 Typescript 2.1(开发者版本)将 async/await 转换为 ES5。

我注意到,在我更改任何绑定到我的异步函数中查看的属性后,视图不会更新为当前值,所以每次我必须在结束时调用 $scope.$apply()功能。

示例异步代码:

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

之后,新的text 值不会显示在视图中。

是否有任何解决方法,这样我就不必每次都手动调用 $scope.$apply() 了?

【问题讨论】:

  • 你不能“省略这个”。因为 async/await 使用原生 Promise 而不是 $q。 $apply 应该被调用,无论是在那里还是在调用者函数中。顺便说一句,在这里使用 $timeout 是个糟糕的主意,它会导致额外的摘要,应该使用基于非 Angular 承诺的解决方案,例如Bluebird.
  • 我还没有读过,但我希望这能很好地帮助labs.magnet.me/nerds/2015/11/16/async-await-in-angularjs.html
  • 在我看来,最好的解决方案是在 AngularJS 中使用此功能的非侵入性方式。我想我们应该更多地关注 typescript 如何转译这个 ES7 特性并找到触发摘要循环的方法。

标签: angularjs typescript angularjs-scope


【解决方案1】:

是否有任何解决方法,这样我就不必每次都手动调用 $scope.$apply() 了?

这是因为 TypeScript 使用浏览器 native Promise 实现,这不是 Angular 1.x 所知道的。要对其不控制的所有异步函数进行脏检查,必须触发摘要循环。

【讨论】:

  • 有没有办法扩展 Promise 原型来检查它是否是一个 Angular Promise,并做相应的事情?一定有可能,只是我对JS不够了解,不知道怎么做。
【解决方案2】:

正如 @basarat 所说,原生 ES6 Promise 不知道摘要周期。

你可以做的是让 Typescript 使用 $q 服务承诺,而不是原生的 ES6 承诺。

这样你就不需要调用$scope.$apply()

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

【讨论】:

  • $q 替换Promise 并不是一个好主意。这将破坏依赖原生 Promise 的代码,因为两个实现确实不同。
  • 这不起作用。请尝试设置一个小提琴来测试这个。
  • 问题比较微妙,不能这样解决。当代码被转译,甚至是其本机形式时,它会在 Angular 的 $digest 循环之外执行。
【解决方案3】:

正如@basarat 所说,原生 ES6 Promise 不知道摘要周期。你应该答应

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

【讨论】:

    【解决方案4】:

    这可以通过angular-async-await 扩展名方便地完成:

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

    可以看出,它所做的只是wrapping promise-returning function with a wrapper that calls $rootScope.$apply() afterwards

    没有可靠的方法在async 函数上自动触发摘要,这样做会导致对框架和Promise 实现的入侵。对于本机 async 函数(TypeScript es2017 目标)没有办法做到这一点,因为它依赖于内部承诺实现而不是 Promise 全局。更重要的是,这种方式是不可接受的,因为这不是默认情况下预期的行为。开发人员应该完全控制它并明确分配此行为。

    鉴于testAsync 被多次调用,并且唯一被调用的地方是testsAsynctestAsync 末尾的自动摘要会导致摘要垃圾邮件。虽然正确的方法是在testsAsync 之后触发一次摘要。

    在这种情况下,$async 将仅应用于 testsAsync 而不是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), ...]);
        ...
      }
    }
    

    【讨论】:

    • 虽然这可行,但您必须使用 $async 而不是本机语法。我们应该更多地关注转译结果,而不是触发新的摘要循环。
    • @CosminAbabei 您必须使用 $async with 本机语法,而不是代替。我不确定你是什么意思。应该以任何方式触发新的摘要循环以使其按预期工作。而且“转译”的结果不一定是转译的——本机异步函数已经存在。
    • $async 改变原生语法。理想情况下,我们应该有一个 webpack 加载器,它将 async/await 代码转换为 AngularJS 高效的代码。
    • @CosminAbabei 这个想法看起来有点过时了,因为同样已经有原生的 async/await 和模块,而且 Webpack 可能会比预期的更早成为可选的。根据我使用 Angular 和 JS 的经验,没有干净的方法可以透明地做到这一点。好的,这可以通过自定义 Babel 转换来处理,但是转换无法可靠地获取 $rootScope 引用以在其上调用 $apply。任何 hack 都是不可靠的,并导致内存泄漏或其他问题。
    • @CosminAbabei 仅供参考,我已经添加了解释,为什么在所有异步函数(即使这是可能的)上默认设置这种行为是一个坏主意。希望这会有所帮助
    【解决方案5】:

    这里的答案是正确的,因为 AngularJS 不知道该方法,因此您需要“告诉”Angular 任何已更新的值。

    我个人会将$q 用于异步行为,而不是使用await 作为其“Angular 方式”。

    你可以很容易地用 $q 包装非 Angular 方法,即[注意这是我包装所有谷歌地图函数的方式,因为它们都遵循这种传递回调以通知完成的模式]

    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 将对所有范围内的所有绑定值执行脏检查(用于更改检测的 Angulars 方法),这在性能方面可能代价高昂 - 特别是如果您有很多绑定。但是,$scope.$digest 将只检查当前 $scope 中的绑定值,使其性能更高。

    【讨论】:

    • $scope.$digest() 默认不推荐。开发人员应该非常清楚他/她在做什么,因为这会影响很多事情,而且并不总是以明显的方式。这是手册甚至没有涵盖的先进技术。一般来说,我只推荐它作为经过广泛测试的应用程序的优化步骤。
    • 我不会认为这是一种先进的技术,即使它肯定是分享知识也是一件好事?我的一般经验法则是:如果您只想影响当前范围,请使用$digest,否则使用$apply。将这项技术应用到我的应用中,性能得到了巨大的提升。
    • 分享知识是件好事。这是一种先进的技术,可以带来巨大的性能改进也带来巨大的麻烦。 AFAIK 从来没有足够的记录。一个不知道后果的开发人员可能会把事情搞砸。在不知道 testAsync 在什么条件下工作的情况下,根据经验推荐 $scope.$digest() 是非常糟糕的建议。即使你知道自己做得很好,它也可能适得其反,因为 AngularJS 领域的很多东西都依赖于全局摘要,例如2路绑定。
    【解决方案6】:

    我会在一些通用工厂中编写一个转换器函数(没有测试过这段代码,但应该可以工作)

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

    这只是为了让你开始,虽然我认为最终的转换不会像这样简单......

    【讨论】:

    • 这是deferred antipattern。只是$q.resolve(promise)。这会导致创建一个永远不会使用的 promise 对象,并且似乎并不优于 $scope.$apply(),但它会起作用。
    • 只发布经过测试的工作解决方案。不要浪费时间。
    【解决方案7】:

    我已经设置了一个小提琴来展示所需的行为。可以在这里看到:Promises with AngularJS。 请注意,它使用了一堆在 1000 毫秒后解析的 Promise、一个异步函数和一个 Promise.race,它仍然只需要 4 个摘要周期(打开控制台)。

    我将重申期望的行为是什么:

    • 允许使用async functions,就像在原生JavaScript中一样;这意味着没有其他 3rd 方库,例如 $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 依赖 Promise 工作,因此只需几行代码即可实现所需的行为。 作为一项附加功能,您可以在 AngularJS 中使用原生 Promises!

    稍后编辑:不需要使用代理,因为这种行为可以用普通的 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;

    【讨论】:

    • 没有真正需要代理(对于初学者来说很慢)。同样的事情也可以通过继承 Promise 来实现。这是一个很好的概念证明,但问题是不可避免的——可能有很多不必要的摘要,这是任何现实世界 Angular 应用程序的瓶颈。任何依赖 Promise 的第三方库都会损害性能,而通常不会(本机 Promise 快速且无阻塞)。其他问题包括竞争条件(可能存在在 Promise 被修补之前创建的承诺)和内存泄漏,尤其是在测试中。
    • 我们不需要throw reason; 来传播拒绝吗?
    【解决方案8】:

    正如已经描述的那样,Angular 不知道原生 Promise 何时完成。所有async 函数都会创建一个新的Promise

    可能的解决方案是这样的:

    window.Promise = $q;

    这样 TypeScript/Babel 将使用 Angular Promise 代替。 安全吗?老实说,我不确定 - 仍在测试此解决方案。

    【讨论】:

    • 这是一种不好的做法,它会破坏任何依赖本机 Promise 的第三方库。此外,这仅影响转译的 async 函数,而不影响本机函数 - 后者使用最初等于全局 Promise 但不受其影响的内部承诺实现。
    【解决方案9】:

    我检查了angular-async-await 的代码,似乎他们在解决异步承诺后使用$rootScope.$apply() 来消化表达式。

    这不是一个好方法。可以使用AngularJS原版$q用一点小技巧,就能达到最佳性能。

    首先,创建一个函数(例如,工厂、方法)

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

    现在,您可以在您的实例服务中使用它。

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

    这与使用 AngularJS $q 一样高效,并且代码最少。

    【讨论】:

      【解决方案10】:

      如果您使用 ngUpgrade 从 AngularJS 升级到 Angular(请参阅 https://angular.io/guide/upgrade#upgrading-with-ngupgrade):

      随着 Zone.js 修补原生 Promises,您可以开始将所有基于 $q 的 AngularJS 承诺重写为原生 Promises,因为当微任务队列为空时(例如,当 Promise 被解析时)Angular 会自动触发 $digest。

      即使您不打算升级到 Angular,您仍然可以这样做,方法是在您的项目中包含 Zone.js 并设置一个类似 ngUpgrade 的钩子。

      【讨论】:

        猜你喜欢
        • 2016-08-22
        • 2013-10-23
        • 2016-12-05
        • 1970-01-01
        • 2017-05-10
        • 1970-01-01
        • 2017-04-26
        • 2016-02-26
        • 2016-06-08
        相关资源
        最近更新 更多