使用 JavaScript 构建节日倒计时器
每年十二月,开发者都会面临同样的需求:为圣诞节、新年前夜或其他节日构建倒计时器。这听起来很简单,直到你的计时器偏差了几分钟、在用户切换标签页时出错,或者在午夜过后显示负数。
本文将向你展示如何构建一个可靠的 JavaScript 倒计时器,正确处理这些边界情况。你将了解为什么在每次计时时重新计算时间比递减计数器更好,如何正确处理 JavaScript 时区,以及如何避免大多数实现中常见的陷阱。
核心要点
- 在每次计时时使用
Date.now()重新计算剩余时间,而不是递减计数器,以避免计时器漂移 - 使用
new Date(year, month, day)在用户本地时区创建日期,无需外部库 - 始终存储并清除定时器 ID 以防止内存泄漏,特别是在单页应用程序中
- 通过基于实际时间差的计算来构建对浏览器标签页节流的弹性
为什么递减计数器会失败
构建节日倒计时 JavaScript 小部件的简单方法看起来是这样的:设置一个变量为剩余秒数,然后使用 setInterval 每秒减一。这在实践中会出问题。
问题在于 setInterval 计时器漂移。JavaScript 计时器不保证精确执行。当用户切换标签页时,浏览器会节流计时器以节省资源——有时每秒触发一次,有时每分钟触发一次。你的计数器继续递减,就好像时间正常流逝一样,但实际上并没有。
解决方法很简单:在每次计时时通过将 Date.now() 与目标日期进行比较来重新计算剩余时间。这样,即使你的计时器触发延迟,显示的时间仍然保持准确。
设置目标日期
对于节日倒计时,你通常希望在用户本地时区的特定日历日期的午夜。以下是正确设置的方法:
function getTargetDate(month, day) {
const now = new Date()
const year = now.getFullYear()
// If the holiday has passed this year, target next year
const target = new Date(year, month - 1, day, 0, 0, 0)
if (target <= now) {
target.setFullYear(year + 1)
}
return target
}
const christmasTarget = getTargetDate(12, 25)
使用 new Date(year, month, day) 会自动在用户的本地时区创建日期。这无需任何库即可处理 JavaScript 时区——倒计时显示的是距离用户所在位置 12 月 25 日午夜的时间。
夏令时注意事项:当你以这种方式构造日期时,JavaScript 的 Date 对象会自动处理夏令时转换。即使时钟发生变化,倒计时仍然保持准确。
核心倒计时逻辑
这是一个解决常见问题的生产就绪实现:
function createCountdown(targetDate, onUpdate, onComplete) {
let intervalId = null
function calculateRemaining() {
const now = Date.now()
const difference = targetDate.getTime() - now
// Prevent negative values
if (difference <= 0) {
clearInterval(intervalId)
onComplete()
return null
}
return {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
minutes: Math.floor((difference / (1000 * 60)) % 60),
seconds: Math.floor((difference / 1000) % 60)
}
}
function tick() {
const remaining = calculateRemaining()
if (remaining) {
onUpdate(remaining)
}
}
// Run immediately, then every second
tick()
intervalId = setInterval(tick, 1000)
// Return cleanup function
return () => clearInterval(intervalId)
}
这种方法同时解决了几个问题:
- 无漂移:每次计时时都会重新计算时间
- 无负值:倒计时完成时停止并清除定时器
- 正确清理:返回的函数让你可以在需要时停止计时器
- 标签页节流弹性:即使计时延迟,显示的时间仍然保持正确
Discover how at OpenReplay.com.
连接到 DOM
将倒计时连接到你的 HTML:
const display = document.getElementById('countdown')
const cleanup = createCountdown(
christmasTarget,
({ days, hours, minutes, seconds }) => {
display.textContent = `${days}d ${hours}h ${minutes}m ${seconds}s`
},
() => {
display.textContent = "It's here!"
}
)
// Call cleanup() when navigating away in a SPA
处理边界情况
已经过去:getTargetDate 函数会在节日已过的情况下自动滚动到下一年。
清除定时器:始终存储定时器 ID 并在倒计时完成时清除它。不这样做会导致内存泄漏,特别是在单页应用程序中。
无障碍访问:考虑在倒计时容器中添加 aria-live="polite",以便屏幕阅读器在不造成干扰的情况下宣布更新。
展望未来
即将推出的 Temporal API 将使 JavaScript 日期和时间处理变得更加简单,内置了明确的时区支持。在它在所有浏览器中发布之前,本文展示的模式仍然是可靠的选择。
结论
使用时间差计算而不是递减计数器来构建节日倒计时 JavaScript 小部件。无论浏览器节流、标签页切换还是夏令时转换,你的计时器都将保持准确——并且你将避免更简单方法带来的调试难题。
常见问题
浏览器会节流后台标签页中的 JavaScript 计时器以节省资源。如果你每秒递减一个计数器,即使标签页处于非活动状态,计数也会继续,就好像时间正常流逝一样。相反,应该在每次计时时使用 Date.now() 与目标日期进行比较来重新计算剩余时间。这确保了无论计时器实际触发频率如何都能保持准确性。
使用带有数字参数的 Date 构造函数,如 new Date(year, month, day),自动在用户的本地时区创建日期。这种方法不需要外部库,并确保每个用户看到的是他们自己时区午夜的倒计时,而不是固定的 UTC 时间。
当倒计时完成后 setInterval 继续运行,或者当用户在单页应用程序中导航离开时,就会发生内存泄漏。始终存储 setInterval 返回的定时器 ID,并在倒计时完成或组件卸载时调用 clearInterval。从倒计时创建器返回一个清理函数以便于处理。
Temporal API 将通过明确的时区支持和更清晰的语义简化日期和时间处理。然而,重新计算时间差而不是递减计数器的核心原则仍然适用。在 Temporal 在所有浏览器中发布之前,本文中的模式提供了可靠的跨浏览器解决方案。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.