设了个目标日期总忍不住看还有几天:做个倒计时小工具
之前报名了个考试,心里老惦记还有多少天。每天打开手机日历数一遍,数完就忘,第二天又数。后来我干脆写了个倒计时网页:填个目标日期,它自己算还剩多少天多少小时,挂在浏览器新标签页上,想看一眼就一眼。
这个小工具没后端、没接口,单页 HTML 加一点 JS 就够。真正要搞清楚的就一件事:怎么算"还剩多久",以及怎么别让它在后台偷偷算错。
一、核心算法:用差值,别用累加
倒计时的本质,是"目标时间减去现在时间"。最直觉的写法是用一个计数器每秒减一,但那是错的——页面切到后台,浏览器的定时器会被节流,每秒减一变成了每几秒减一,回来就发现时间对不上了。
正确做法永远是:每次刷新都拿当前时间去减目标时间,差值本身就是真相。
function tick() {
const target = new Date(localStorage.getItem('targetDate')).getTime();
const diff = target - Date.now(); // 毫秒差值,永远准确
if (diff <= 0) {
render(0, 0, 0, 0, true);
return;
}
const s = Math.floor(diff / 1000);
const days = Math.floor(s / 86400);
const hours = Math.floor((s % 86400) / 3600);
const mins = Math.floor((s % 3600) / 60);
const secs = s % 60;
render(days, hours, mins, secs, false);
}
setInterval(tick, 1000);
Date.now() 取的是系统当前时间戳,跟页面有没有被节流没关系。所以哪怕后台跑了半小时,切回来那一瞬间算出来的差值也是准的。setInterval 在这里只是负责"多久看一眼",不参与计算。
二、目标日期存本地,刷新不丢
填一次目标日期,刷新页面就没了,体验很差。用 localStorage 存起来,启动时读回来。
const input = document.getElementById('target');
input.addEventListener('change', () => {
// 存成 ISO 字符串,读的时候直接 new Date() 就能还原
localStorage.setItem('targetDate', new Date(input.value).toISOString());
tick();
});
// 启动时恢复
const saved = localStorage.getItem('targetDate');
if (saved) input.value = saved.slice(0, 16);
注意 input[type=datetime-local] 的值是 "2026-10-01T09:00" 这种格式,new Date() 能直接解析。存的时候转成 ISO 字符串,读的时候 slice(0,16) 截回输入框要的格式。
三、几个容易踩的坑
| 坑 | 现象 | 处理 |
|---|---|---|
| 用累加计数 | 后台回来时间变慢 | 改用 Date.now() 差值 |
| 没处理已过期 | 显示一堆负数 | diff<=0 直接归零并提示"已到" |
| 时区没注意 | 差了 8 小时 | 存之前用本地时间,读也用本地 |
| 刷新后目标丢 | 每次重填 | 存 localStorage |
时区这点特别容易中招:如果你存的是 UTC 时间、显示时又按本地算,就会差 8 小时。本工具全程用本地时间(datetime-local 和 Date.now() 都是本地),不存在这个问题。
四、想更省心可以加的地方
想要数字跳动的动画、想要到期弹个通知,都是叠加层,不是核心。核心就前面那 30 行:差值计算 + 每秒刷新 + 本地持久化。
小结
倒计时工具的核心不是"计时",是"算差值"——永远拿目标时间减当前时间,别用累加计数器,否则后台一节流就失真。目标日期存 localStorage 保证刷新不丢,记得处理"已过期"和时区两个边界。整个工具一百来行,复制过去加点样式就能用。
- 点赞
- 收藏
- 关注作者
评论(0)