为何setInterval中Promise包装的pipe使用await不生效?
我把问题简化成了如下代码:
// test.js const request = require("request"); const fs = require("fs"); setInterval(async function () { console.log('before getData'); await getData(); }, 1000) function getData() { return new Promise((f, r) => { const file = fs.createWriteStream(`test.txt`); request(`https://www.google.com`) .pipe(file) .on("error", error => { r(error); }); file.on("finish", () => { console.log('pipe finished'); f(); }); }); }
我希望setInterval必须等待从Google流式传输数据到文本文件的操作完成后,再执行下一轮。为此我把流操作包装在Promise里,只有当finish回调触发时才resolve这个Promise。我预期pipe finished总会紧跟在before getData之后打印,但运行脚本时日志顺序不符合预期,比如:
node test.js before getData pipe finished before getData pipe finished before getData before getData pipe finished pipe finished before getData pipe finished before getData pipe finished before getData before getData pipe finished pipe finished ...
调试发现,流操作完成前setInterval就已经多次触发调用,为什么getData函数没有阻塞执行直到流操作完成?
核心原因
setInterval的工作机制是到固定间隔时间就触发回调,完全不管上一次的回调有没有执行完毕。你在回调里用async/await,虽然await getData()会等待Promise完成,但这只会让当前回调函数内部暂停,根本无法阻止setInterval到点就触发下一次回调。
比如如果getData()执行需要2秒,而setInterval间隔是1秒,第一次回调执行到await getData()时会暂停,1秒后setInterval会直接触发第二次回调,此时第一次的getData()还没完成,就会出现日志里重复的before getData。
解决方案
放弃使用setInterval,改用递归调用+setTimeout的方式,这样能确保上一次异步操作完全完成后,再安排下一次执行:
修改后的代码:
// test.js const request = require("request"); const fs = require("fs"); // 定义递归执行的任务函数 async function runTask() { console.log('before getData'); await getData(); // 上一次任务完成后,间隔1秒再执行下一次 setTimeout(runTask, 1000); } // 启动任务 runTask(); function getData() { return new Promise((f, r) => { const file = fs.createWriteStream(`test.txt`); request(`https://www.google.com`) .pipe(file) .on("error", error => { r(error); }); file.on("finish", () => { console.log('pipe finished'); f(); }); }); }
这样修改后,每次都会等getData()的流操作完全完成(pipe finished打印后),再过1秒才会触发下一轮的before getData,日志顺序会完全符合你的预期。
内容的提问来源于stack exchange,提问作者wizzfizz94

