2017-04-15 161 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