ChatGPT解决这个技术问题 Extra ChatGPT

TypeScript 中的异步构造函数?

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

https://i.stack.imgur.com/xUSOH.png

这意味着我不能使用:

https://i.stack.imgur.com/IIlGJ.png

我还应该怎么做?

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

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

  await topic.setup();

P
Pang

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

你可以:

使您的公共设置异步。不要从构造函数中调用它。每当您想“完成”对象构造时调用它。异步函数运行(){让主题;调试(“新主题模型”);尝试 { 主题 = 新的 TopicsModel();等待主题.setup(); } 捕捉(错误){ 调试(“错误”,错误); } }


也可以在你的类中使用工厂(方法?)来创建一个,即异步。 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 创建的是一个空闭包。对象本身是其属性的总和,这些属性由构造函数创建。
P
Peter Wone

就绪设计模式

不要把对象放在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 构建此解决方案,以更直接地解决所提出的问题。

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

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

与工厂模式相比缺乏执行力

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

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

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

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

概括

模式是

在它描述的对象中放一个承诺

将其公开为名为 Ready 的属性

始终通过 Ready 属性引用承诺(不要在客户端代码变量中捕获它)

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

承诺将被创建和管理

承诺与它描述的对象具有相同的范围

就绪依赖的语义在客户端代码中非常明显和清晰

如果 promise 被替换(例如,连接未就绪,然后由于网络条件再次就绪)通过 thing.Ready 引用它的客户端代码将始终使用当前的 promise

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

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

结束语

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

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

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


完全按要求回答问题通常不是一个好主意。如果提出的问题是“我太高了,我不适合通过门,我如何在膝盖处切断自己?”用电锯的使用细节来回答并不理想。相反,您建议使用其他方法来过渡门。
我只是喜欢人们在没有任何解释的情况下投票否决建设性答案。即使是坏主意的建议也应该解释为什么它们是坏主意。
这是一个坏主意,因为没有工厂函数,就没有什么可以强制检查就绪性的不变量。它留给客户,您几乎可以保证会不时搞砸。
嗯,你确实问了。但可以肯定的是,我们也可以全部用 C 编程,对,因为我们是超级英雄,他们总是记住每一件小事,根本不需要计算机来帮助我们。这只是需要纪律,不是吗。
我更喜欢将属性设为私有,并将所有公开暴露(并且需要“准备就绪”)的东西都作为一个 Promise 暴露出来,并在内部等待。这确保准备就绪是其他组件不必关心的实现细节。当然,这并不像其他所有模式一样每次都符合要求,但如果可能的话,这是我的首选,因为它降低了设计的复杂性。
D
Dave Cousineau

请改用异步工厂方法。

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 我想这将取决于您的设计,但如果可能的话,我会完全避免它。
最后一种方法是我认为最好的方法。它不允许调用公共构造函数,并且只为开发人员提供了一种正确的方法。
P
Paul Flame

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

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
  }

它之所以有效,是因为支持 async/await 的 Promises 可以使用相同的值多次解析。


唯一的问题是类中的所有内容都必须是异步的。但是很好的解决方案
我正在使用打字稿,并且在调用 await this.initialization(); 时得到 Uncaught TypeError: this.initialization is not a functionawait this.initialization; 对我有用,不太清楚为什么
@JuanJoséRamírez 打字稿是对的。 this.initialization 是一个承诺,而不是一个函数。
d
darksoulsong

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

// 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 :)
H
Hady

使用私有构造函数和静态工厂方法 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();


A
Abakhan

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

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

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

主要是风格问题……


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

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

这对我有用,使用角度 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 的语法糖。
M
Michał Wojas

为承诺状态创建持有者:

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

J
Jason Rice

或者您可以坚持使用真正的 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 行为,而不会因空异常而损害组件,因为在回调之前使用默认值(空对象和空字符串)设置了状态。用户可能会在一秒钟内看到一个空列表,但很快就会填充。更好的是在数据加载时应用微调器。我经常听到有人建议过分复杂的工作,就像这篇文章中的情况一样,但应该重新检查原始流程。