目前,我正在尝试在类构造函数中使用 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()?
<e-mail data-uid="1028"></email>
并使用 customElements.define()
方法填充信息。
.init()
这样的方法来执行异步操作。另外,由于您是子类 HTMLElement,因此使用此类的代码极有可能不知道它是异步的,因此您可能不得不寻找一个完全不同的解决方案。
这永远行不通。
async
关键字允许在标记为 async
的函数中使用 await
,但它也将该函数转换为 Promise 生成器。因此,标有 async
的函数将返回一个承诺。另一方面,构造函数返回它正在构造的对象。因此,我们遇到了一种情况,您希望同时返回一个对象和一个承诺:一种不可能的情况。
您只能在可以使用 Promise 的地方使用 async/await,因为它们本质上是 Promise 的语法糖。您不能在构造函数中使用 Promise,因为构造函数必须返回要构造的对象,而不是 Promise。
有两种设计模式可以克服这个问题,它们都是在 Promise 出现之前发明的。
使用 init() 函数。这有点像 jQuery 的 .ready()。您创建的对象只能在它自己的 init 或 ready 函数中使用: 用法:var myObj = new myClass(); myObj.init(function() { // 在这里你可以使用 myObj });实现:class myClass { constructor () { } init (callback) { // 做一些异步操作并调用回调: callback.bind(this)(); } } 使用生成器。我没有看到这在 javascript 中被大量使用,但是当需要异步构造对象时,这是 Java 中更常见的解决方法之一。当然,构建器模式在构造需要大量复杂参数的对象时使用。这正是异步构建器的用例。不同之处在于异步构建器不返回对象,而是该对象的承诺: 用法:myClass.build().then(function(myObj) { // myObj 由承诺返回,// 不是由构造函数/ / 或建造者 }); // 使用 async/await: async function foo () { var myObj = await myClass.build(); } 实现:class myClass { constructor (async_param) { if (typeof async_param === 'undefined') { throw new Error('不能直接调用'); } } 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'); } } 静态异步构建 () { var async_result = await doSomeAsyncStuff();返回新的 myClass(async_result); } }
注意:虽然在上面的例子中我们使用了异步构建器的 Promise,但严格来说它们并不是必需的。您可以轻松地编写一个接受回调的构建器。
注意在静态函数中调用函数。
这与异步构造函数无关,而是与关键字 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 () {}
}
您可以绝对做到这一点,方法是从构造函数返回一个 Immediately Invoked Async Function Expression。 IIAFE
是在 top-level await 可用之前在异步函数之外使用 await
所需的非常常见模式的花哨名称:
(async () => {
await someFunction();
})();
我们将使用此模式在构造函数中立即执行异步函数,并将其结果返回为 this
:
// 要在异步构造函数中使用的示例异步函数 async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } class AsyncConstructor { constructor(value) { return (async () => { // 在这里调用异步函数 await sleep(500); this.value = value; // 构造函数隐式返回 `this`,但这是一个 IIFE,所以 // 显式返回 `this`(否则我们会返回一个空对象)。 return this; })(); } } (async () => { console.log('Constructing...'); const obj = await new AsyncConstructor(123); console.log('Done:', obj); })();
要实例化类,请使用:
const instance = await new AsyncConstructor(...);
对于 TypeScript,您需要 assert 构造函数的类型是类类型,而不是返回类类型的承诺:
class AsyncConstructor {
constructor(value) {
return (async (): Promise<AsyncConstructor> => {
// ...
return this;
})() as unknown as AsyncConstructor; // <-- type assertion
}
}
缺点
使用异步构造函数扩展类会有限制。如果您需要在派生类的构造函数中调用 super,则必须在没有 await 的情况下调用它。如果您需要使用 await 调用超级构造函数,您将遇到 TypeScript 错误 2337:超级调用不允许在构造函数外部或在构造函数内部的嵌套函数中。有人认为让构造函数返回 Promise 是一种“不好的做法”。
在使用此解决方案之前,请确定您是否需要扩展该类,并记录必须使用 await
调用构造函数。
AsyncConstructor
创建一个实例并使用 export default await new AsyncConstructor();
将其共享到其他文件?这不起作用,因为我们只能在 async
函数中使用 await
。但是在我的示例中没有功能。
因为异步函数是承诺,你可以在你的类上创建一个静态函数,它执行一个返回类实例的异步函数:
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()
。
build
模式不一样吗?
不像其他人说的那样,你可以让它工作。
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();
call
的参数被忽略,因为它是一个箭头函数。
.call(this)
调用替换为正常的函数调用应该没问题。感谢您指出了这一点
权宜之计
您可以创建一个 async init() {... return this;}
方法,然后在您通常只说 new MyClass()
时执行 new MyClass().init()
。
这并不干净,因为它依赖于使用您的代码的每个人以及您自己,总是像这样实例化对象。但是,如果您仅在代码中的一两个特定位置使用此对象,则可能没问题。
不过,由于 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)
之后调用的东西,但在这里我将使用 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}
(不要在生产中使用:我没有考虑过复杂的场景,例如这是否是为关键字参数编写包装器的正确方法。)
根据您的评论,您可能应该做所有其他带有资产加载的 HTMLElement 所做的事情:使构造函数启动侧加载操作,根据结果生成加载或错误事件。
是的,这意味着使用 Promise,但这也意味着“以与所有其他 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 {
connectedCallback() {
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, use a promise:
new Promise((resolve, reject) => {
yourDataBase.getByUID(uid, (err, result) => {
if (err) return reject(err);
resolve(result);
});
})
.then(result => {
this.renderLoaded(result.message);
})
.catch(error => {
this.renderError(error);
});
}
};
customElements.define('e-mail', EmailElement);
然后让 renderLoaded/renderError 函数处理事件调用和 shadow 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>
元素的单个实例,否则您不能使用唯一标识符然后分配它到一堆元素。
我通常更喜欢返回新实例的静态异步方法,但这是另一种方法。它更接近于字面上等待构造函数。它适用于 TypeScript。
class Foo {
#promiseReady;
constructor() {
this.#promiseReady = this.#init();
}
async #init() {
await someAsyncStuff();
return this;
}
ready() {
return this.promiseReady;
}
}
let foo = await new Foo().ready();
我根据@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 集合,例如厨师。 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();
这不违反规则。
在构造函数中使用异步方法???
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());
});
}
qwe
、q
、w
、rs
和 rj
是什么意思?
que
和 q
只是数据库上查询函数的占位符。 rs
和 rj
代表 Promise 对象中的 resolve
和 reject
:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
如果您可以避免 extend
,您可以一起避免使用类并使用函数组合作为构造函数。您可以使用范围内的变量而不是类成员:
async function buildA(...) {
const data = await fetch(...);
return {
getData: function() {
return data;
}
}
}
并简单地将其用作
const a = await buildA(...);
如果您使用的是 typescript 或 flow,您甚至可以强制执行构造函数的接口
Interface A {
getData: object;
}
async function buildA0(...): Promise<A> { ... }
async function buildA1(...): Promise<A> { ... }
...
构建器模式的变体,使用 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;
}
我发现自己处于这样的情况并最终使用了 IIFE
// using TypeScript
class SomeClass {
constructor() {
// do something here
}
doSomethingAsync(): SomeClass {
(async () => await asyncTask())();
return this;
}
}
const someClass = new SomeClass().doSomethingAsync();
如果您有其他任务依赖于异步任务,您可以在 IIFE 完成执行后运行它们。
这里有很多很棒的知识和一些 super() 深思熟虑的回应。简而言之,下面概述的技术相当简单、非递归、异步兼容并且符合规则。更重要的是,我不相信它已经被正确地覆盖在这里 - 虽然如果有错请纠正我!
我们简单地将 II(A)FE 分配给实例 prop,而不是方法调用:
// it's async-lite!
class AsyncLiteComponent {
constructor() {
// our instance includes a 'ready' property: an IIAFE promise
// that auto-runs our async needs and then resolves to the instance
// ...
// this is the primary difference to other answers, in that we defer
// from a property, not a method, and the async functionality both
// auto-runs and the promise/prop resolves to the instance
this.ready = (async () => {
// in this example we're auto-fetching something
this.msg = await AsyncLiteComponent.msg;
// we return our instance to allow nifty one-liners (see below)
return this;
})();
}
// we keep our async functionality in a static async getter
// ... technically (with some minor tweaks), we could prefetch
// or cache this response (but that isn't really our goal here)
static get msg() {
// yes I know - this example returns almost immediately (imagination people!)
return fetch('data:,Hello%20World%21').then((e) => e.text());
}
}
看起来很简单,它是如何使用的?
// Ok, so you *could* instantiate it the normal, excessively boring way
const iwillnotwait = new AsyncLiteComponent();
// and defer your waiting for later
await iwillnotwait.ready
console.log(iwillnotwait.msg)
// OR OR OR you can get all async/awaity about it!
const onlywhenimready = await new AsyncLiteComponent().ready;
console.log(onlywhenimready.msg)
// ... if you're really antsy you could even "pre-wait" using the static method,
// but you'd probably want some caching / update logic in the class first
const prefetched = await AsyncLiteComponent.msg;
// ... and I haven't fully tested this but it should also be open for extension
class Extensior extends AsyncLiteComponent {
constructor() {
super();
this.ready.then(() => console.log(this.msg))
}
}
const extendedwaittime = await new Extensior().ready;
在发布之前,我在 the comments of @slebetman's comprehensive answer 中简要讨论了这种技术的可行性。我并不完全相信直接解雇,所以我想我会打开它以进行进一步的辩论/拆除。请尽你最大的努力:)
您可以使用 Proxy 的 construct
句柄来执行此操作,代码如下:
const SomeClass = new Proxy(class A {
constructor(user) {
this.user = user;
}
}, {
async construct(target, args, newTarget) {
const [name] = args;
// you can use await in here
const user = await fetch(name);
// invoke new A here
return new target(user);
}
});
const a = await new SomeClass('cage');
console.log(a.user); // user info
您可以立即调用返回消息的匿名异步函数并将其设置为消息变量。如果您不熟悉此模式,您可能需要查看立即调用函数表达式 (IEFES)。这将像一个魅力。
var message = (async function() { return await grabUID(uid) })()
其他答案缺少显而易见的。只需从构造函数中调用异步函数:
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>
`
}
this.myPromise =
(一般意义上)是微不足道的,因此在任何意义上都不是反模式。有完全有效的情况需要在构造时启动一个异步算法,它本身没有返回值,并且无论如何添加一个我们都很简单,所以建议不要这样做的人是误解了一些事情
您应该将 then
函数添加到实例。 Promise
会自动将其识别为带有 Promise.resolve
的 thenable 对象
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)
}
innerResolve(this)
不起作用,因为 this
仍然是 thenable。这导致了一个永无止境的递归解决方案。
@slebetmen 接受的答案很好地解释了为什么这不起作用。除了该答案中提供的两种模式之外,另一种选择是仅通过自定义异步 getter 访问您的异步属性。然后,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;
}
}
}
最接近异步构造函数的方法是等待它完成执行,如果它还没有在它的所有方法中执行:
class SomeClass {
constructor() {
this.asyncConstructor = (async () => {
// Perform asynchronous operations here
})()
}
async someMethod() {
await this.asyncConstructor
// Perform normal logic here
}
}
init()
,但具有与src
或href
等特定属性相关的功能(在这种情况下,data-uid
) 这意味着使用一个 setter 来绑定并在每次绑定新值时启动 init(也可能在构造期间,但当然无需等待生成的代码路径)this
指的是myClass
。如果您总是使用myObj
而不是this
,则不需要它const a = await new A()
。