You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

同一div绑定keydown与keyup事件异常:计时器仅首次可用

问题:空格键控制计时器仅首次有效,后续操作失效

需求与异常表现

需求:松开空格键启动计时器,按下空格键停止计时器。
异常:首次操作正常,后续按空格无法正常控制计时器,逻辑完全失效。

原代码

HTML代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/public/css/style.css">
    <script src="/public/js/timer.js" defer></script>
</head>

<body>
    <section id="play-area" tabindex="-1">
        <h1 id="timer">00:00</h1>
    </section>
</body>

</html>

JavaScript代码

const timer = document.querySelector('#timer')
const playArea = document.querySelector('#play-area')
let timerStarted = false

// starts timer when spacebar is released
playArea.addEventListener('keyup', (e) => {
    if (e.keyCode === 32 && !timerStarted) startTimer()
})

// start timer function
function startTimer() {
    // assigning time values to zero
    let [milliseconds, seconds, minutes] = [0, 0, 0]
    let Interval
    // actual timer code
    Interval = setInterval(() => {
        milliseconds++
        if (milliseconds % 100 === 0) {
            milliseconds = 0
            seconds++
        }
        if (seconds % 60 === 0 && seconds !== 0) {
            seconds = 0
            minutes++
        }
        timer.innerText = `${minutes}:${seconds}.${milliseconds}`
    }, 10)
    playArea.addEventListener('keydown', (e) => {
        if (e.keyCode === 32) stopTimer(Interval)
    })
}

function stopTimer(interval) {
    clearInterval(interval)
    timerStarted = true
    setTimeout(function () {
        timerStarted = false
        console.log(timerStarted)
    }, 500)
    console.log(timerStarted)
}

问题原因

每次调用startTimer()时,都会给playArea重复绑定keydown事件监听器。第一次启动后绑定1个监听器,第二次启动绑定第2个,以此类推。按下空格时,所有绑定的监听器都会触发:多次执行clearInterval,同时反复修改timerStarted状态,直接导致逻辑彻底混乱。

修复后的代码

修改后的JavaScript代码

const timer = document.querySelector('#timer')
const playArea = document.querySelector('#play-area')
let isTimerRunning = false // 标记计时器是否正在运行
let timerInterval = null // 全局存储计时器实例
let [milliseconds, seconds, minutes] = [0, 0, 0] // 时间变量提至全局,保留计时状态(如需每次启动重置可移回startTimer)

// 只绑定一次keyup和keydown事件
playArea.addEventListener('keyup', (e) => {
    if (e.key === ' ' && !isTimerRunning) {
        startTimer()
    }
})

playArea.addEventListener('keydown', (e) => {
    if (e.key === ' ' && isTimerRunning) {
        stopTimer()
    }
})

function startTimer() {
    isTimerRunning = true
    // 如需每次启动重置时间,取消下面注释
    // [milliseconds, seconds, minutes] = [0, 0, 0]
    
    timerInterval = setInterval(() => {
        milliseconds++
        if (milliseconds % 100 === 0) {
            milliseconds = 0
            seconds++
        }
        if (seconds % 60 === 0 && seconds !== 0) {
            seconds = 0
            minutes++
        }
        // 格式化时间显示,补零更美观
        const formattedMinutes = String(minutes).padStart(2, '0')
        const formattedSeconds = String(seconds).padStart(2, '0')
        const formattedMs = String(milliseconds).padStart(2, '0')
        timer.innerText = `${formattedMinutes}:${formattedSeconds}.${formattedMs}`
    }, 10)
}

function stopTimer() {
    clearInterval(timerInterval)
    isTimerRunning = false
    // 防抖:500ms内禁止再次启动,避免误触
    setTimeout(() => {
        isTimerRunning = false
    }, 500)
}
关键优化点
  • 全局仅绑定一次keyup/keydown事件,彻底避免重复绑定导致的逻辑冲突
  • 用isTimerRunning明确标记计时器状态,逻辑更清晰,替代原有的timerStarted
  • 将计时器实例timerInterval和时间变量提至全局,方便事件监听器直接访问
  • 优化时间显示格式,补零后更规范
  • 保留防抖逻辑,避免短时间内重复触发启动

内容的提问来源于stack exchange,提问作者Umar Zahid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 14:01:36