Async/Await类构造函数

Ale*_*ggs 108 javascript node.js async-await

目前,我正在尝试async/await在类构造函数中使用.这样我就可以获得e-mail我正在研究的Electron项目的自定义标签.

customElements.define('e-mail', class extends HTMLElement {
  async constructor() {
    super()

    let uid = this.getAttribute('data-uid')
    let message = await grabUID(uid)

    const shadowRoot = this.attachShadow({mode: 'open'})
    shadowRoot.innerHTML = `
      <div id="email">A random email message has appeared. ${message}</div>
    `
  }
})
Run Code Online (Sandbox Code Playgroud)

但是,目前该项目不起作用,出现以下错误:

Class constructor may not be an async method
Run Code Online (Sandbox Code Playgroud)

有没有办法绕过这个,以便我可以在其中使用async/await?而不是要求回调或.then()?

sle*_*man 181

永远不会奏效.

async关键字允许await在标记为的函数中使用,async但它也将该函数转换为promise生成器.所以标记的函数async将返回一个promise.另一方面,构造函数返回它正在构造的对象.因此,我们有一种情况,你想要同时返回一个对象和一个承诺:一个不可能的情况.

你只能使用async/await来使用promises,因为它们本质上是promises的语法糖.您不能在构造函数中使用promises,因为构造函数必须返回要构造的对象,而不是promise.

有两种设计模式可以克服这个问题,这两种模式都是在承诺出现之前发明的.

  1. 使用init()功能.这有点像jQuery的.ready().您创建的对象只能在其自己initready函数内使用:

    用法:

    var myObj = new myClass();
    myObj.init(function() {
        // inside here you can use myObj
    });
    
    Run Code Online (Sandbox Code Playgroud)

    执行:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用构建器.我没有看到这在javascript中使用得太多但是当需要异步构造对象时,这是Java中更常见的解决方法之一.当然,构造需要大量复杂参数的对象时使用构建器模式.这正是异步构建器的用例.区别在于异步构建器不返回对象,而是返回该对象的承诺:

    用法:

    myClass.build().then(function(myObj) {
        // myObj is returned by the promise, 
        // not by the constructor
        // or builder
    });
    
    // with async/await:
    
    async function foo () {
        var myObj = await myClass.build();
    }
    
    Run Code Online (Sandbox Code Playgroud)

    执行:

    class myClass {
        constructor (async_param) {
            if (typeof async_param === 'undefined') {
                throw new Error('Cannot be called directly');
            }
        }
    
        static build () {
            return doSomeAsyncStuff()
               .then(function(async_result){
                   return new myClass(async_result);
               });
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    使用async/await实现:

    class myClass {
        constructor (async_param) {
            if (typeof async_param === 'undefined') {
                throw new Error('Cannot be called directly');
            }
        }
    
        static async build () {
            var async_result = await doSomeAsyncStuff();
            return new myClass(async_result);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

注意:虽然在上面的例子中我们使用了异步构建器的promises,但严格来说它们并不是必需的.您可以轻松编写接受回调的构建器.


关于调用静态函数内部函数的注意

这与异步构造函数没有任何关系,但与关键字this实际意味着什么(对于来自自动解析方法名称的语言的人来说,这可能有点令人惊讶,也就是说,不需要this关键字的语言).

this关键字是指实例化的对象.不是班级.因此,您通常不能使用this内部静态函数,因为静态函数不绑定到任何对象,而是直接绑定到类.

也就是说,在以下代码中:

class A {
    static foo () {}
}
Run Code Online (Sandbox Code Playgroud)

你做不到:

var a = new A();
a.foo() // NOPE!!
Run Code Online (Sandbox Code Playgroud)

相反,你需要将其称为:

A.foo();
Run Code Online (Sandbox Code Playgroud)

因此,以下代码将导致错误:

class A {
    static foo () {
        this.bar(); // you are calling this as static
                    // so bar is undefinned
    }
    bar () {}
}
Run Code Online (Sandbox Code Playgroud)

要修复它,您可以bar使用常规函数或静态方法:

function bar1 () {}

class A {
    static foo () {
        bar1();   // this is OK
        A.bar2(); // this is OK
    }

    static bar2 () {}
}
Run Code Online (Sandbox Code Playgroud)

  • 目前是语言的限制,但我不明白为什么将来你不能像我们拥有常规函数和异步函数一样拥有 `const a = wait new A()`。 (6认同)
  • 我不明白这怎么是绝对不可能的。异步函数最终仍然返回,只是被推迟了。异步函数可以像普通函数一样愉快地返回,你只需要等待它。不存在根本性的不匹配。如下所示,有人已经解决了这个问题。 (5认同)
  • 这应该有更多的投票;) (2认同)
  • 您应该评论为什么下面的答案不充分(如果是)。或者以其他方式解决它。 (2认同)

Dow*_*oat 84

你绝对可以做到这一点.基本上:

class AsyncConstructor {
    constructor() {
        return (async () => {

            // All async code here
            this.value = await asyncFunction();

            return this; // when done
        })();
    }
}
Run Code Online (Sandbox Code Playgroud)

创建类使用:

let instance = await new AsyncConstructor();
Run Code Online (Sandbox Code Playgroud)

注意:如果需要使用super,则无法在异步回调中调用它.你必须在它之外调用它,所以这个解决方案不是 100%完美,但在我看来它是非常惯用的,我在我的代码中一直使用它.

  • @PAStheLoD 是的,这是打字稿的限制。通常在 JS 中,类“T”在构造时应该返回“T”,但为了获得异步能力,我们返回“Promise&lt;T&gt;”,它解析为“this”,但这会混淆打字稿。您确实需要外部返回,否则您将不知道 Promise 何时完成 - 因此这种方法在 TypeScript 上不起作用(除非有一些可能带有类型别名的 hack?)。但不是打字稿专家,所以不能谈论这一点 (3认同)
  • @JuanLanus异步块将自动捕获参数,因此对于参数x,您只需要执行`constructor(x){return(async()=&gt; {await f(x); return this})()}` (2认同)
  • @PAStheLoD:“return this”是必要的,因为虽然“constructor”会自动为您执行此操作,但 async IIFE 不会,并且您最终将返回一个空对象。 (2认同)

Vid*_*dar 10

因为异步函数是承诺,你可以在你的类上创建一个静态函数,它执行一个返回类实例的异步函数:

class Yql {
  constructor () {
    // Set up your class
  }

  static init () {
    return (async function () {
      let yql = new Yql()
      // Do async stuff
      await yql.build()
      // Return instance
      return yql
    }())
  }  

  async build () {
    // Do stuff with await if needed
  }
}

async function yql () {
  // Do this instead of "new Yql()"
  let yql = await Yql.init()
  // Do stuff with yql instance
}

yql()
Run Code Online (Sandbox Code Playgroud)

let yql = await Yql.init()从异步函数调用 with 。

  • 这不是 [slebetman 的回答](/sf/ask/3040208531/#43433773) 在 2017 年描述的“构建”模式吗? (2认同)

Dav*_*zzo 9

Unlike others have said, you can get it to work.

JavaScript classes can return literally anything from their constructor , even an instance of another class. So, you might return a Promise from the constructor of your class that resolves to its actual instance.

Below is an example:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

            return this; // Return the newly-created instance
        })();
    }
}
Run Code Online (Sandbox Code Playgroud)

