按钮点击瞬间必须立即禁用并显示加载动画,而非等待请求返回;正确做法是在事件回调首行设置loading状态,用css动画实现旋转图标,请求结束在finally中清理状态,并处理移动端触摸穿透问题。

点击瞬间就该禁用并显示动画,不是等请求返回才动
用户点下去那一帧,按钮状态就必须变——否则网络稍慢,就会连点好几次,后端收到重复请求。很多人把 button.disabled = true 和 loading 图标塞在 fetch().then() 里,结果请求已发、动画才起,完全失去防重复意义。
正确做法是:事件回调第一行就处理 UI 状态。不管请求是否成功,都要走完整生命周期。
- 立即设
button.classList.add('loading'),配合aria-busy="true"和aria-disabled="true" - 不要直接改
disabled属性——它会让 CSS 动画失效、:hover失效、屏幕阅读器跳过该按钮 - 用
pointer-events: none+opacity+filter: grayscale()模拟视觉禁用,保留过渡和伪元素能力 - 旋转图标必须只用
transform: rotate()和opacity,避免top/left/width触发重排
用纯 CSS 实现旋转图标,别依赖 JS 控制显隐
内联 SVG 或 ::after 伪元素配 @keyframes 是最轻量、最稳的方案。GIF 增加 HTTP 请求,JS 控制易漏掉错误路径,而 CSS 动画由浏览器合成层驱动,不卡主线程。
示例(可直接复制):
.btn.loading::after {
content: '';
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #999;
border-top-color: #007bff;
border-radius: 50%;
margin-left: 8px;
animation: spin 1s linear infinite;
will-change: transform;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
注意:border-radius: 50% + border-top-color 才能做出标准“缺一口”的旋转环;will-change: transform 在低端设备上仍建议保留。
请求结束必须在 finally 里清理状态,不是只写 success 路径
fetch 或 axios 的 .catch() 里恢复按钮状态是常见做法,但漏掉 .finally() 就会卡死——比如网络中断、AbortError、超时被 cancel,这些都不会进 .catch(),但 loading 状态却一直挂着。
关键逻辑必须落在 finally:
fetch(url).then(...).catch(...).finally(() => { btn.classList.remove('loading'); btn.setAttribute('aria-busy', 'false'); })- 如果封装了统一请求函数,确保拦截器里也覆盖
finally场景,而非只靠响应拦截 - 手动触发的取消(如
AbortController)也要同步清理 UI,不能指望浏览器自动处理
移动端要额外加 touch-action: none 防误触穿透
按钮加了 pointer-events: none 后,在 iOS Safari 上仍可能因 touch-action 默认值导致点击穿透到下层元素——尤其当 loading 层是 fixed 全屏遮罩时,用户可能误点背后按钮。
安全做法是显式控制:
- 给 loading 状态的按钮加
style="touch-action: none;"(或 CSS 中写touch-action: none;) - 若用了全屏 loading 遮罩,遮罩本身也要加
pointer-events: none+touch-action: none,否则滚动时可能触发底层操作 - 测试真机,模拟弱网+快速连点,确认无穿透、无卡顿、无 aria-busy 残留
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











