不能用 <time> 标签实现倒计时,因其仅用于静态时间语义;应使用 setInterval 结合 Date.now() 计算剩余毫秒数,并通过 textContent 更新显示,过期时清除定时器并设终止文案。

用 <time> 标签显示倒计时?别这么干
<time> 是语义化标签,用来标记 ** 静态的时间点或时间区间 **(比如发布日期、活动开始时间),浏览器不会自动更新它,也不支持动态倒计时。强行往里面塞 JS 更新的文本,反而破坏语义——屏幕阅读器会读出“2025-04-05T14:30”,而不是“还剩 2 小时 17 分”。真要用语义化,得配合 datetime 属性存原始时间,视觉部分另起炉灶。
倒计时核心:用 setInterval 算差值 + textContent 更新 DOM
最轻量、兼容性最好、也最容易控制的方式。关键不是“怎么显示”,而是“怎么算”和“怎么不卡”:
- 用
Date.now()算当前毫秒数,减去目标时间戳,避免依赖系统时钟同步(new Date()构造慢且易受本地时区 / 修改干扰) - 别用
setTimeout递归模拟定时器——误差会越滚越大;setInterval虽然也有漂移,但至少节奏可控 - 每次更新前先判断是否已过期,过期就
clearInterval并写死文案(比如“已结束”),否则继续跑 - 格式化建议封装成小函数,避免内联拼接:
formatDuration(ms)返回"02:17:05"这种字符串
const deadline = Date.parse('2025-04-05T14:30:00Z'); const timer = setInterval(() => { const remaining = deadline - Date.now(); if (remaining <= 0) {el.textContent = '已结束'; clearInterval(timer); return; } el.textContent = formatDuration(remaining); }, 1000);
为什么不用 requestAnimationFrame 做倒计时?
它适合动画帧同步(比如 canvas 动画、滚动效果),但倒计时本质是 ** 时间驱动而非帧驱动 **:
- 用户切到其他 tab 时,
requestAnimationFrame会被浏览器节流甚至暂停,倒计时直接“卡住”,而setInterval在后台仍按毫秒级推进(只要页面没被完全冻结) - 每秒调用 60 次
requestAnimationFrame去检查时间差,纯属浪费——你并不需要每帧都更新,用户也看不出 16ms 和 1000ms 更新一次的区别 - 如果硬要用,必须搭配
performance.now()+ 上次记录时间做差值补偿,代码复杂度陡增,得不偿失
倒计时文案要可访问,但不是靠 <time>
想让视障用户知道“还剩 3 分 22 秒”,正确做法是:
立即学习 “ 前端免费学习笔记(深入)”;
- 保持视觉文案清晰(如
<span id="countdown">03:22</span>) - 用
aria-live="polite"让屏幕阅读器感知变化:<span aria-live="polite" aria-atomic="true"></span> - 在 JS 中每次更新时,同步设置
aria-label(比如el.setAttribute('aria-label', '剩余 3 分钟 22 秒')),比塞进<time>更直接有效 - 别忘了加
role="timer",这是 ARIA 规范里专为倒计时定义的角色
真正容易被忽略的,是过期后是否清除了定时器、是否重置了 aria-live 区域、以及服务端时间与客户端时间偏差超过分钟级时该怎么对齐——这些不处理,倒计时看着动,其实早就不准了。