Async / Await类构造函数

时间:2017-04-15 21:41:26

标签: javascript node.js async-await

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

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

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

Class constructor may not be an async method

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

16 个答案:

答案 0 :(得分:128)

这可以从不工作。

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

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

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

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

    用法:

    var myObj = new myClass();
    myObj.init(function() {
        // inside here you can use myObj
    });
    

    实现:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
    
  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();
    }
    

    实现:

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

    使用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);
        }
    }
    
  3.   

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

    关于在静态函数内调用函数的注意事项。

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

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

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

    class A {
        static foo () {}
    }
    

    你做不到:

    var a = new A();
    a.foo() // NOPE!!
    

    而你需要将其称为:

    A.foo();
    

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

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

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

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

答案 1 :(得分:42)

可以绝对这样做。基本上是:

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

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

            return this; // when done
        })();
    }
}

创建类使用:

let instance = await new AsyncConstructor();

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

答案 2 :(得分:5)

不同于其他人所说的,您可以使其正常工作。

JavaScript class可以从其constructor返回任何内容,甚至可以返回另一个类的实例。因此,您可能会从类的构造函数返回Promise来解析其实际实例。

下面是一个示例:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

            return this; // Return the newly-created instance
        })();
    }
}

然后,您将通过以下方式创建Foo的实例:

const foo = await new Foo();

答案 3 :(得分:4)

根据你的评论,你应该做每个其他带有资产加载的HTMLElement做的事情:让构造函数启动一个sideloading动作,根据结果生成一个load或error事件。

是的,这意味着使用promises,但它也意味着“以与其他所有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";

这将启动源资产的异步加载,当它成功时,以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);

然后你让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', ...));
  }

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

答案 4 :(得分:3)

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

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

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

答案 5 :(得分:2)

我根据@Downgoat的答案做了这个测试用例。
它在NodeJS上运行。 这是Downgoat的代码,其中setTimeout()调用提供了异步部分。

'use strict';
const util = require( 'util' );

class AsyncConstructor{

  constructor( lapse ){
    this.qqq = 'QQQ';
    this.lapse = lapse;
    return ( async ( lapse ) => {
      await this.delay( lapse );
      return this;
    })( lapse );
  }

  async delay(ms) {
    return await new Promise(resolve => setTimeout(resolve, ms));
  }

}

let run = async ( millis ) => {
  // Instatiate with await, inside an async function
  let asyncConstructed = await new AsyncConstructor( millis );
  console.log( 'AsyncConstructor: ' + util.inspect( asyncConstructed ));
};

run( 777 );

我的用例是Web应用程序服务器端的DAO。
在我看到的DAO中,它们每个都与一种记录格式相关联,在我的例子中是MongoDB集合,例如Cook。 cooksDAO实例保存厨师的数据。
在我烦躁不安的头脑中,我将能够实例化一个厨师的DAO,将cookId作为参数,实例化将创建对象并用厨师的数据填充它。
因此需要在构造函数中运行异步内容。
我想写:

let cook = new cooksDAO( '12345' );  

具有cook.getDisplayName()之类的可用属性。
使用此解决方案,我必须要做:

let cook = await new cooksDAO( '12345' );  

与理想非常相似。
另外,我需要在async函数中执行此操作。

我的B计划是根据@slebetman建议使用init函数将数据装入构造函数,然后执行以下操作:

let cook = new cooksDAO( '12345' );  
async cook.getData();

这不会违反规则。

答案 6 :(得分:1)

在构造中使用异步方法?

constructor(props) {
    super(props);
    (async () => await this.qwe(() => console.log(props), () => console.log(props)))();
}

async qwe(q, w) {
    return new Promise((rs, rj) => {
        rs(q());
        rj(w());
    });
}

答案 7 :(得分:1)

如果可以避免 extend,则可以避免所有类,并且可以将函数组合用作构造函数。您可以在范围中使用变量而不是类成员:

async function buildA(...) {
  const data = await fetch(...);
  return {
    getData: function() {
      return data;
    }
  }
}