Then, you'll create instances of Foo this way:

const foo = await new Foo();
Run Code Online (Sandbox Code Playgroud)

  • 我很抱歉,@trincot,发布了同样的内容。我在写我的文章时没有注意到 Downgoat 的回答。我希望我的回答不会搞乱整个 StackOverflow 问题;)我尊重你的反对票,尽管我不完全理解这对社区有何帮助,因为我的行为不是故意的。 (2认同)

nin*_*cko 8

权宜之计

您可以创建一个async init() {... return this;}方法,然后new MyClass().init()在您通常只说new MyClass().

这并不干净,因为它依赖于使用您的代码的每个人以及您自己,总是像这样实例化对象。但是,如果您仅在代码中的一两个特定位置使用此对象,则可能没问题。

但是,由于 ES 没有类型系统,因此出现了一个重大问题,因此如果您忘记调用它,您只是返回了,undefined因为构造函数什么都不返回。哎呀。更好的是做这样的事情:

最好的做法是:

class AsyncOnlyObject {
    constructor() {
    }
    async init() {
        this.someField = await this.calculateStuff();
    }

    async calculateStuff() {
        return 5;
    }
}

async function newAsync_AsyncOnlyObject() {
    return await new AsyncOnlyObject().init();
}

newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject {someField: 5}
Run Code Online (Sandbox Code Playgroud)

工厂方法解决方案(稍微好一点)

但是,您可能会不小心执行新的 AsyncOnlyObject,您可能应该创建Object.create(AsyncOnlyObject.prototype)直接使用的工厂函数:

async function newAsync_AsyncOnlyObject() {
    return await Object.create(AsyncOnlyObject.prototype).init();
}

newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject {someField: 5}
Run Code Online (Sandbox Code Playgroud)

但是,如果您想在许多对象上使用此模式……您可以将其抽象为装饰器或您在定义 like 后(详细地,呃)调用的东西postProcess_makeAsyncInit(AsyncOnlyObject),但在这里我将使用extends它,因为它有点适合子类语义(子类是父类+额外的,因为它们应该遵守父类的设计契约,并且可以做额外的事情;如果父类不是异步的,异步子类会很奇怪,因为它不能被初始化为相同的道路):


