如何实现函数调用排队,让每次调用在前一个执行完成后触发
问题:实现点击请求排队的slideUp动画
我写了一个slideUp函数,点击按钮会触发它,这个函数会调用返回Promise的异步函数removeSlideUp。但多次点击按钮时,removeSlideUp会被立即触发多次,我希望所有点击请求进入队列,等上一次的transitionend事件完成后,再按顺序逐个执行removeSlideUp。我是JavaScript新手,不知道问题出在哪。
原代码(含jQuery,需改为纯JS)
async function slideUp() { await removeSlideUp(); } function removeSlideUp() { return new Promise(function (resolve, reject) { //console.log(smartAlert.classList.contains("slideup")); if (smartAlert.classList.contains("slideup") === false) { smartAlert.classList.add("slideup"); smartAlert.addEventListener( "transitionend", function () { console.log("completed"); smartAlert.classList.remove("slideup"); resolve(); }, { once: true } ); } else { smartAlert.classList.remove("slideup"); resolve(); } }); } // 原代码用了jQuery,需替换为纯JS // $('button[name="btn_show"]').click(function () { // slideUp(); // });
问题根源
每次点击按钮都会独立调用slideUp(),虽然slideUp里用了await,但这些调用彼此独立,没有形成顺序执行的队列。前一个动画还没完成,下一个点击就已经触发了新的removeSlideUp调用,导致动画混乱。
解决方案:维护Promise队列
我们需要一个全局的Promise变量来保存当前的执行队列,每次点击都把新的任务追加到队列末尾,确保上一个任务完成后再执行下一个。
修正后的完整代码
// 获取DOM元素 const smartAlert = document.getElementById("smart-alert"); const buttonShow = document.getElementById("btn_show"); // 维护执行队列的Promise,初始为已完成状态 let queuePromise = Promise.resolve(); async function slideUp() { await removeSlideUp(); } function removeSlideUp() { return new Promise(function (resolve, reject) { if (!smartAlert.classList.contains("slideup")) { smartAlert.classList.add("slideup"); smartAlert.addEventListener( "transitionend", function () { console.log("completed"); smartAlert.classList.remove("slideup"); resolve(); }, { once: true } ); } else { smartAlert.classList.remove("slideup"); // 移除类时也等待transitionend,保证队列顺序执行 smartAlert.addEventListener( "transitionend", function () { resolve(); }, { once: true } ); } }); } // 点击事件:将任务加入队列 buttonShow.addEventListener("click", () => { queuePromise = queuePromise.then(() => slideUp()); });
参考优化版(基于Stack Overflow方案)
如果需要更灵活的动画切换(显示/隐藏),可以参考以下代码,同时修正了原参考代码的拼写错误(transitoinEnd改为transitionEnd):
const buttonShow = document.getElementById("btn_show"); const buttonHide = document.getElementById("btn_hide"); const smartAlert = document.getElementById("smart-alert"); // 执行队列 let queuePromise = Promise.resolve(); // 监听transitionend的通用函数 const transitionEnd = (element, message) => { return new Promise((resolve) => { const listener = () => { element.removeEventListener("transitionend", listener); resolve(message); }; element.addEventListener("transitionend", listener); }); }; // 显示按钮点击逻辑 buttonShow.addEventListener("click", () => { queuePromise = queuePromise.then(async () => { let result; if (smartAlert.classList.contains("slideup")) { smartAlert.classList.remove("slideup"); result = await transitionEnd(smartAlert, "提示框显示完成"); console.log(result); } else { smartAlert.classList.add("slideup"); result = await transitionEnd(smartAlert, "提示框隐藏完成"); console.log(result); setTimeout(() => { smartAlert.classList.remove("slideup"); }, 1000); result = await transitionEnd(smartAlert, "提示框显示完成"); console.log(result); } }); }); // 隐藏按钮点击逻辑 buttonHide.addEventListener("click", () => { queuePromise = queuePromise.then(async () => { smartAlert.classList.add("slideup"); const result = await transitionEnd(smartAlert, "提示框隐藏完成"); console.log(result); }); });
关键说明
queuePromise变量始终保存当前队列的最后一个Promise,每次点击都通过.then()追加新任务,确保顺序执行。- 所有涉及过渡动画的操作(添加/移除类)都必须等待
transitionend事件后再resolve,否则队列会提前执行下一个任务。 - 使用
{ once: true }或手动移除事件监听器,避免重复监听导致的逻辑混乱。
内容的提问来源于stack exchange,提问作者Zubair Ahmd
相关产品推荐
相关产品推荐

