【问题标题】:async constructor functions in TypeScript?TypeScript 中的异步构造函数?
【发布时间】:2016-06-15 01:58:00
【问题描述】:

我在构造函数期间有一些我想要的设置,但似乎不允许这样做

这意味着我不能使用:

我应该怎么做呢?

目前我在外面有这样的东西,但不能保证按我想要的顺序运行?

async function run() {
  let topic;
  debug("new TopicsModel");
  try {
    topic = new TopicsModel();
  } catch (err) {
    debug("err", err);
  }

  await topic.setup();

【问题讨论】:

    标签: constructor typescript async-await


    【解决方案1】:

    构造函数必须返回它“构造”的类的实例。因此,无法返回 Promise<...> 并等待它。

    你可以:

    1. 公开设置async

    2. 不要从构造函数中调用它。

    3. 只要您想“完成”对象构造,就调用它。

      async function run() 
      {
          let topic;
          debug("new TopicsModel");
          try 
          {
              topic = new TopicsModel();
              await topic.setup();
          } 
          catch (err) 
          {
              debug("err", err);
          }
      }
      

    【讨论】:

    • 也可以在你的类中使用工厂(方法?)来创建一个,即异步。 topic = await TopicsModel.create();
    • 更准确的说法是“不可能直接返回 Promise<...>”。有关如何成功利用这种微妙但重要的区别的详细信息,请参阅我的回答。
    • 构造函数不返回任何内容。调用new MyClass 创建一个对象,将其存储在this 中并调用MyClass.constructor 对其进行初始化。返回它的是new,而不是构造函数。所以说构造函数返回一个 Promise 是有意义的。
    • 类构造函数不能在没有“new”的情况下被调用。因此,即使纯粹从 javascript/typescript 的理论角度来看,构造函数返回承诺也是没有意义的。
    • developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… 不同意你的观点。它说 constructor 属性返回对创建实例对象的 Object 构造函数的引用。 new 创建的是一个空闭包。对象本身是其属性的总和,这些属性由构造函数创建。
    【解决方案2】:

    Readiness design pattern

    不要将对象放入promise中,将promise放入对象中。

    就绪是对象的一个​​属性。所以让它成为对象的属性。

    接受的答案中描述的等待初始化方法有一个严重的限制。像这样使用await 意味着只有一个代码块可以隐式地取决于准备好的对象。这对于保证线性执行的代码来说很好,但在多线程或事件驱动的代码中它是站不住脚的。

    正确构图时,问题更容易处理。目标不是等待构造,而是等待构造对象的准备就绪。这是两个完全不同的东西。甚至像数据库连接对象这样的东西可能处于就绪状态,然后返回到非就绪状态,然后再次就绪。

    如果它依赖于构造函数返回时可能未完成的活动,我们如何确定准备就绪?很明显,准备就绪是对象的属性。许多框架直接表达了就绪的概念。在 JavaScript 中我们有 Promise,在 C# 中我们有 Task。两者都具有对对象属性的直接语言支持。

    将构造完成承诺公开为构造对象的属性。当你的构造的异步部分完成时,它应该解决这个承诺。

    .then(...) 是在 promise 解决之前还是之后执行都没有关系。 Promise 规范指出,在已解决的 Promise 上调用 then 只会立即执行处理程序。

    class Foo {
      public Ready: Promise.IThenable<any>;
      constructor() {
        ...
        this.Ready = new Promise((resolve, reject) => {
          $.ajax(...).then(result => {
            // use result
            resolve(undefined);
          }).fail(reject);
        });
      }
    }
    
    var foo = new Foo();
    foo.Ready.then(() => {
      // do stuff that needs foo to be ready, eg apply bindings
    });
    // keep going with other stuff that doesn't need to wait for foo
    
    // using await
    // code that doesn't need foo to be ready
    await foo.Ready;
    // code that needs foo to be ready
    

    为什么是resolve(undefined); 而不是resolve();?因为 ES6。根据需要进行调整以适合您的目标。

    来自花生画廊

    使用await

    在评论中,有人建议我应该使用await 构建此解决方案,以更直接地解决所提出的问题。

    您可以将awaitReady 属性一起使用,如上例所示。我不是await 的忠实粉丝,因为它要求您按依赖项对代码进​​行分区。您必须将所有依赖代码放在await 之后,并将所有独立代码放在它之前。这可能会掩盖代码的意图。

    我鼓励人们从回调的角度来思考。像这样在精神上构建问题与 C 等语言更兼容。Promise 可以说是源自IO completion 使用的模式。

    与工厂模式相比缺乏强制执行

    一位赌徒认为这种模式“是个坏主意,因为没有工厂函数,就没有什么可以强制检查就绪性的不变量。它留给了客户,你几乎可以保证它会时不时地搞砸。”

    他将如何阻止人们构建不执行检查的工厂方法?你在哪里画线?答案是您了解领域特定代码和框架代码之间的区别,并应用不同的标准,并具有一些常识:您会禁止除法运算符,因为没有什么可以阻止人们通过零除数?


    这是我的原创作品。我设计这种设计模式是因为我对外部工厂和其他类似的变通方法不满意。尽管搜索了一段时间,但我没有找到适合我的解决方案的现有技术,所以我声称自己是这种模式的创始人,直到有争议为止。

    2020 年,我发现 Stephen Cleary 在 2013 年发布了一个非常类似问题的解决方案。回顾我自己的工作,这种方法的最初痕迹出现在我几乎同时工作的代码中。我怀疑 Cleary 首先将它们放在一起,但他没有将其正式化为设计模式,也没有将其发布到其他有问题的人容易发现的地方。此外,Cleary 仅处理仅是就绪模式的一种应用的构造(见下文)。

    总结

    模式是

    • 在它描述的对象中放一个承诺
    • 将其公开为名为@9​​87654337@ 的属性
    • 始终通过 Ready 属性引用承诺(不要在客户端代码变量中捕获它)

    这建立了清晰简单的语义并保证

    • promise 将被创建和管理
    • promise 与其描述的对象具有相同的范围
    • 就绪依赖的语义在客户端代码中非常明显和清晰
    • 如果 Promise 被替换(例如,由于网络状况,连接未就绪,然后再次准备就绪)通过 thing.Ready 引用它的客户端代码将始终使用当前的 Promise

    在您使用该模式并让对象管理自己的 Promise 之前,最后一个是一场噩梦。这也是避免将承诺捕获到变量中的一个很好的理由。

    某些对象具有暂时将它们置于无效条件的方法,并且该模式可以在该场景中使用而无需修改。 obj.Ready.then(...) 形式的代码将始终使用 Ready 属性返回的任何 Promise 属性,因此每当某些操作即将使对象状态无效时,都可以创建新的 Promise。

    结束语

    就绪模式并非特定于构造。它很容易应用到构造中,但它实际上是为了确保满足状态依赖关系。在异步代码时代,您需要一个系统,而 Promise 的简单声明性语义可以直接表达应该尽快采取行动的想法,并强调可能。一旦你开始用这些术语来构建事物,关于长时间运行的方法或构造函数的争论就变得没有意义了。

    延迟初始化仍然有它的位置;正如我所提到的,您可以将准备就绪与延迟加载结合起来。但是,如果您可能不会使用该对象,那么为什么要尽早创建它呢?按需创建可能会更好。

    还有其他解决方案。当我编写嵌入式软件时,我会预先创建所有内容,包括资源池。这使得泄漏不可能并且内存需求在编译时是已知的。但这只是解决小型封闭问题空间的方法。

    【讨论】:

    • 我会将代码中的用法部分更改为不使用then,而是使用await,以使其与所提出的问题更相关。
    • 完全按要求回答问题通常不是一个好主意。如果提出的问题是“我太高了,我不适合通过门,我如何在膝盖处切断自己?” 回答电锯使用细节的理想选择。取而代之的是,您建议使用替代方法来过渡门。
    • 我只是喜欢人们在没有任何解释的情况下投票否决建设性的答案。即使是坏主意的建议也应该解释为什么它们是坏主意。
    • 这是个坏主意,因为没有工厂函数,就没有什么可以强制检查就绪性的不变量。它留给客户,您几乎可以保证会不时搞砸。
    • 嗯,你确实问过了。但可以肯定的是,我们也可以全部用 C 编程,对,因为我们是超级英雄,他们总是记住每一件小事,根本不需要计算机来帮助我们。这只是需要纪律,不是吗。
    【解决方案3】:

    改用异步工厂方法。

    class MyClass {
       private mMember: Something;
    
       constructor() {
          this.mMember = await SomeFunctionAsync(); // error
       }
    }
    

    变成:

    class MyClass {
       private mMember: Something;
    
       // make private if possible; I can't in TS 1.8
       constructor() {
       }
    
       public static CreateAsync = async () => {
          const me = new MyClass();
          
          me.mMember = await SomeFunctionAsync();
    
          return me;
       };
    }
    

    这意味着您将不得不等待这些对象的构造,但这应该已经暗示您处于无论如何都必须等待某些东西来构造它们的情况。

    您还可以做另一件事,但我怀疑这不是一个好主意:

    // probably BAD
    class MyClass {
       private mMember: Something;
    
       constructor() {
          this.LoadAsync();
       }
    
       private LoadAsync = async () => {
          this.mMember = await SomeFunctionAsync();
       };
    }
    

    这可以工作,我以前从未遇到过实际问题,但这对我来说似乎很危险,因为当你开始使用它时,你的对象实际上并没有完全初始化。

    另一种方法,在某些方面可能比第一种方法更好,是等待部件,然后在之后构造你的对象:

    export class MyClass {
       private constructor(
          private readonly mSomething: Something,
          private readonly mSomethingElse: SomethingElse
       ) {
       }
    
       public static CreateAsync = async () => {
          const something = await SomeFunctionAsync();
          const somethingElse = await SomeOtherFunctionAsync();
    
          return new MyClass(something, somethingElse);
       };
    }
    

    【讨论】:

    • 这是否依赖于在对象真正准备好之前发生的一些nextTick 魔法?
    • @dcsan 您需要调用await 来获取对象。 const myObject = await MyClass.CreateAsync();
    • 我说的是下面的第二个选项,它看起来有点少打字,但有问题。
    • @dcsan 我想这取决于你的设计,但如果可能的话我会完全避免它。
    • 我认为最后一种方法是最好的。它不允许调用公共构造函数,并且只为开发人员提供了一种正确的方法。
    【解决方案4】:

    我找到了一个看起来像这样的解决方案

    export class SomeClass {
      private initialization;
    
      // Implement async constructor
      constructor() {
        this.initialization = this.init();
      }
    
      async init() {
        await someAsyncCall();
      }
    
      async fooMethod() {
        await this.initialization();
        // ...some other stuff
      }
    
      async barMethod() {
        await this.initialization();
        // ...some other stuff
      }
    
    

    之所以有效,是因为支持异步/等待的 Promise 可以使用相同的值多次解析。

    【讨论】:

    • 唯一的问题是类中的所有内容都必须是异步的。但是很好的解决方案
    • 我正在使用打字稿并且在调用await this.initialization(); 时得到Uncaught TypeError: this.initialization is not a functionawait this.initialization; 对我有用,不太清楚为什么
    【解决方案5】:

    我知道它已经很老了,但另一种选择是拥有一个工厂来创建对象并等待其初始化:

    // Declare the class
    class A {
    
      // Declare class constructor
      constructor() {
    
        // We didn't finish the async job yet
        this.initialized = false;
    
        // Simulates async job, it takes 5 seconds to have it done
        setTimeout(() => {
          this.initialized = true;
        }, 5000);
      }
    
      // do something usefull here - thats a normal method
      useful() {
        // but only if initialization was OK
        if (this.initialized) {
          console.log("I am doing something useful here")
    
        // otherwise throw an error which will be caught by the promise catch
        } else {
          throw new Error("I am not initialized!");
        }
      }
    
    }
    
    // factory for common, extensible class - that's the reason for the constructor parameter
    // it can be more sophisticated and accept also params for constructor and pass them there
    // also, the timeout is just an example, it will wait for about 10s (1000 x 10ms iterations
    function factory(construct) {
    
      // create a promise
      var aPromise = new Promise(
        function(resolve, reject) {
    
          // construct the object here
          var a = new construct();
    
          // setup simple timeout
          var timeout = 1000;
    
          // called in 10ms intervals to check if the object is initialized
          function waiter() {
        
            if (a.initialized) {
              // if initialized, resolve the promise
              resolve(a);
            } else {
    
              // check for timeout - do another iteration after 10ms or throw exception
              if (timeout > 0) {     
                timeout--;
                setTimeout(waiter, 10);            
              } else {            
                throw new Error("Timeout!");            
              }
    
            }
          }
      
          // call the waiter, it will return almost immediately
          waiter();
        }
      );
    
      // return promise of the object being created and initialized
      return a Promise;
    }
    
    
    // this is some async function to create object of A class and do something with it
    async function createObjectAndDoSomethingUseful() {
    
      // try/catch to capture exceptions during async execution
      try {
        // create object and wait until its initialized (promise resolved)
        var a = await factory(A);
        // then do something usefull
        a.useful();
      } catch(e) {
        // if class instantiation failed from whatever reason, timeout occured or useful was called before the object finished its initialization
        console.error(e);
      }
    
    }
    
    // now, perform the action we want
    createObjectAndDoSomethingUsefull();
    
    // spaghetti code is done here, but async probably still runs
    

    【讨论】:

    • 也可以只使用工厂方法
    • 我不再喜欢静态方法了 ;)
    • 使用静态方法的完全正当理由。
    • 我同意和不同意。因为我通常使用某种 DIC,所以我不再使用静态方法。除了 DIC :)
    【解决方案6】:

    使用返回实例的设置异步方法

    在以下情况下我遇到了类似的问题:如何使用 'FooSession' 类的实例或使用 'fooSessionParams' 对象来实例化一个 'Foo' 类,知道从 fooSessionParams 对象创建 fooSession 是一个异步功能? 我想通过这样做来实例化:

    let foo = new Foo(fooSession);
    

    let foo = await new Foo(fooSessionParams);
    

    并且不想要工厂,因为这两种用法太不同了。但正如我们所知,我们不能从构造函数返回承诺(并且返回签名不同)。我是这样解决的:

    class Foo {
        private fooSession: FooSession;
    
        constructor(fooSession?: FooSession) {
            if (fooSession) {
                this.fooSession = fooSession;
            }
        }
    
        async setup(fooSessionParams: FooSessionParams): Promise<Foo> {
            this.fooSession = await getAFooSession(fooSessionParams);
            return this;
        }
    }
    

    有趣的部分是设置异步方法返回实例本身的地方。 然后,如果我有一个 'FooSession' 实例,我可以这样使用它:

    let foo = new Foo(fooSession);
    

    如果我没有“FooSession”实例,我可以通过以下方式之一设置“foo”:

    let foo = await new Foo().setup(fooSessionParams);
    

    (女巫是我的首选方式,因为它接近我首先想要的方式) 或

    let foo = new Foo();
    await foo.setup(fooSessionParams);
    

    我也可以添加静态方法:

        static async getASession(fooSessionParams: FooSessionParams): FooSession {
            let fooSession: FooSession = await getAFooSession(fooSessionParams);
            return fooSession;
        }
    

    并以这种方式实例化:

    let foo = new Foo(await Foo.getASession(fooSessionParams));
    

    主要是风格问题……

    【讨论】:

    • 我认为这很复杂。如果您为其他开发人员开发模块,那么您将必须为他们编写全面的文档以正确实例化该类。我更喜欢使用私有构造函数和只有一个静态异步构建器方法的设计。这个静态方法也可以有可选的参数。
    【解决方案7】:

    使用私有构造函数和静态工厂方法 FTW。它是 best way to enforce 任何验证逻辑或数据丰富,封装在客户端之外。

    class Topic {
      public static async create(id: string): Promise<Topic> {
        const topic = new Topic(id);
        await topic.populate();
        return topic;
      }
    
      private constructor(private id: string) {
        // ...
      }
    
      private async populate(): Promise<void> {
        // Do something async. Access `this.id` and any other instance fields
      }
    }
    
    // To instantiate a Topic
    const topic = await Topic.create();
    
    

    【讨论】:

      【解决方案8】:

      您可以选择完全将等待排除在外。如果需要,您可以从构造函数中调用它。需要注意的是,您需要在 setup/initialise 函数中处理任何返回值,而不是在构造函数中。

      这对我有用,使用 angular 1.6.3。

      import { module } from "angular";
      import * as R from "ramda";
      import cs = require("./checkListService");
      
      export class CheckListController {
      
          static $inject = ["$log", "$location", "ICheckListService"];
          checkListId: string;
      
          constructor(
              public $log: ng.ILogService,
              public $loc: ng.ILocationService,
              public checkListService: cs.ICheckListService) {
              this.initialise();
          }
      
          /**
           * initialise the controller component.
           */
          async initialise() {
              try {
                  var list = await this.checkListService.loadCheckLists();
                  this.checkListId = R.head(list).id.toString();
                  this.$log.info(`set check list id to ${this.checkListId}`);
               } catch (error) {
                  // deal with problems here.
               }
          }
      }
      
      module("app").controller("checkListController", CheckListController)
      

      【讨论】:

      • 这会创建一个竞争条件,因为它就像调用一个返回承诺的函数一样。该函数将在 Promise 完成之前返回。在这种情况下,this.initialise() 不是构造函数中的阻塞调用。 initialise 和构造函数在 list 获得其等待值之前返回。
      • @rob3c,如果您查看生成的状态机,您会注意到constructor 确实会在initialise 返回之前返回:但是initialise 函数将不会 在加载列表之前返回。 initialise 中的 await 正是为了实现这一目标。
      • 我们说的是同一件事。您所指的状态机正是幕后生成的代码,它允许单线程 javascript 从初始化和构造函数返回,以便在初始化的第二部分在完成后在 await 行恢复之前继续执行。这并不能消除竞争条件——它只会混淆它。
      • 好吧,我觉得没关系,我不需要任何一个函数的返回状态。放弃 async 和 await 并使用原生链式 Promise 可以更好地实现该模式。除了所有其他生成的代码之外,我绝对不认为创建一个完整的工厂模式有什么大价值。
      • @Jim 不正确。如果不等待初始化本身,则不等待异步初始化中的等待。使用 Promise 不会改变任何东西 - async/await 只是普通 Promise 的语法糖。
      【解决方案9】:

      为承诺状态创建持有者:

      class MyClass {
          constructor(){
              this.#fetchResolved = this.fetch()
          }
          #fetchResolved: Promise<void>;
          fetch = async (): Promise<void> => {
              return new Promise(resolve => resolve()) // save data to class property or simply add it by resolve() to #fetchResolved reference
          }
          isConstructorDone = async (): boolean => {
              await this.#fetchResolved;
              return true; // or any other data depending on constructor finish the job
          }
      }
      

      使用方法:

      const data = new MyClass();
      const field = await data.isConstructorDone();
      

      【讨论】:

        【解决方案10】:

        或者您可以坚持使用真正的 ASYNC 模型,而不会使设置过于复杂。 10 次中有 9 次归结为异步与同步设计。例如,如果我在构造函数的 promise 回调中初始化状态变量,那么我有一个 React 组件需要同样的东西。事实证明,要解决空数据异常,我需要做的只是设置一个空状态对象,然后在异步回调中设置它。例如,这是一个带有返回承诺和回调的 Firebase 读取:

                this._firebaseService = new FirebaseService();
                this.state = {data: [], latestAuthor: '', latestComment: ''};
        
                this._firebaseService.read("/comments")
                .then((data) => {
                    const dataObj = data.val();
                    const fetchedComments = dataObj.map((e: any) => {
                        return {author: e.author, text: e.text}
                    });
        
                    this.state = {data: fetchedComments, latestAuthor: '', latestComment: ''};
        
                });
        

        通过采用这种方法,我的代码保持了它的 AJAX 行为,而不会因 null 异常而损害组件,因为在回调之前已使用默认值(空对象和空字符串)设置了状态。用户可能会在一秒钟内看到一个空列表,但很快就会填充。更好的是在数据加载时应用微调器。我经常听到有人建议像这篇文章中那样使用过于复杂的解决方法,但应该重新检查原始流程。

        【讨论】:

          猜你喜欢
          • 2017-04-14
          • 2012-08-05
          • 1970-01-01
          • 2013-12-27
          • 2014-10-28
          • 2018-06-26
          • 1970-01-01
          • 2018-09-29
          相关资源
          最近更新 更多