实现原生 JavaScript 倒计时组件
运行预览
代码实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>倒计时组件</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#box {
width: 300px;
height: 200px;
border: 1px solid #000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
}
#countdown {
font-size: 32px;
font-variant-numeric: tabular-nums;
}
.actions {
display: flex;
gap: 8px;
}
</style>
</head>
<body>
<div id="box">
<div id="countdown" role="timer" aria-live="polite">00:00:10</div>
<div class="actions">
<button id="start" type="button">开始 / 继续</button>
<button id="pause" type="button">暂停</button>
<button id="reset" type="button">重置</button>
</div>
</div>
<script>
const countdown = document.getElementById('countdown')
const startButton = document.getElementById('start')
const pauseButton = document.getElementById('pause')
const resetButton = document.getElementById('reset')
let timer = null
let endTime = null
const duration = 10 * 1000
let remainingTime = duration
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
return [hours, minutes, secs]
.map(item => String(item).padStart(2, '0'))
.join(':')
}
function stopTimer() {
clearInterval(timer)
timer = null
}
function render(milliseconds) {
const totalSeconds = Math.ceil(milliseconds / 1000)
countdown.innerText = formatTime(totalSeconds)
}
function update() {
remainingTime = Math.max(0, endTime - Date.now())
render(remainingTime)
if (remainingTime === 0) {
stopTimer()
endTime = null
}
}
// 开始 / 继续
startButton.onclick = function () {
if (timer) return
if (remainingTime <= 0) return
endTime = Date.now() + remainingTime
update()
timer = setInterval(update, 1000)
}
// 暂停
pauseButton.onclick = function () {
if (!timer) return
remainingTime = Math.max(0, endTime - Date.now())
stopTimer()
endTime = null
render(remainingTime)
}
// 重置
resetButton.onclick = function () {
stopTimer()
endTime = null
remainingTime = duration
render(remainingTime)
}
</script>
</body>
</html>
笔记记录
原生 JavaScript 倒计时组件
是什么
这个倒计时组件从 10 秒开始,支持开始、暂停、继续和重置。
它最重要的设计不是每隔一秒把数字减一,而是记录目标结束时间,并在每次更新时用“结束时间减当前时间”重新计算剩余时长。
剩余时长 = 目标结束时间 - 当前时间这样即使定时器晚执行了,下一次更新也会根据真实时间自动校准,而不会不断累积误差。
长什么样
组件包含一个时间显示区域和三个操作按钮:
┌──────────────────────────┐
│ 00:00:10 │
│ │
│ 开始/继续 暂停 重置 │
└──────────────────────────┘时间统一显示为 HH:MM:SS。例如,10 秒显示为 00:00:10。
状态怎么保存
代码中有三个关键状态:
timer:保存setInterval返回的定时器标识。它不为null时,说明倒计时正在运行。endTime:倒计时运行时的目标结束时间。remainingTime:剩余毫秒数。暂停时保存当前进度,继续时用它计算新的结束时间。
运行和暂停时,事实来源不同:
运行中:endTime 决定还剩多久
暂停时:remainingTime 记录停在何处怎么执行
开始或继续
点击开始时,先用当前剩余时长计算目标结束时间:
endTime = Date.now() + remainingTime随后立即渲染一次,再启动定时器。立即渲染可以避免必须等待第一个一秒周期后页面才更新。
如果已有定时器,或者剩余时间已经为零,就直接返回,避免重复启动多个定时器。
每次更新
定时器只负责定期唤醒 update,真正的剩余时间由系统时间计算:
remainingTime = Math.max(0, endTime - Date.now())Math.max(0, ...) 确保时间不会显示成负数。到达零时清理定时器,并把结束时间恢复为 null。
渲染时使用 Math.ceil 把毫秒换算成秒。只要还剩一小部分时间,界面就仍显示一秒;真正结束后才显示 00:00:00。
暂停
暂停时先根据当前时间重新计算剩余时长,然后清理定时器。这样继续时可以从暂停的位置重新生成新的 endTime:
暂停:remainingTime = endTime - 当前时间
继续:endTime = 当前时间 + remainingTime重置
重置会清理正在运行的定时器、移除结束时间,并把剩余时长恢复为最初的 10 秒。
为什么不能只做 totalSeconds--
setInterval(fn, 1000) 的含义是:时间到达后,把回调放进任务队列等待执行。它不保证回调一定能严格每隔一秒执行。
主线程繁忙、浏览器切到后台或设备进入省电状态时,定时器都可能被延迟。如果每次回调只执行 totalSeconds--,这些延迟会不断累积,页面上的倒计时就会比真实时间慢。
错误思路:剩余秒数 = 上一次秒数 - 1
当前实现:剩余时长 = 结束时间 - 当前时间因此,setInterval 在这里是“更新界面的提醒器”,endTime 才是计算倒计时的时间基准。
记一下
- 时间统一用毫秒保存,只在渲染时转换成秒,减少单位混用。
- 开始前要防止重复创建定时器。
- 暂停、结束和重置时都要清理定时器。
clearInterval(null)不会报错,因此可以把清理逻辑集中到stopTimer。Date.now()可能受到系统时间手动调整的影响。如果场景要求只测量当前页面内的稳定时间间隔,可以进一步考虑单调递增的performance.now()。