2017-04-15 159 views
8

此刻,我试图在类构造函数中使用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 

是否有办法规避,这样我可以在此使用异步/ AWAIT?而不是要求回调或.then()?

+3

构造函数的目的就是为了你分配一个对象,然后立即返回。你可以更具体地了解*为什么*你认为你的构造函数应该是异步的?因为我们几乎可以保证在这里处理[XY问题](https://meta.stackexchange.com/a/66378)。 –

+1

@ Mike'Pomax'Kamermans这很可能。基本上,我需要查询数据库才能获取加载此元素所需的元数据。查询数据库是一个异步操作,因此我需要一些方法来在构造元素之前等待它完成。我宁愿不使用回调,因为我在整个项目的其余部分都使用了等待/异步,并希望保持连续性。 –

+0

@ Mike'Pomax'Kamermans这是一个电子邮件客户端,其中每个HTML元素看起来都类似于'',并且使用'customElements .define()'方法。 –

回答

25

这可以从来没有工作。

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

您只能使用async/await在哪里使用promise,因为它们本质上是承诺的语法糖。您不能在构造函数中使用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 
    }); 
    

    实现:

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

    实现与异步/等待:

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

Note: although in the examples above we use promises for the async builder they are not strictly speaking necessary. You can just as easily write a builder that accept a callback.

+0

请注意,根据这些注释,这个想法是这是一个html元素,它通常没有手动的'init()',但具有与'src'或'href'(在本例中为'data-uid')等特定属性相关的功能,这意味着使用一个setter绑定并在每次绑定新值时启动init(也可能在构建过程中,但是当然不会等待所产生的代码路径) –

3

根据你的意见,你可能应该做任何其他HTMLElement的资产加载:做构造函数启动一个sideloading动作,根据结果产生一个负载或错误事件。

是的,这意味着使用承诺,但它也意味着“以与其他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', ...)); 
    } 

另外请注意,我改变你的idclass,因为除非您编写一些奇怪的代码,以便在页面上只允许单个实例的<e-mail>元素,否则不能使用唯一标识符,然后将其分配给一组元素。

1

您还可以创建在构造函数自动执行的异步匿名函数:

class MyClass { 
    constructor() { 
     (async() => { 
      let result = await foo(); 
      this.someProperty = result; 
      console.log(this.someProperty);// outputs: bar 
     })(); 
    } 
} 

function foo() { 
    return new Promise((resolve, reject) => { 
     setTimeout(() => resolve('bar'), 2000); 
    }); 
} 

new MyClass(); 
+2

这不符合您的期望。而不是'new MyClass()',尝试'const obj = new MyClass();的console.log(OBJ。someProperty)'。你会得到'undefined',因为'someProperty'只会在*对象被创建后被赋予“bar”2000 ms *。期望的是,一旦我创建了一个对象,它已经设置了'someProperty'。这段代码说明了为什么构造函数*不应该是异步的。 –

+1

我同意。虽然我确实认为对于某些使用情况,这种解决方案非常好,但我不会推荐它。当你需要在你的构造函数中做一些异步操作时,无论如何你的应用程序的设计可能会更好。 – afterburn

-3

您应该添加then函数实例。 Promise将其识别为一个thenable对象与Promise.resolve自动

const asyncSymbol = Symbol(); 
class MyClass { 
    constructor() { 
    // nothing to do with constructor 
    } 
    then(resolve, reject) { 
     return (this[asyncSymbol] = this[asyncSymbol] || new Promise((innerResolve, innerReject) => { 
      setTimeout(() => innerResolve(this), 3000) 
     })).then(resolve, reject) 
    } 
} 

async function wait() { 
    const myInstance = await new MyClass(); 
    alert('run 3s later') 
} 
+0

'innerResolve(this)'将不起作用,因为'this'仍然是可用的。这导致了一个永无止境的递归解决方案。 – Bergi

+0

对不起,我的错误没有覆盖'then'函数的返回来避免递归 –

-2

其他的答案缺少明显。只需拨打一个异步函数从你的构造:

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> 
    ` 
} 
+0

像[另一个“明显的”答案在这里](https://stackoverflow.com/a/47611907/1269037),这一个不会做程序员通常期望构造函数的内容,即在创建对象时设置内容。 –

+0

@DanDascalescu它是异步设置的,这正是提问者所要求的。你的观点是,创建对象时不会同步设置内容,问题不需要该内容。这就是为什么这个问题是关于在构造函数中使用await/async的原因。我已经演示了如何通过调用异步函数从构造函数中调用尽可能多的await/async。我已经完美地回答了这个问题。 – Navigateur