Функции async-конструктора в TypeScript?
У меня есть какая-то настройка, которую я хочу во время конструктора, но кажется, что это не разрешено
![no async const]()
Это означает, что я не могу использовать:
![ожидание]()
Как мне это сделать?
В настоящее время у меня есть что-то подобное, но это не гарантируется в том порядке, в котором я хочу?
async function run() {
let topic;
debug("new TopicsModel");
try {
topic = new TopicsModel();
} catch (err) {
debug("err", err);
}
await topic.setup();
Ответы
Ответ 1
Конструктор должен возвращать экземпляр класса, который он "создает", поэтому его невозможно вернуть Promise <... > и ждать его.
Вы можете:
- Сделайте свою общедоступную настройку async
- Не вызывайте его из конструктора.
-
Вызовите его, когда хотите завершить строительство объекта
async function run()
{
let topic;
debug("new TopicsModel");
try
{
topic = new TopicsModel();
await topic.setup();
}
catch (err)
{
debug("err", err);
}
}
Ответ 2
Если вы не можете поместить объект в обещание, поместите обещание в объект.
Проблема более поддается решению, когда правильно оформлена. Задача - не ждать строительства, а ждать готовности построенного объекта. Это две совершенно разные вещи.
Как мы можем определить готовность, если это зависит от действий, которые могут быть не завершены, когда конструктор вернется? Совершенно очевидно, что готовность является свойством объекта. Многие рамки прямо выражают понятие готовности. В JavaScript у нас есть Promise
, а в С# у нас есть 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
});
Зачем resolve(undefined);
вместо resolve();
? Потому что ES6. Отрегулируйте по мере необходимости в соответствии с вашей целью.
В комментарии было предложено, чтобы я сформулировал это решение в await
более непосредственного решения вопроса в соответствии с await
вопросом.
Это плохое решение, потому что оно позволяет только коду в области сразу после оператора await ждать завершения. Предоставление объекта обещания в качестве свойства асинхронно инициализированного объекта означает, что любой код в любом месте может гарантировать, что инициализация завершена, поскольку обещание находится в области действия везде, где объект находится в области действия, поэтому оно гарантировано доступно везде, где существует риск.
Кроме того, маловероятно, что использование ключевого слова await является полезным результатом для любого проекта, который не является университетским заданием, демонстрирующим использование ключевого слова await.
Это оригинальная работа мной. Я разработал этот шаблон проектирования, потому что был недоволен внешними фабриками и другими подобными обходными путями. Несмотря на поиски в течение некоторого времени, я не нашел предшествующего уровня техники для своего решения, поэтому я претендую на кредит как на создателя этого шаблона, пока не оспорю.
В комментарии @suhas предлагает использовать await
а не .then
и это будет работать, но менее широко совместимо. Что касается совместимости, Typescript изменился с тех пор, как я написал это, и теперь вам придется объявить public Ready: Promise<any>
Ответ 3
Я знаю его тихий старый, но еще один вариант - иметь factory, который создаст объект и ждет его инициализации:
// 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
usefull() {
// but only if initialization was OK
if (this.initialized) {
console.log("I am doing something usefull here")
// otherwise throw error which will be catched by the promise catch
} else {
throw new Error("I am not initialized!");
}
}
}
// factory for common, extensible class - thats the reason of the constructor parameter
// it can be more sophisticated and accept also params for constructor and pass them there
// also, the timeout is just example, it will wait 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 object being created and initialized
return aPromise;
}
// this is some async function to create object of A class and do something with it
async function createObjectAndDoSomethingUsefull() {
// 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.usefull();
} catch(e) {
// if class instantiation failed from whatever reason, timeout occured or usefull was called before the object finished its initialization
console.error(e);
}
}
// now, perform the action we want
createObjectAndDoSomethingUsefull();
// spagetti code is done here, but async probably still runs
Ответ 4
Вместо этого используйте асинхронный фабричный метод.
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();
};
}
Это может сработать, и у меня никогда не было реальной проблемы с этим раньше, но это кажется мне опасным, поскольку ваш объект на самом деле не будет полностью инициализирован, когда вы начнете его использовать.
Ответ 5
Вы можете выбрать выйти из всего уравнения. Вы можете вызвать его из конструктора, если вам нужно. Суть в том, что вам нужно иметь дело с любыми возвращаемыми значениями в функции 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)
Ответ 6
Используйте установочный асинхронный метод, который возвращает экземпляр
У меня была похожая проблема в следующем случае: как создать экземпляр класса Foo с экземпляром класса FooSession или с объектом fooSessionParams, зная, что создание fooSession из объекта fooSessionParams является асинхронной функцией? Я хотел создать экземпляр либо, выполнив:
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 async возвращает сам экземпляр. Тогда, если у меня есть экземпляр 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
Я нашел решение, которое выглядит как
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
}
Это работает, потому что Promises, который поддерживает async/await, может быть разрешен несколько раз с одним и тем же значением.
Ответ 8
Или вы можете просто придерживаться истинной модели ASYNC и не усложнять настройку. 9 из 10 раз это сводится к асинхронному и синхронному проектированию. Например, у меня есть компонент 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 без компрометации компонента с нулевым исключением, поскольку состояние устанавливается с настройками по умолчанию (пустой объект и пустые строки) до обратного вызова. Пользователь может увидеть пустой список на секунду, но затем он быстро заполняется. Еще лучше было бы применить счетчик, пока данные загружаются. Часто я слышу о людях, предлагающих чрезмерно сложные обходные пути, как это имеет место в этом посте, но первоначальный поток должен быть пересмотрен.
Ответ 9
На самом деле легко сначала определить вашу функцию как асинхронную функцию, чтобы эта функция возвращала объект/значение; Затем в конструкторе определите Немедленно вызванную функцию как асинхронную. Пример ниже
async function example() {return {name: 'test'};}
в конструкторе
(async () => {
this.value = await example();
})();