使用async/await进行顺序调用时返回过早的问题排查
我原本希望async函数fetchAsync在返回数据(或抛出异常)前保持阻塞状态,但实际输出的顺序和我预期的不一样。下面是使用Fetch API的示例代码,我本来以为要等对象完成文件内容初始化后,*** we are here!!! ***才会在finish initialization之后打印,但现在的顺序完全相反,我有点疑惑async/await是不是应该阻塞到执行完成才继续?
实际输出:
constructor entering fetchAsync... *** we are here!!! *** leaving fetchAsync. finish initialization
示例代码:
class A { filename = "./resources/test.json"; constructor() { console.log("constructor"); this.fetchAsync(this.filename) .then(data => this.initialize(data)).catch(reason => console.log(reason.message)) } async fetchAsync(filename) { console.log("entering fetchAsync..."); let data = await (await fetch(filename)).json(); console.log("leaving fetchAsync."); return data; } initialize() { setTimeout(() => console.log("finish initialization"), 1000) } } let a = new A(); console.log("*** we are here!!! ***");
别急,我来帮你理清楚这里的问题所在:
async函数本质是非阻塞的:哪怕你用了
await,async函数一被调用就会立即返回一个Promise,不会阻塞后续同步代码的执行。所以当构造函数里调用this.fetchAsync()时,这个函数会启动异步请求,但构造函数不会等它完成,而是直接继续执行后面的代码——也就是console.log("*** we are here!!! ***"),这就是为什么这句话会提前打印。构造函数不能用async/await阻塞:JavaScript不允许构造函数被标记为
async,因为构造函数必须返回实例本身,而不是Promise。所以你没法直接在构造函数里用await来等待异步操作完成。initialize里的setTimeout也是异步的:就算
fetchAsync完成后调用了initialize,里面的setTimeout是个宏任务,会把finish initialization的打印放到1秒后执行,所以它肯定会比前面的同步代码晚。
解决方法:让代码按预期顺序执行
如果你想让*** we are here!!! ***在finish initialization之后打印,可以试试这两种方案:
方案1:用静态async方法创建实例
通过静态方法来异步初始化实例,这样就能等待所有异步操作完成后再执行后续代码:
class A { filename = "./resources/test.json"; constructor() { console.log("constructor"); } async fetchAsync(filename) { console.log("entering fetchAsync..."); let data = await (await fetch(filename)).json(); console.log("leaving fetchAsync."); return data; } async initialize(data) { // 把setTimeout转为Promise,确保能被await等待 await new Promise(resolve => setTimeout(resolve, 1000)); console.log("finish initialization"); } static async createInstance() { const a = new A(); const data = await a.fetchAsync(a.filename); await a.initialize(data); return a; } } // 使用时等待实例初始化完成 A.createInstance().then(() => { console.log("*** we are here!!! ***"); }).catch(reason => console.log(reason.message));
方案2:暴露初始化Promise
在类里把初始化的Promise存起来,创建实例后手动等待这个Promise完成:
class A { filename = "./resources/test.json"; initPromise; // 存储初始化的Promise constructor() { console.log("constructor"); this.initPromise = this.fetchAsync(this.filename) .then(data => this.initialize(data)).catch(reason => console.log(reason.message)) } async fetchAsync(filename) { console.log("entering fetchAsync..."); let data = await (await fetch(filename)).json(); console.log("leaving fetchAsync."); return data; } async initialize() { await new Promise(resolve => setTimeout(resolve, 1000)); console.log("finish initialization"); } } // 创建实例后等待初始化完成 const a = new A(); a.initPromise.then(() => { console.log("*** we are here!!! ***"); });
核心总结:async/await只是让异步代码的写法更像同步,但它本质上还是异步的,不会阻塞整个事件循环。构造函数里的异步调用不会阻止后续同步代码执行,你需要显式等待这些异步操作完成后,再执行你想延后的代码。
内容的提问来源于stack exchange,提问作者John Difool