抽象解决方案(扩展/子类版本)

class AsyncObject {
    constructor() {
        throw new Error('classes descended from AsyncObject must be initialized as (await) TheClassName.anew(), rather than new TheClassName()');
    }

    static async anew(...args) {
        var R = Object.create(this.prototype);
        R.init(...args);
        return R;
    }
}

class MyObject extends AsyncObject {
    async init(x, y=5) {
        this.x = x;
        this.y = y;
        // bonus: we need not return 'this'
    }
}

MyObject.anew('x').then(console.log);
// output: MyObject {x: "x", y: 5}
Run Code Online (Sandbox Code Playgroud)

(不要在生产中使用:我没有考虑过复杂的场景,例如这是否是为关键字参数编写包装器的正确方法。)


Mik*_*ans 5

根据您的评论,您可能应该执行所有其他带有资产加载的HTMLElement所做的事情:使构造函数启动侧载操作,根据结果生成加载或错误事件。

是的,这意味着使用诺言,但同时也意味着“以与其他所有HTML元素相同的方式进行操作”,因此您处于良好的状态。例如:

var img = new Image();
img.onload = function(evt) { ... }
img.addEventListener("load", evt => ... );
img.onerror = function(evt) { ... }
img.addEventListener("error", evt => ... );
img.src = "some url";
Run Code Online (Sandbox Code Playgroud)

这将启动源资产的异步负载,该负载成功时将终止于源资产,onload错误时将终止于onerror。因此,让自己的班级也这样做:

class EMailElement extends HTMLElement {
  constructor() {
    super();
    this.uid = this.getAttribute('data-uid');
  }

  setAttribute(name, value) {
    super.setAttribute(name, value);
    if (name === 'data-uid') {
      this.uid = value;
    }
  }

  set uid(input) {
    if (!input) return;
    const uid = parseInt(input);
    // don't fight the river, go with the flow
    let getEmail = new Promise( (resolve, reject) => {
      yourDataBase.getByUID(uid, (err, result) => {
        if (err) return reject(err);
        resolve(result);
      });
    });
    // kick off the promise, which will be async all on its own
    getEmail()
    .then(result => {
      this.renderLoaded(result.message);
    })
    .catch(error => {
      this.renderError(error);
    });
  }
};

customElements.define('e-mail', EmailElement);
Run Code Online (Sandbox Code Playgroud)

然后,使renderLoaded / renderError函数处理事件调用和阴影dom:

  renderLoaded(message) {
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <div class="email">A random email message has appeared. ${message}</div>
    `;
    // is there an ancient event listener?
    if (this.onload) {
      this.onload(...);
    }
    // there might be modern event listeners. dispatch an event.
    this.dispatchEvent(new Event('load', ...));
  }

  renderFailed() {
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <div class="email">No email messages.</div>
    `;
    // is there an ancient event listener?
    if (this.onload) {
      this.onerror(...);
    }
    // there might be modern event listeners. dispatch an event.
    this.dispatchEvent(new Event('error', ...));
  }
Run Code Online (Sandbox Code Playgroud)

另请注意,我将您更改idclass,因为除非您编写一些怪异的代码以仅允许<e-mail>页面上元素的单个实例,否则您将无法使用唯一标识符,然后将其分配给一堆元素。


Iva*_*nin 5

该线程中有许多好的(和一些坏的)注释......但没有一个真正涵盖整个故事和 TypeScript。这是我的看法。

此问题有 2 个解决方法。

1. 使用闭包代替类:

async function makeAPI() {
  await youCanGoAsyncHere()

  async function fetchFirst() {}
  async function fetchSecond() {}

  return {
    fetchFirst,
    fetchSecond,
  }
}
Run Code Online (Sandbox Code Playgroud)

用闭包复制一些继承模式很混乱,但对于更简单的情况,它通常就足够了。

2. 使用带protected构造函数的工厂 和init

import * as U from "lib/utils"

class API {
  data!: number // the use of ! here is fine 
  // we marked the constructor "protected" + we call `init` in `make` 
  // assuming we don't like multiple `data?.something` checks

  protected constructor() {
    ...
  }

  protected async init() {
    await youCanGoAsyncHere()
    this.data = 123 // assume other methods depend on this data
  }

  fetchFirst() {}
  fetchSecond() {}

  static async make() {
    const api = new API()
    await api.init()
    return api
  }
}

const t = await Test.make()
console.log(t.data)
Run Code Online (Sandbox Code Playgroud)

这里的主要缺点是 JS/TS 中静态方法与泛型的继承有些削弱。


归档时间:

查看次数:

80176 次

最近记录:

6 年,1 月 前