如何异步等待一个值被设置?

huangapple go评论72阅读模式
英文:

How to await a value being set asynchronously?

问题

预期常见问题:

  • 是的,我知道 Promise 是什么。
  • 不,我不能简单地将初始化逻辑移到构造函数中。它需要在 initMethod 中调用,因为 initMethod 是需要在特定时间调用的钩子。

抱歉,只是因为我看到一些类似的问题被标记为“重复”,所以我想把这些常见问题放在最前面。

问题

我的问题是以下的竞态条件:

class Service {

  private x: string | null = null;

  initMethod() {
    this.x = 'hello';
  }

  async methodA() {
    return this.x.length;
  }
}

const service = new Service();
setTimeout(() => service.initMethod(), 1000);
service.methodA().catch(console.log);
TypeError: Cannot read properties of null (reading 'length')
    at Service.methodA (<anonymous>:15:19)
    at <anonymous>:20:9
    at dn (<anonymous>:16:5449)

我需要类似 Promise 的东西,其已解决的值可以从代码的另一部分设置。类似于:

class Service {

  private x: SettablePromise<string> = new SettablePromise();

  initMethod() {
    this.x.set('hello');
  }

  async methodA() {
    return (await this.x).length;
  }
}

const service = new Service();
setTimeout(() => service.initMethod(), 1000);
service.methodA().catch(console.log);

我能想到的最好的方法是创建一个类,轮询一个值,直到它变为非空。我希望有更智能的方法。我不想调整轮询间隔。

编辑

对于最初的混乱示例表示抱歉。竞态条件是 methodA 可能在 initMethod 之前被调用。

initMethod 中也有一个不必要的 async。我只是把它设为 async,因为它基于的真实方法是 async 的。

英文:

Anticipated FAQ:

  • Yes, I know what a Promise is.
  • No, I can't simply move the init logic to the constructor. It needs to be called in the initMethod because the initMethod is a hook that needs to be called at a certain time.

Sorry, it's just that I saw some similar questions marked as "duplicate", so I wanted to put these FAQ at the top.

Question

My issue is the following race condition:

class Service {

  private x: string | null = null;

  initMethod() {
    this.x = &#39;hello&#39;;
  }

  async methodA() {
    return this.x.length;
  }
}

