类型脚本中的异步构造函数 TypeScript? ?

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

no async const

也就是说我不能用:

await

不然我该怎么做?

目前我有一些像这样的外部,但这不能保证运行的顺序,我想?

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


await topic.setup();
150778 次浏览

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

你可以:

  1. 使您的公共设置 async

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

  3. 无论何时您想要‘ finalize’对象构造,都可以调用它。

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

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

// 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

你完全可以选择方程的 把等待留在外面。如果需要,可以从构造函数调用它。需要注意的是,您需要处理 setup/initialize 函数中的任何返回值,而不是构造函数中的返回值。

这对我有用,角度是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)

准备就绪设计模式

不要把目标放在承诺里,把承诺放在目标里。

准备状态是对象的属性,所以要让它成为对象的属性。

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

可以捕获任务/承诺并等待它,但是如何管理使其可用于依赖于它的每个上下文呢?

如果框架正确,这个问题就更容易解决。目标不是等待构造,而是等待构造对象的 准备就绪。这完全是两码事。甚至有可能数据库连接对象处于就绪状态,回到非就绪状态,然后再次就绪。

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

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

在承诺解决之前或之后执行 .then(...)并不重要。承诺规范指出,在已经解决的承诺上调用 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之后,并将所有的独立代码放在 await之前。这可能会模糊代码的意图。

我鼓励人们从回电的角度思考问题。像这样的心理框架问题更适合像 C 语言这样的语言,承诺可以说是 IO 完成模式的后裔。

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

一位顾客认为这种模式“是个坏主意,因为没有工厂函数,就没有什么可以强制执行检查准备就绪的不变式。这是留给客户的,你几乎可以保证,客户会时不时地把事情搞砸。”

如果您采取这种立场,那么您将如何阻止人们构建不执行检查的工厂方法?你的底线是什么?例如,你会禁止除法运算符因为没有什么能阻止人们传递零因子吗?残酷的事实是,您必须了解特定于领域的代码和框架代码之间的区别,并应用不同的标准,同时还要熟悉一些常识。


先例

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

然而,在2020年,我发现史蒂芬 · 克利里在2013年发布了一个类似于 非常的解决方案。回顾我自己的工作,这种方法的最初痕迹出现在我差不多同一时间编写的代码中。我怀疑是 Cleary 先把它们放在一起的,但是他没有将它们正式化为一种设计模式,也没有在其他有问题的人很容易发现的地方发布它们。此外,Cleary 只处理构造,而构造只是 Readity 模式的一个应用程序(见下文)。

摘要

模式是

  • 作出承诺
  • 将其公开为名为 Ready的属性
  • 始终通过 Ready 属性引用承诺(不要在客户端代码变量中捕获它)

This establishes clear simple semantics and guarantees that

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

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

有些对象具有暂时将其置于无效状态的方法,并且模式可以在该场景中服务,而不需要修改。形式为 obj.Ready.then(...)的代码将始终使用由 Ready属性返回的承诺属性,因此每当某个操作将要使对象状态无效时,就可以创建一个新的承诺。

结案陈词

Readity 模式并不特定于构造。构建 应用很容易,但它实际上是为了确保满足状态依赖性。在异步代码的今天,您需要一个系统,承诺的简单声明性语义使得表达应该尽快采取行动(重点是 有可能)的想法变得非常简单。一旦你开始用这些术语框架事物,关于长时间运行的方法或构造函数的争论就变得毫无意义。

延迟初始化仍然有它的位置; 正如我提到的,您可以将 Readity 与延迟加载结合起来。但是,如果您不会使用该对象,那么为什么要提前创建它呢?按需创作可能更好。或者也可能不会; 有时你不能容忍在认识到需要和满足之间的延迟。

剥猫皮的方法不止一种。当我编写嵌入式软件时,我会预先创建所有东西,包括资源池。这使得不可能发生泄漏,并且在编译时知道内存需求。但这只是一个小的封闭问题空间的解决方案。

而是使用异步工厂方法。

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

或者,您可以只使用真正的 ASYNC 模型,而不会使设置过于复杂。10次中有9次归结为异步设计和同步设计。例如,我有一个 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 异常损害组件,因为在回调之前,状态是以默认值(空对象和空字符串)设置的。用户可能会看到一个空列表,但很快就会填充它。更好的方法是在数据加载时应用一个 spinner。我经常听到个人建议过于复杂的工作方法,就像这篇文章中的情况一样,但是最初的流程应该重新检查。

使用返回实例的安装异步方法

在下面的例子中,我遇到了类似的问题: 如何用一个 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;
}
}

有趣的部分是 setup 异步方法返回实例本身的地方。 然后,如果我有一个 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));

这主要是一个风格的问题..。

我找到了一个解决方案

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
}


它之所以能够工作,是因为可以使用相同的值多次解析异步/等待承诺。

创建承诺状态持有人:

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

使用私有构造函数和静态工厂方法 FTW。它是从客户端封装起来的 最好的执行方式任何验证逻辑或数据充实。

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


用工厂,这是处理这种情况的最佳方法。

问题在于,为工厂模式定义类型是一个棘手的问题,特别是在继承方面。

让我们看看如何正确地实现它的类型。

没有遗产

如果不需要类继承,模式是这样的:

class Person {
constructor(public name: string) {}


static async Create(name: string): Promise<Person> {
const instance = new Person(name);


/** Your async code here! **/


return instance;
}
}


const person = await Person.Create('John');

类继承

如果需要扩展类,则会遇到问题。Create方法始终返回基类。

在 Typecript 中,可以使用泛型类修复此问题。

type PersonConstructor<T = {}> = new (...args: any[]) => T;


class Person {
constructor(public name: string) {}


static async Create<T extends Person>(
this: PersonConstructor<T>,
name: string,
...args: any[]
): Promise<T> {
const instance = new this(name, ...args);


/** Your async code here! **/


return instance;
}
}
class MyPerson extends Person {
constructor(name: string, public lastName: string) {
super(name);
}
}


const myPerson = await MyPerson.Create('John', 'Snow');

扩建工厂

您也可以扩展 Create方法。

class MyPerson extends Person {
constructor(name: string, public lastName: string) {
super(name);
}


static async Create<T extends Person>(
this: PersonConstructor<T>,
name: string,
lastName: string,
...args: any[]
): Promise<T> {
const instance = await super.Create(name, lastName, ...args);


/** Your async code here! **/


return instance as T;
}
}


const myPerson = await MyPerson.Create('John', 'Snow');

不那么冗长的选择

我们可以通过将异步代码利用为非静态方法来减少代码的冗长性,在扩展 Create方法时,非静态方法不需要 Generic Class 定义。

type PersonConstructor<T = {}> = new (...args: any[]) => T;


class Person {
constructor(public name: string) {}


protected async init(): Promise<void> {
/** Your async code here! **/
// this.name = await ...
}


static async Create<T extends Person>(
this: PersonConstructor<T>,
name: string,
...args: any[]
): Promise<T> {
const instance = new this(name, ...args);


await instance.init();


return instance;
}
}
class MyPerson extends Person {
constructor(name: string, public lastName: string) {
super(name);
}


override async init(): Promise<void> {
await super.init();


/** Your async code here! **/
// this.lastName = await ...
}
}


const myPerson = await MyPerson.Create('John', 'Snow');

静态方法不是坏习惯吗?

是的,除了一个例外: 工厂。

为什么不在构造函数中返回一个承诺?

您可以这样做,但是许多人会认为您的代码是一个糟糕的模式,因为构造函数:

  • 应该总是返回类类型(Promise<Person>不是 Person) ;
  • 永远不要运行异步代码;