Skip to content

验证码倒计时定时器错乱问题

问题描述

在登录页面获取验证码时,当接口返回错误后调用 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 次:

leadingtrailing执行时机执行次数
truetrue0ms 和 1000ms2次
truefalse0ms1次
falsetrue1000ms1次
falsefalse不执行0次

返回函数的方法

javascript
const throttled = _.throttle(fn, 1000);

throttled();         // 调用
throttled.cancel();  // 取消待执行的 trailing 调用
throttled.flush();   // 立即执行待执行的调用

throttle vs debounce vs 状态锁

方案适用场景本场景问题
throttle滚动、拖拽等持续触发冷却期内无法重试
debounce搜索输入等需要等待停止点击后要等一会才执行
状态锁按钮防重复点击✓ 最适合

经验总结

  1. 使用 throttle 时注意 trailing 配置:默认 trailing: true 会在节流结束后再执行一次,可能导致意外行为。

  2. 重置状态时要取消 pending 调用:如果使用了 throttle/debounce,重置时要调用 .cancel() 方法。

  3. 按钮防重复点击优先用状态锁:比 throttle/debounce 更直观,逻辑更清晰。

  4. throttle 时间不宜过长:1500ms 太长,300ms 足够防双击。

基于 VitePress 构建