并简单地将其用作

const a = await buildA(...);

如果您使用打字稿或流,甚至可以强制使用构造函数的界面

Interface A {
  getData: object;
}

async function buildA0(...): Promise<A> { ... }
async function buildA1(...): Promise<A> { ... }
...

答案 8 :(得分:1)

权宜之计

您可以创建一个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}

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

但是,那么您可能会意外地创建新的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}

但是,您说您想在许多对象上使用此模式...您可以将其抽象为装饰器或定义为postProcess_makeAsyncInit(AsyncOnlyObject)后调用的对象(俗称ugh),但是在这里我将使用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}

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

答案 9 :(得分:0)

构建器模式的变化,使用call():

function asyncMethod(arg) {
    function innerPromise() { return new Promise((...)=> {...}) }
    innerPromise().then(result => {
        this.setStuff(result);
    }
}

const getInstance = async (arg) => {
    let instance = new Instance();
    await asyncMethod.call(instance, arg);
    return instance;
}

答案 10 :(得分:0)

您可以立即调用返回消息的匿名异步函数并将其设置为message变量。如果您不熟悉这种模式,则可能需要看一下立即调用的函数表达式(IEFES)。这将像一种魅力。

var message = (async function() { return await grabUID(uid) })()

答案 11 :(得分:0)

这里有一些简单的事情。它适用于 TypeScript。

class Foo {
  private promiseReady;

  constructor() {
    this.promiseReady = this.init();
  }

  private async init() {
    await someAsyncStuff();
  }

  public ready() {
    return this.promiseReady;
  }
}

与承诺一起使用:

let foo = new Foo();
foo.ready().then(() =>{
  // It's ready
});

与异步/等待一起使用:

let foo= new Foo();
await foo.ready();

答案 12 :(得分:-1)

其他答案缺少明显的答案。只需从构造函数中调用异步函数:

constructor() {
    setContentAsync();
}

async setContentAsync() {
    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>
    `
}

答案 13 :(得分:-1)

@slebetmen接受的答案很好地解释了为什么它不起作用。除了该答案中介绍的两种模式之外,另一种选择是仅通过自定义异步获取器访问异步属性。然后,constructor()可以触发属性的异步创建,但是getter然后在使用或返回属性之前检查该属性是否可用。

当您要在启动时初始化全局对象,并且想在模块内部进行初始化时,此方法特别有用。无需初始化index.js并将实例传递到需要它的位置,而只需require将模块放在需要全局对象的任何地方。

用法

const instance = new MyClass();
const prop = await instance.getMyProperty();

实施

class MyClass {
  constructor() {
    this.myProperty = null;
    this.myPropertyPromise = this.downloadAsyncStuff();
  }
  async downloadAsyncStuff() {
    // await yourAsyncCall();
    this.myProperty = 'async property'; // this would instead by your async call
    return this.myProperty;
  }
  getMyProperty() {
    if (this.myProperty) {
      return this.myProperty;
    } else {
      return this.myPropertyPromise;
    }
  }
}

答案 14 :(得分:-1)

与异步构造函数最接近的是,如果尚未在其所有方法中都使用异步构造函数,则等待它完成执行:

class SomeClass {
    constructor() {
        this.asyncConstructor = (async () => {
            // Perform asynchronous operations here
        })()
    }

    async someMethod() {
        await this.asyncConstructor
        // Perform normal logic here
    }
}

答案 15 :(得分:-2)

您应该将then函数添加到实例。 Promise会将其识别为具有Promise.resolve自动

的可对象
const asyncSymbol = Symbol();
class MyClass {
    constructor() {
        this.asyncData = null
    }
    then(resolve, reject) {
        return (this[asyncSymbol] = this[asyncSymbol] || new Promise((innerResolve, innerReject) => {
            this.asyncData = { a: 1 }
            setTimeout(() => innerResolve(this.asyncData), 3000)
        })).then(resolve, reject)
    }
}

async function wait() {
    const asyncData = await new MyClass();
    alert('run 3s later')
    alert(asyncData.a)
}