验证码倒计时定时器错乱问题
问题描述
在登录页面获取验证码时,当接口返回错误后调用 resetCount() 重置倒计时,但倒计时并没有被清除,仍在继续执行,且按钮仍可点击,导致定时器错乱。
涉及组件
login.vue
└── codeLogin.vue (codeLoginInstance)
└── countDown/index.vue (countInstance)问题代码
javascript
// countDown/index.vue
const startTiming = _.throttle(
function (noEmit: boolean = false) {
times.value = time.value;
work.value = new Worker(url);
// ...
},
1500,
{ leading: true, trailing: true } // 问题在这里
);
const refreshFn = () => {
timer.value && clearInterval(timer.value);
work.value && work.value.terminate();
timer.value && clearTimeout(timer.value);
times.value = 0;
// 缺少 startTiming.cancel()
};根本原因
1. trailing: true 导致延迟执行
lodash 的 _.throttle 配置了 trailing: true,这意味着在节流期间如果有调用,会在节流结束后再执行一次。
时间线还原:
0ms → 用户点击获取验证码
→ startTiming() 立即执行(leading: true)
→ times = 120,Worker 启动
→ trailing 调用被排队,等待 1500ms 后执行
500ms → 接口返回错误
→ 调用 refreshFn()
→ times = 0,Worker 终止
→ 但 trailing 调用没有被取消!
1500ms → trailing 触发 startTiming() 再次执行
→ times 又被设为 120,新 Worker 启动
→ 倒计时"复活",定时器错乱2. refreshFn 没有取消 throttle 的 pending 调用
refreshFn 只清理了 timer 和 Worker,但没有调用 startTiming.cancel() 来取消 throttle 排队的 trailing 调用。
3. 点击按钮没有状态检查
handleSendClick 没有检查当前是否正在倒计时,导致倒计时期间仍可触发。
解决方案
方案一:修复 throttle 配置(最小改动)
javascript
// 1. trailing 改为 false
const startTiming = _.throttle(
function (noEmit: boolean = false) {
times.value = time.value;
// ...
},
300, // 1500ms 太长,改为 300ms
{ leading: true, trailing: false }
);
// 2. refreshFn 中取消 pending 调用
const refreshFn = () => {
startTiming.cancel(); // 新增:取消 throttle 的 pending 调用
timer.value && clearInterval(timer.value);
work.value && work.value.terminate();
timer.value && clearTimeout(timer.value);
times.value = 0;
};
// 3. handleSendClick 添加状态检查
const handleSendClick = () => {
if (showTiming.value) return; // 新增:倒计时期间不允许点击
if (enableSlideVerify.value) {
captchaButton.value?.click();
} else {
startTiming();
}
};方案二:用状态锁替代 throttle(推荐)
javascript
// 移除 throttle,用 times.value > 0 作为天然的锁
const startTiming = (noEmit: boolean = false) => {
// 正在倒计时,不执行
if (times.value > 0) return;
times.value = time.value;
work.value = new Worker(url);
work.value.onmessage = function () {
times.value -= 1;
if (times.value <= 0) {
text.value = "重新发送";
work.value.terminate();
if (enableSlideVerify.value) {
reInitAliyunCaptcha();
}
}
};
if (!noEmit) {
emit("getCode");
}
};
const refreshFn = () => {
work.value && work.value.terminate();
times.value = 0;
};
const handleSendClick = () => {
if (showTiming.value) return;
if (enableSlideVerify.value) {
captchaButton.value?.click();
} else {
startTiming();
}
};知识点:lodash throttle
参数说明
javascript
_.throttle(func, wait, { leading, trailing })| 参数 | 说明 | 默认值 |
|---|---|---|
func | 要节流的函数 | - |
wait | 节流时间(毫秒) | - |
leading | 是否在节流开始时执行 | true |
trailing | 是否在节流结束时执行 | true |
四种组合效果
假设 wait=1000ms,用户在 0-1000ms 内点击了 5 次:
| leading | trailing | 执行时机 | 执行次数 |
|---|---|---|---|
| true | true | 0ms 和 1000ms | 2次 |
| true | false | 0ms | 1次 |
| false | true | 1000ms | 1次 |
| false | false | 不执行 | 0次 |
返回函数的方法
javascript
const throttled = _.throttle(fn, 1000);
throttled(); // 调用
throttled.cancel(); // 取消待执行的 trailing 调用
throttled.flush(); // 立即执行待执行的调用throttle vs debounce vs 状态锁
| 方案 | 适用场景 | 本场景问题 |
|---|---|---|
| throttle | 滚动、拖拽等持续触发 | 冷却期内无法重试 |
| debounce | 搜索输入等需要等待停止 | 点击后要等一会才执行 |
| 状态锁 | 按钮防重复点击 | ✓ 最适合 |
经验总结
使用 throttle 时注意
trailing配置:默认trailing: true会在节流结束后再执行一次,可能导致意外行为。重置状态时要取消 pending 调用:如果使用了 throttle/debounce,重置时要调用
.cancel()方法。按钮防重复点击优先用状态锁:比 throttle/debounce 更直观,逻辑更清晰。
throttle 时间不宜过长:1500ms 太长,300ms 足够防双击。