Vue函数执行期间如何更新DOM?加载Spinner不显示求助
问题:耗时函数执行时加载Spinner无法及时显示
我有一个耗时约15秒的函数调用,想要在该函数执行期间显示加载Spinner。我知道DOM要等到函数执行完成后才会更新,除非手动触发。我试过nextTick和async/await,但Spinner始终要等到函数执行完成后才会显示。我故意省略了代码末尾的loading.value = false,验证下来Spinner确实是延迟显示,而非功能异常。
我的UI代码
<template> <Background> <Card> <CardHeader title="Materials" description="All Current Materials" add-button-text="Add Materials" add-button-link="/settings/Materials/addMaterial" > </CardHeader> <div v-if="loading"> <div class="row"> <div class="col-sm d-flex justify-content-center"> <ProgressSpinner /> </div> </div> </div> <button @click="add">Add</button> </Card> </Background> </template>
核心逻辑代码
<script setup> const loading = ref(false); let femaleStripSpacersNeeded = []; let spacers = []; let spacerQty = []; const getSpacerAlgo = SpacerAlgo(); function add() { loading.value = true; let stripWidths = [3000, 21000, 3000]; let stripTotal = [1, 1, 1]; for (let z = 0; z < stripWidths.length; z++) { for (let n = 0; n < stripTotal[z]; n++) { let stripWidth = stripWidths[z]; let femaleStripSpacers = getSpacerAlgo.minSpacers( stripWidth, spacers, spacerQty ); femaleStripSpacersNeeded.push(femaleStripSpacers[0]); spacerQty = femaleStripSpacers[1]; } } } </script>
尝试过的async/await版本
function add() { return new Promise((resolve) => { let stripWidths = [3000, 21000, 3000]; let stripTotal = [1, 1, 1]; for (let z = 0; z < stripWidths.length; z++) { for (let n = 0; n < stripTotal[z]; n++) { let stripWidth = stripWidths[z]; let femaleStripSpacers = getSpacerAlgo.minSpacers( stripWidth, spacers, spacerQty ); femaleStripSpacersNeeded.push(femaleStripSpacers[0]); spacerQty = femaleStripSpacers[1]; } } resolve("resolved"); }); } async function asyncCall() { loading.value = true; await add(); loading.value = false; }
解决方案
你之前的async/await写法无效,因为Promise内部的代码还是同步阻塞的——设置loading.value = true后,JavaScript引擎立刻执行了Promise里的所有耗时循环,根本没给浏览器留出渲染Spinner的时间。
正确的做法是:让浏览器先完成Spinner的渲染,再执行耗时任务。可以通过以下两种方式实现:
方法1:使用setTimeout将耗时任务推入宏任务队列
<script setup> const loading = ref(false); // ... 其他变量定义不变 async function add() { loading.value = true; // 用setTimeout把耗时任务放到下一个宏任务,让浏览器先渲染DOM await new Promise(resolve => setTimeout(resolve, 0)); let stripWidths = [3000, 21000, 3000]; let stripTotal = [1, 1, 1]; for (let z = 0; z < stripWidths.length; z++) { for (let n = 0; n < stripTotal[z]; n++) { let stripWidth = stripWidths[z]; let femaleStripSpacers = getSpacerAlgo.minSpacers( stripWidth, spacers, spacerQty ); femaleStripSpacersNeeded.push(femaleStripSpacers[0]); spacerQty = femaleStripSpacers[1]; } } loading.value = false; // 任务完成后关闭加载状态 } </script>
方法2:使用queueMicrotask推入微任务队列
如果不想用setTimeout(0延迟可能有极小的等待时间),可以用queueMicrotask,它会在当前宏任务结束后、下一个宏任务开始前执行,同样能让浏览器先完成渲染:
async function add() { loading.value = true; await new Promise(resolve => queueMicrotask(resolve)); // ... 耗时循环代码不变 loading.value = false; }
原理说明
- 设置
loading.value = true后,Vue会标记DOM需要更新,但不会立刻渲染。 - 通过
await一个微任务/宏任务,让JavaScript引擎暂停当前函数执行,把控制权交还给浏览器。 - 浏览器此时会执行DOM更新,显示Spinner,之后再回到JavaScript执行剩下的耗时任务。
另外,15秒的执行时间过长,建议把耗时的minSpacers计算拆分成多个小任务,用分批次执行的方式,避免长时间阻塞主线程,让页面保持响应。
内容的提问来源于stack exchange,提问作者bobthegoalie
相关产品推荐
相关产品推荐