const service = new Service();
setTimeout(() =&gt; service.initMethod(), 1000);
service.methodA().catch(console.log);
TypeError: Cannot read properties of null (reading &#39;length&#39;)
    at Service.methodA (&lt;anonymous&gt;:15:19)
    at &lt;anonymous&gt;:20:9
    at dn (&lt;anonymous&gt;:16:5449)

I need something like a Promise whose settled value can be set from another part of the code. Something like:

class Service {

  private x: SettablePromise&lt;string&gt; = new SettablePromise();

  initMethod() {
    this.x.set(&#39;hello&#39;);
  }

  async methodA() {
    return (await this.x).length;
  }
}

const service = new Service();
setTimeout(() =&gt; service.initMethod(), 1000);
service.methodA().catch(console.log);

The best I can come up with is to make a class that polls a value until it turns non-null. I'm hoping there's something smarter. I don't want to have to fine-tune a poll interval.

Edits

Sorry for the initial confusing example. The race condition is that methodA can be called before initMethod.

There was also an unnecessary async in the initMethod. I just made it async because the real method it was based on is async.

答案1

得分: 2

以下是翻译好的内容:

在以下示例中,您可以在异步方法调用之前或之后运行init。两种方式都可以工作 -

const s = new Service()
// 初始化
s.init()
// 然后查询
s.query("SELECT * FROM evil").then(console.log)
const s = new Service()
// 首先查询
s.query("SELECT * FROM evil").then(console.log)
// 然后初始化
s.init()

deferred

解决方案始于通用的deferred值,允许我们在外部解析或拒绝一个Promise -

function deferred() {
  let resolve, reject
  const promise = new Promise((res, rej) => {
    resolve = res
    reject = rej
  })
  return { promise, resolve, reject }
}

service

现在我们将编写一个Service类,它有一个resource延迟值。init方法将在某个时间点解析资源。异步方法query将在继续之前等待资源 -

class Service {
  resource = deferred() // 延迟资源
  async init() {
    this.resource.resolve(await connect()) // 解析资源
  }
  async query(input) {
    const db = await this.resource.promise // 等待资源
    return db.query(input)
  }
}

connect

这只是我们在init方法中运行的一些示例操作。它返回一个具有模拟数据库调用的query方法的对象 -

async function connect() {
  await sleep(2000) // 模拟延迟
  return {
    query: (input) => {
      console.log("运行查询:", input)
      return ["hello", "world"] // 模拟数据结果
    }
  }
}

function sleep(ms) {
  return newPromise(r => setTimeout(r, ms))
}

demo

function deferred() {
  let resolve, reject
  const promise = new Promise((res, rej) => {
    resolve = res
    reject = rej
  })
  return { promise, resolve, reject }
}

class Service {
  resource = deferred()
  async init() {
    this.resource.resolve(await connect())
  }
  async query(input) {
    const db = await this.resource.promise
    return db.query(input)
  }
}

async function connect() {
  await sleep(2000)
  return {
    query: (input) => {
      console.log("运行查询:", input)
      return ["hello", "world"]
    }
  }
}

function sleep(ms) {
  return new Promise(r => setTimeout(r, ms))
}

const s = new Service()
s.query("SELECT * FROM evil").then(console.log)
s.init()

始终处理拒绝

如果init无法连接到数据库,我们需要通过资源来反映这一点,否则我们的程序将无限期挂起 -

class Service {
  resource = deferred()
  async init() {
    try {
      const db = await timeout(connect(), 5000) // 超时
      this.resource.resolve(db)
    }
    catch (err) {
      this.resource.reject(err) // 拒绝错误
    }
  }
  async query(input) {
    const db = await timeout(this.resource.promise, 5000) // 超时
    return db.query(input)
  }
}

function timeout(p, ms) {
  return Promise.race([
    p,
    sleep(ms).then(() => { throw Error("超时") }),
  ])
}
英文:

In the following example, you can run the init before or after the async method call. Either will work -

const s = new Service()
// init
s.init()
// then query
s.query(&quot;SELECT * FROM evil&quot;).then(console.log)
const s = new Service()
// query first
s.query(&quot;SELECT * FROM evil&quot;).then(console.log)
// then init
s.init()

deferred

The solution begins with a generic deferred value that allows us to externally resolve or reject a promise -

function deferred() {
  let resolve, reject
  const promise = new Promise((res,rej) =&gt; {
    resolve = res
    reject = rej
  })
  return { promise, resolve, reject }
}

service

Now we will write Service which has a resource deferred value. The init method will resolve the resource at some point in time. The asynchronous method query will await the resource before it proceeds -

class Service {
  resource = deferred() // deferred resource
  async init() {
    this.resource.resolve(await connect()) // resolve resource
  }
  async query(input) {
    const db = await this.resource.promise // await resource
    return db.query(input)
  }
}

connect

This is just some example operation that we run in the init method. It returns an object with a query method that mocks a database call -

async function connect() {
  await sleep(2000) // fake delay
  return {
    query: (input) =&gt; {
      console.log(&quot;running query:&quot;, input)
      return [&quot;hello&quot;, &quot;world&quot;] // mock data result
    }
  }
}

function sleep(ms) {
  return new Promise(r =&gt; setTimeout(r, ms))
}

demo

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

function deferred() {
  let resolve, reject
  const promise = new Promise((res,rej) =&gt; {
    resolve = res
    reject = rej
  })
  return { promise, resolve, reject }
}

class Service {
  resource = deferred()
  async init() {
    this.resource.resolve(await connect())
  }
  async query(input) {
    const db = await this.resource.promise
    return db.query(input)
  }
}

async function connect() {
  await sleep(2000)
  return {
    query: (input) =&gt; {
      console.log(&quot;running query:&quot;, input)
      return [&quot;hello&quot;, &quot;world&quot;]
    }
  }
}

function sleep(ms) {
  return new Promise(r =&gt; setTimeout(r, ms))
}

const s = new Service()
s.query(&quot;SELECT * FROM evil&quot;).then(console.log)
s.init()

<!-- end snippet -->

always handle rejections

If init fails to connect to the database, we need to reflect that with the resource, otherwise our program will hang indefinitely -

class Service {
  resource = deferred()
  async init() {
    try {
      const db = await timeout(connect(), 5000) // timeout
      this.resource.resolve(db)
    }
    catch (err) {
      this.resource.reject(err) // reject error
    }
  }
  async query(input) {
    const db = await timeout(this.resource.promise, 5000) // timeout
    return db.query(input)
  }
}

function timeout(p, ms) {
  return Promise.race([
    p,
    sleep(ms).then(() =&gt; { throw Error(&quot;timeout&quot;) }),
  ])
}

答案2

得分: 1

根据我理解,您需要在构造函数中解决一个Promise,但只有在调用initMethod时才解决它。

您可以在Promise旁边公开一个Promise解决器:

class Service {
  private x: string | null = null;
  private promise;
  private resolve;

  constructor() {
    this.promise = new Promise(resolve => this.resolve = resolve);
  }

  async initMethod() {
    // 进行异步操作
    await new Promise(resolve => setTimeout(resolve, 1000));
    this.x = 'hello';
    // 然后解决Service的Promise
    this.resolve();
  }

  async methodA() {
    await this.promise;
    return this.x.length;
  }
}

请参考 https://stackoverflow.com/questions/26150232/resolve-javascript-promise-outside-the-promise-constructor-scope 获取更多示例。

英文:

As I understand it, you need to have a promise settled within constructor but it have to be resolved only when initMethod is called.

You can expose a promise resolver alongside the promise :

class Service {
 private x: string | null = null;
  private promise;
  private resolve;

  constructor() {
     this.promise = new Promise(resolve =&gt; this.resolve = resolve);
  }

  async initMethod() {
    // Do your async stuff
    await new Promise(resolve =&gt; setTimeout(resolve, 1000));
    this.x = &#39;hello&#39;;
    // And resolve Service promise
    this.resolve();
  }

  async methodA() {
    await this.promise;
    return this.x.length;
  }
}

See https://stackoverflow.com/questions/26150232/resolve-javascript-promise-outside-the-promise-constructor-scope for more examples.

答案3

得分: 1

你需要使用一个 Promise 值来初始化一个属性,该属性只有在 initMethod() 完成后才会解析。

这还涉及维护 promise 的 resolve 回调作为类属性。

class Service {

  #initResolve;
  #initPromise = new Promise((resolve) => {
    this.#initResolve = resolve;
  });
  
  #x = null;

  async initMethod() {
    await new Promise(resolve => setTimeout(resolve, 1000));
    this.#x = 'hello';
    this.#initResolve(); // 解析 #initPromise
  }

  async methodA() {
    await this.#initPromise;
    return this.#x.length;
  }
}

