我在构造函数期间有一些我想要的设置,但似乎不允许
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();
构造函数必须返回它“构造”的类的实例。因此,不可能返回 Promise<...>
并等待它。
你可以:
使您的公共设置异步。不要从构造函数中调用它。每当您想“完成”对象构造时调用它。异步函数运行(){让主题;调试(“新主题模型”);尝试 { 主题 = 新的 TopicsModel();等待主题.setup(); } 捕捉(错误){ 调试(“错误”,错误); } }
就绪设计模式
不要把对象放在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
与 Ready
属性一起使用,如上例所示。我不是 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 的简单声明性语义可以直接表达应该尽快采取行动的想法,并强调可能。一旦你开始用这些术语来构建事物,关于长时间运行的方法或构造函数的争论就变得没有意义了。
延迟初始化仍然有它的位置;正如我所提到的,您可以将准备就绪与延迟加载结合起来。但是,如果您可能不会使用该对象,那么为什么要尽早创建它呢?按需创建可能会更好。
还有其他解决方案。当我编写嵌入式软件时,我会预先创建所有内容,包括资源池。这使得泄漏不可能并且内存需求在编译时是已知的。但这只是一个小的封闭问题空间的解决方案。
请改用异步工厂方法。
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
魔法?
await
调用来获取对象。 const myObject = await MyClass.CreateAsync();
我找到了一个看起来像的解决方案
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 function
但 await this.initialization;
对我有用,不太清楚为什么
我知道它已经很老了,但另一种选择是拥有一个工厂来创建对象并等待其初始化:
// 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
使用私有构造函数和静态工厂方法 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();
使用返回实例的设置异步方法
在以下情况下我遇到了类似的问题:知道从 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));
主要是风格问题……
您可以选择将 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)
this.initialise()
不是构造函数中的阻塞调用。 initialise
和构造函数在 list
获得其等待值之前返回。
constructor
确实会在 initialise
返回之前返回:但是 initialise
函数将在列表加载之前不返回。 initialise
中的 await
正是为了实现这一点。
为承诺状态创建持有者:
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();
或者您可以坚持使用真正的 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 行为,而不会因空异常而损害组件,因为在回调之前使用默认值(空对象和空字符串)设置了状态。用户可能会在一秒钟内看到一个空列表,但很快就会填充。更好的是在数据加载时应用微调器。我经常听到有人建议过分复杂的工作,就像这篇文章中的情况一样,但应该重新检查原始流程。
topic = await TopicsModel.create();
new MyClass
创建一个对象,将其存储在this
中并调用 MyClass.constructor 对其进行初始化。返回它的是new
,而不是构造函数。因此,谈论返回 Promise 的构造函数是有意义的。Object
构造函数的引用。new
创建的是一个空闭包。对象本身是其属性的总和,这些属性由构造函数创建。