const service = new Service();
console.log("methodA: start");
service.methodA().then(console.log.bind(console, "methodA: end:"));

setTimeout(async () => {
  console.log("initMethod: start");
  await service.initMethod()
  console.log("initMethod: end");
}, 1000);

备注:代码中使用 JavaScript 私有类特性,但我相信你可以将其翻译成 TypeScript。

英文:

You need to initialise a property with a Promise value that only resolves after initMethod() has completed.

This involves also maintaining the promise's resolve callback as a class property.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

class Service {

  #initResolve;
  #initPromise = new Promise((resolve) =&gt; {
    this.#initResolve = resolve;
  });
  
  #x = null;

  async initMethod() {
    await new Promise(resolve =&gt; setTimeout(resolve, 1000));
    this.#x = &#39;hello&#39;;
    this.#initResolve(); // resolve #initPromise
  }

  async methodA() {
    await this.#initPromise;
    return this.#x.length;
  }
}

const service = new Service();
console.log(&quot;methodA: start&quot;);
service.methodA().then(console.log.bind(console, &quot;methodA: end:&quot;));

setTimeout(async () =&gt; {
  console.log(&quot;initMethod: start&quot;);
  await service.initMethod()
  console.log(&quot;initMethod: end&quot;);
}, 1000);

<!-- end snippet -->

Note: Using JavaScript private class features for the Snippet but I'm sure you can translate it to Typescript.

huangapple
  • 本文由 发表于 2023年2月16日 08:34:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/75466751.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定