告别 Date | JavaScript Temporal API 使用教程
写在之前
JavaScript 终于有了靠谱的日期时间 API,但这个 API 似乎还没有比较全面的中文文档。因此我写了这篇入门教程抛砖引玉,待大佬们完善Temporal的中文文档。
如果你写过前端,大概率被 Date 折磨过:
- 月份从 0 开始(1 月是
0,12 月是11); - 没有原生时区支持,跨时区计算全靠手搓;
- 对象可变,一不小心就改了原始值;
- 想做"加一天"这种简单操作,还要自己算毫秒。
Temporal 就是为了彻底解决这些问题而设计的新一代日期时间 API。
本文会按这个顺序来:
- 先看
Date到底有什么问题 - 快速上手 Temporal 核心类型
- 逐个讲解常用方法与实战
- 把原理放在靠后位置集中梳理
- 最后给你一个完整实例 + 逐段解析
⚠️ 注意 本文强调"先上手,再吃透"。 如果某个类型第一次没完全理解,先跟着写,后面原理章节会回收解释。
⚠️ 注意 本文封面源自于 MDN博客,如有侵权请联系作者删除。
🚫 警告 本内容首发伴莺の小窝,未经授权禁止搬运!
ℹ️ 提示 文章中包含部分AI内容,用于校正、完善文章,如有错误请向作者反馈。
一、Date 到底有什么问题?
在正式学 Temporal 之前,先快速过一遍 Date 的经典坑,这样你才会理解 Temporal 的设计动机。
1.1 月份从 0 开始
const d = new Date(2026, 2, 6);
console.log(d.toISOString());
// 输出:2026-03-06T... 而不是 2026-02-06
// 第二个参数 2 代表三月,不是二月
这是几乎所有 JS 初学者都会踩的坑。
1.2 对象可变(Mutable)
const start = new Date(2026, 0, 1);
const end = start; // 你以为复制了一份?
end.setMonth(11); // 实际上 start 也被改了!
console.log(start.getMonth()); // 11,不是 0
Date 是引用类型且可变,赋值只是复制引用,不是复制值。
1.3 没有原生时区支持
想知道东京当前时间? Date 只能拿到本地时间和 UTC,其它时区全靠手动偏移或第三方库。
1.4 日期运算靠手搓
// "今天加 30 天"
const today = new Date();
today.setDate(today.getDate() + 30);
// 能用,但可读性差,且修改了原对象
ℹ️ 提示 正是这些问题,催生了
moment.js、dayjs、date-fns等第三方库。 而 Temporal 的目标就是在语言层面一次性解决它们。
二、Temporal 是什么?
Temporal ( /ˈtempərəl/ ) 是 TC39(JavaScript 标准委员会)提出的新日期时间 API,目前处于 Stage 3 阶段,已被主流浏览器逐步实现。
它的核心设计原则:
- 不可变(Immutable) :所有操作返回新对象,不修改原值
- 类型明确:日期、时间、日期时间、时区时间各有专属类型
- 原生时区支持:内置 IANA 时区数据库
- 无歧义:月份从 1 开始,API 命名直观
⚠️ 注意 截至文档编辑时间,仅 Safari 暂不支持。 Chrome、Firefox、Safari 的预览版已支持 Temporal。 如果你的浏览器暂不支持,可以升级浏览器内核或使用 polyfill:
@js-temporal/polyfill。
Temporal 类型一览
先记住这张一图流,后面每个类型会逐一讲解:
| 类型 | 含义 | 示例 |
|---|---|---|
Temporal.PlainDate | 纯日期(无时间、无时区) | 2026-03-06 |
Temporal.PlainTime | 纯时间(无日期、无时区) | 14:30:00 |
Temporal.PlainDateTime | 日期 + 时间(无时区) | 2026-03-06T14:30:00 |
Temporal.ZonedDateTime | 日期 + 时间 + 时区(完整) | 2026-03-06T14:30:00+08:00[Asia/Shanghai] |
Temporal.Instant | 时间刻(UTC) | YYYY-MM-DD T HH:mm:ss.sssssssss Z/±HH:mm[TimeZone] |
Temporal.Duration | 时间段/持续时间 | 2 小时 30 分 |
Temporal.PlainYearMonth | 年月 | 2026-03 |
Temporal.PlainMonthDay | 月日 | 03-06 |
一句话总结:需要时区就用
ZonedDateTime,不需要就用Plain*系列。
三、快速上手:核心类型逐个写
3.1 Temporal.PlainDate — 纯日期
表示"某一天",不关心具体时间和时区。适用于生日、纪念日、排班日期等场景。
// 方式一:from 字符串
const date1 = Temporal.PlainDate.from('2026-03-06');
console.log(date1.toString()); // "2026-03-06"
// 方式二:from 对象
const date2 = Temporal.PlainDate.from({ year: 2026, month: 3, day: 6 });
console.log(date2.year); // 2026
console.log(date2.month); // 3 ← 终于是 3 了,不是 2!
console.log(date2.day); // 6
// 方式三:获取今天的日期
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // 当前日期,如 "2026-03-06"
原理补充
PlainDate没有时间信息,所以不会出现"跨时区日期变了"的问题。from()是 Temporal 所有类型的统一构造方式,支持字符串和对象两种入参。- 月份从 1 开始,这是 Temporal 最直观的改进之一。
日期运算
const date = Temporal.PlainDate.from('2026-03-06');
// 加 10 天
const later = date.add({ days: 10 });
console.log(later.toString()); // "2026-03-16"
// 减 1 个月
const earlier = date.subtract({ months: 1 });
console.log(earlier.toString()); // "2026-02-06"
// 原始对象不变!
console.log(date.toString()); // "2026-03-06" ← 不可变
💡 技巧 Temporal 的运算方法(如
add()、subtract())都返回新对象,原始值保持不变,详情请看 章节4.2。 因此,你可以放心地链式调用或传参,不用担心被意外修改。(它的便利程度不亚于map方法)
日期比较
const a = Temporal.PlainDate.from('2026-01-01');
const b = Temporal.PlainDate.from('2026-12-31');
console.log(Temporal.PlainDate.compare(a, b)); // -1(a 在 b 之前)
console.log(Temporal.PlainDate.compare(b, a)); // 1(b 在 a 之后)
console.log(Temporal.PlainDate.compare(a, a)); // 0(相等)
// 更直观的方式:计算两个日期的差
const diff = a.until(b);
const diff2 = b.since(a);
console.log(diff.toString()); // "P365D"-> P 指示 Period(期间),365D 表示 365 天
console.log(diff2.toString()); // "P365D"
3.2 Temporal.PlainTime — 纯时间
表示"某个时刻的时间",没有日期、没有时区。适合表达营业时间、闹钟等。
const time = Temporal.PlainTime.from('14:30:00');
console.log(time.hour); // 14
console.log(time.minute); // 30
console.log(time.second); // 0
// 加 2 小时 15 分
const later = time.add({ hours: 2, minutes: 15 });
console.log(later.toString()); // "16:45:00"
// 获取当前时间
const now = Temporal.Now.plainTimeISO();
console.log(now.toString()); // 如 "20:15:30.123456789"
💡 技巧
PlainTime精度可达纳秒级(9 位小数),比Date的毫秒精度高得多。
3.3 Temporal.PlainDateTime — 日期 + 时间
把日期和时间组合在一起,但仍然没有时区。适合表达"本地事件",比如会议安排。
const dt = Temporal.PlainDateTime.from('2026-03-06T14:30:00');
console.log(dt.year); // 2026
console.log(dt.hour); // 14
// 也可以从对象创建
const dt2 = Temporal.PlainDateTime.from({
year: 2026, month: 3, day: 6,
hour: 14, minute: 30
});
// 运算:加 1 天 2 小时
const result = dt.add({ days: 1, hours: 2 });
console.log(result.toString()); // "2026-03-07T16:30:00"
原理补充
PlainDateTime不包含时区信息,所以它表示的是一个"挂在墙上的钟看到的时间"。- 如果同一个会议需要让不同时区的人看到各自的本地时间,应该用
ZonedDateTime。
3.4 Temporal.ZonedDateTime — 完整时间(带时区)
这是最重量级的类型:日期 + 时间 + 时区,三者缺一不可。 适合表达:全球统一的某个精确时刻在某个时区的表现。
// 创建一个时区为上海的时间
const zdt = Temporal.ZonedDateTime.from({
year: 2026, month: 3, day: 6,
hour: 14, minute: 30,
timeZone: 'Asia/Shanghai'
});
console.log(zdt.toString());
// "2026-03-06T14:30:00+08:00[Asia/Shanghai]"
// 获取当前时区的当前时间
const now = Temporal.Now.zonedDateTimeISO();
console.log(now.toString());
// 查看同一时刻在东京是几点
const tokyo = now.withTimeZone('Asia/Tokyo');
console.log(tokyo.toString());
时区转换实战
// 场景:北京时间 2026-03-06 20:00 开播,纽约观众看几点?
const beijing = Temporal.ZonedDateTime.from({
year: 2026, month: 3, day: 6,
hour: 20, minute: 0,
timeZone: 'Asia/Shanghai'
});
const newYork = beijing.withTimeZone('America/New_York');
console.log(newYork.toString());
// "2026-03-06T07:00:00-05:00[America/New_York]"
// 纽约是早上 7 点
ℹ️ 提示
withTimeZone()不会改变"时间线上的那个点",只是换一种时区来表达同一个瞬间。 这和Date完全不同——Date根本没有时区切换的概念。
3.5 Temporal.Instant — 时间轴上的一刻
Instant 代表时间线上的一个精确点,不关心时区,类似 Unix 时间戳。
// 当前瞬间
const now = Temporal.Now.instant();
console.log(now.toString()); // 如 "2026-03-06T12:30:00Z"(UTC)
// 从纪元毫秒数创建
const fromMs = Temporal.Instant.fromEpochMilliseconds(1772582400000);
console.log(fromMs.toString());
// 转换为特定时区的 ZonedDateTime
const zdt = now.toZonedDateTimeISO('Asia/Shanghai');
console.log(zdt.toString());
原理补充
Instant只代表"时间线上的点",没有年月日时分秒的概念。- 想要年月日,必须给它一个时区,通过
toZonedDateTimeISO()转换。 - 它和
Date.now()本质类似,但精度更高(纳秒级)。
3.6 Temporal.Duration — 持续时间
表示一段时间长度,比如"2 小时 30 分"或"3 年 6 个月"。
// 创建 Duration
const d1 = Temporal.Duration.from({ hours: 2, minutes: 30 });
console.log(d1.toString()); // "PT2H30M" -> T 表示分隔日期部分和时间部分
const d2 = Temporal.Duration.from('P1Y6M'); // 1 年 6 个月
console.log(d2.months); // 6
// 两个日期之间的差距
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-03-06');
const diff = start.until(end);
console.log(diff.toString()); // "P64D"(64 天)
// 指定更大单位
const diff2 = start.until(end, { largestUnit: 'month' });
console.log(diff2.toString()); // "P2M5D"(2 个月 5 天)
Duration 运算
const meeting = Temporal.Duration.from({ hours: 1, minutes: 30 });
const breakTime = Temporal.Duration.from({ minutes: 15 });
// Duration 相加
const total = meeting.add(breakTime);
console.log(total.toString()); // "PT1H45M"
// 取绝对值(处理可能的负值)
const neg = Temporal.Duration.from({ hours: -3 });
console.log(neg.abs().toString()); // "PT3H"
3.7 Temporal.PlainYearMonth 与 PlainMonthDay
这两个是精简类型,分别表示"年月"和"月日"。
// 年月:适合表示账单月、学期等
const ym = Temporal.PlainYearMonth.from('2026-03');
console.log(ym.daysInMonth); // 31
console.log(ym.inLeapYear); // false
// 月日:适合表示生日(不含年份)、纪念日等
const md = Temporal.PlainMonthDay.from('03-06');
console.log(md.toString()); // "--03-06"(ISO 8601 格式)
四、从"会用"到"用对"
4.1 类型选择指南
不确定该用哪个类型?看这个决策流程:
-
需要时区吗?
- 是 →
ZonedDateTime - 否 → 继续
- 是 →
-
需要日期和时间都有吗?
- 都要 →
PlainDateTime - 只要日期 →
PlainDate - 只要时间 →
PlainTime
- 都要 →
-
只需要年月或月日?
- 年月 →
PlainYearMonth - 月日 →
PlainMonthDay
- 年月 →
-
只关心时间轴上的精确时刻?
- →
Instant
- →
4.2 不可变性
Temporal 所有类型都是不可变的——告别"改着改着原值没了"。每次运算都返回新对象:
const date = Temporal.PlainDate.from('2026-03-06');
const next = date.add({ days: 1 });
// date 完全不变
console.log(date.toString()); // "2026-03-06"
console.log(next.toString()); // "2026-03-07"
这意味着你可以放心传参、存储,不用担心被意外修改。
4.3 with() — 修改部分字段
所有 Plain* 类型都支持 with() 方法,用于"只改一部分":
const date = Temporal.PlainDate.from('2026-03-06');
// 只改月份
const july = date.with({ month: 7 });
console.log(july.toString()); // "2026-07-06"
// 只改年份
const nextYear = date.with({ year: 2027 });
console.log(nextYear.toString()); // "2027-03-06"
// PlainTime 同理
const time = Temporal.PlainTime.from('14:30:00');
const evening = time.with({ hour: 20 });
console.log(evening.toString()); // "20:30:00"
4.4 until() 与 since() — 计算时间差
这两个方法互为反向:
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-03-06');
// until:从 start 到 end 的距离(正值)
console.log(start.until(end).toString()); // "P64D"
// since:从 end 回看 start 的距离(也是正值,方向相反)
console.log(end.since(start).toString()); // "P64D"
常用选项:
// 指定最大单位为月
start.until(end, { largestUnit: 'month' });
// "P2M5D" → 2 个月零 5 天
// 指定最小单位为小时(PlainDateTime 场景)
const dt1 = Temporal.PlainDateTime.from('2026-03-06T08:00');
const dt2 = Temporal.PlainDateTime.from('2026-03-06T14:45');
dt1.until(dt2, { largestUnit: 'hour' });
// "PT6H45M" → 6 小时 45 分
⚠️ 注意 需要注意的是,
until()和since()是互为反向的,以下是 MDN 的说明:
since()method does this - other. To do other - this, use theuntil()method.
until()method does other - this. To do this - other, use thesince()method.按照 MDN 建议,
until()作用的是其它时间点到当前时间的方法。按照上述start和end的值举例,start.until(end)建议改为end.since(start),反之亦然。
4.5 比较与排序
Temporal 提供静态 compare() 方法,可直接配合 Array.sort():
const dates = [
Temporal.PlainDate.from('2026-12-25'),
Temporal.PlainDate.from('2026-01-01'),
Temporal.PlainDate.from('2026-07-04'),
];
dates.sort(Temporal.PlainDate.compare);
console.log(dates.map(d => d.toString()));
// ["2026-01-01", "2026-07-04", "2026-12-25"]
💡 技巧
compare(a, b)返回 -1 / 0 / 1,和Array.sort()的比较函数签名完全兼容,直接传入即可。
五、核心原理
5.1 为什么需要这么多类型?
传统 Date 只有一个类型,试图同时表示"本地时间"和"UTC 时间",结果两头都做不好。
Temporal 的设计哲学是:
不同场景用不同类型,类型本身就是文档。
当你看到代码里用了 PlainDate,你立刻知道这里不涉及时区。 当你看到 ZonedDateTime,你知道这个时间是带时区的精确时刻。
这种"类型即文档"的思路,能在编码阶段就避免大量歧义。
⚠️ 注意 虽然各个方法之间单词几乎一致,但大小写会直接影响调用。因此这也是常踩的坑之一。
在写教程时就踩了很多次
5.2 ISO 8601 字符串格式
Temporal 遵循 ISO 8601 标准,常见格式:
| 格式 | 含义 |
|---|---|
2026-03-06 | 纯日期 |
14:30:00 | 纯时间 |
2026-03-06T14:30:00 | 日期时间(无时区) |
2026-03-06T14:30:00+08:00[Asia/Shanghai] | 日期时间 + 时区 |
2026-03-06T06:30:00Z | UTC 时间 |
P1Y2M3DT4H5M6S | 持续时间:1年2月3天4时5分6秒 |
Duration 格式说明:P 开头,T 分隔日期部分和时间部分。
5.3 Temporal.Now — 获取当前时间的统一入口
Temporal.Now.instant() // 当前 Instant(UTC 精确时刻)
Temporal.Now.zonedDateTimeISO() // 当前 ZonedDateTime(系统时区)
Temporal.Now.plainDateISO() // 当前 PlainDate
Temporal.Now.plainTimeISO() // 当前 PlainTime
Temporal.Now.plainDateTimeISO() // 当前 PlainDateTime
所有方法都可以传入时区参数来获取特定时区的当前时间:
// 获取东京当前日期
const tokyoDate = Temporal.Now.plainDateISO('Asia/Tokyo');
5.4 溢出处理(overflow)
当你创建一个不合法的日期(比如 2 月 30 日),Temporal 提供两种策略:
// 默认:constrain(约束到合法范围)
Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 }, { overflow: 'constrain' });
// → "2026-02-28"(约束到该月最后一天)
// 严格模式:reject(直接报错)
Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 }, { overflow: 'reject' });
// → 抛出 RangeError
⚠️ 注意 默认行为是
constrain(静默纠正),不是报错。 如果你的业务需要严格校验输入,记得显式传入{ overflow: 'reject' }。
5.5 和 Date 的互操作
在迁移过渡期,你可能需要在 Date 和 Temporal 之间转换:
// Date → Temporal.Instant
const legacy = new Date('2026-03-06T12:00:00Z');
const instant = Temporal.Instant.fromEpochMilliseconds(legacy.getTime());
// Temporal.Instant → Date
const back = new Date(instant.epochMilliseconds);
// Temporal.Instant → ZonedDateTime
const zdt = instant.toZonedDateTimeISO('Asia/Shanghai');
console.log(zdt.toString());
// "2026-03-06T20:00:00+08:00[Asia/Shanghai]"
六、模拟实战:倒计时组件
这个例子综合了你前面学到的核心类型和方法,实现一个简单的倒计时页面。
6.1 完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Temporal 倒计时</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
font-family: Arial, sans-serif;
background: #0f172a;
color: #e2e8f0;
}
h1 { font-size: 1.4rem; font-weight: 400; }
/* 倒计时数字容器 */
.countdown {
display: flex;
gap: 16px;
}
/* 每个时间块 */
.block {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
/* 数字样式 */
.number {
font-size: 3rem;
font-weight: 700;
background: #1e293b;
border-radius: 12px;
padding: 12px 20px;
min-width: 90px;
text-align: center;
color: #38bdf8;
}
/* 标签文字 */
.label {
font-size: 0.85rem;
color: #94a3b8;
}
/* 状态提示 */
.status {
font-size: 1rem;
color: #facc15;
}
</style>
</head>
<body>
<h1>距离 2027 年元旦还有</h1>
<div class="countdown">
<div class="block">
<span class="number" id="days">--</span>
<span class="label">天</span>
</div>
<div class="block">
<span class="number" id="hours">--</span>
<span class="label">时</span>
</div>
<div class="block">
<span class="number" id="mins">--</span>
<span class="label">分</span>
</div>
<div class="block">
<span class="number" id="secs">--</span>
<span class="label">秒</span>
</div>
</div>
<p class="status" id="status"></p>
<script>
// 1. 定义目标时间:2027-01-01 00:00:00 上海时区
const target = Temporal.ZonedDateTime.from({
year: 2027, month: 1, day: 1,
hour: 0, minute: 0, second: 0,
timeZone: 'Asia/Shanghai'
});
// 2. 获取 DOM 元素引用
const elDays = document.getElementById('days');
const elHours = document.getElementById('hours');
const elMins = document.getElementById('mins');
const elSecs = document.getElementById('secs');
const elStatus = document.getElementById('status');
// 3. 核心更新函数
function tick() {
// 获取当前时区时间
const now = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
// 比较:如果已经过了目标时间
if (Temporal.ZonedDateTime.compare(now, target) >= 0) {
elDays.textContent = '00';
elHours.textContent = '00';
elMins.textContent = '00';
elSecs.textContent = '00';
elStatus.textContent = '🎉 新年快乐!';
return; // 停止计时
}
// 计算剩余时间,指定各单位
const remaining = now.until(target, {
largestUnit: 'day',
smallestUnit: 'second',
roundingMode: 'floor'
});
// 更新页面
elDays.textContent = String(remaining.days).padStart(2, '0');
elHours.textContent = String(remaining.hours).padStart(2, '0');
elMins.textContent = String(remaining.minutes).padStart(2, '0');
elSecs.textContent = String(remaining.seconds).padStart(2, '0');
}
// 4. 启动:立即执行一次 + 每秒更新
tick();
setInterval(tick, 1000);
</script>
</body>
</html>
6.2 逐段解析
- 目标时间用
ZonedDateTime创建,明确指定了Asia/Shanghai时区,不会因为用户系统时区不同而出错。 - 当前时间通过
Temporal.Now.zonedDateTimeISO('Asia/Shanghai')获取,保证和目标在同一时区下比较。 - 时间差用
now.until(target, { largestUnit: 'day' })计算,直接得到天、时、分、秒各字段,不需要手动除以 86400000 再取余。 - 比较用
ZonedDateTime.compare(),语义清晰:>= 0代表当前时间已到达或超过目标。 - 不可变性的好处:每次
tick()都重新获取now,不存在累计误差或意外修改。
6.3 对比 Date 写法
如果用传统 Date 实现同样功能:
// Date 版本(对比参考)
const target = new Date('2027-01-01T00:00:00+08:00').getTime();
function tick() {
const diff = target - Date.now();
if (diff <= 0) { /* ... */ return; }
const days = Math.floor(diff / 86400000);
const hours = Math.floor((diff % 86400000) / 3600000);
const mins = Math.floor((diff % 3600000) / 60000);
const secs = Math.floor((diff % 60000) / 1000);
// ...手动计算每一级单位
}
对比之下,Temporal 版本的优势很明显:
- 不需要手动做毫秒除法
until()直接返回结构化的时间差- 时区处理显式且可靠
七、真实实例:管理系统中时间模块
前面的倒计时是一个独立小页面,下面来看我的真实 React 项目中 Temporal 的用法——后台管理系统的布局组件。 虽然是用的 React 技术栈,但 Temporal 的核心用法和原理在任何框架中都是一样的。
👉 后台管理系统
这个管理系统的顶栏需要实时显示当前时间,同时根据时间自动切换暗黑模式。来看它是怎么用 Temporal 实现的。
7.1 自动暗黑模式:PlainDateTime 判断时段
const [darkMode, setDarkMode] = useState(
Temporal.Now.plainDateTimeISO().hour >= 18 ||
Temporal.Now.plainDateTimeISO().hour < 6
);
// 晚上 6 点到早上 6 点默认开启暗黑模式
逐行解析
Temporal.Now.plainDateTimeISO()获取当前本地日期时间(PlainDateTime类型)。.hour直接取小时数(0–23),命名直观,不需要像Date那样调getHours()。- 这里用
PlainDateTime而非ZonedDateTime,因为"当前用户看到的时间"就是本地时间,不需要跨时区。
💡 技巧 这是一种典型的用对类型场景: 判断当前是白天还是夜晚,只关心本地时间,所以用
PlainDateTime就够了,不需要引入时区复杂度。
7.2 实时时钟:ZonedDateTime + 精准定时器
管理系统的顶栏和锁屏界面都需要一个秒级实时时钟:
const [time, setTime] = useState(Temporal.Now.zonedDateTimeISO());
const timerRef = useRef(null);
useEffect(() => {
const nextTick = () => {
// ① 每次获取最新的带时区时间
const now = Temporal.Now.zonedDateTimeISO();
setTime(now);
// ② 计算"下一整秒"的时间点
const nextSecond = now.add({ seconds: 1 })
.round({
smallestUnit: 'second',
roundingMode: 'floor'
});
// ③ 用 until + total 精确算出到下一秒还剩多少毫秒
const msUntilNextSecond = now.until(nextSecond)
.total({ unit: 'millisecond' });
// ④ setTimeout 递归,动态计算下次执行时间
timerRef.current = setTimeout(nextTick, msUntilNextSecond);
};
nextTick(); // 立即执行第一次
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
}, []);
逐行解析
这段代码的核心思路是:每次动态计算到下一整秒的精确延迟。
- ① 获取当前时间:
Temporal.Now.zonedDateTimeISO()返回带时区的完整时间,精度到纳秒。 - ② 计算下一整秒:先
add({ seconds: 1 })加一秒,再round({ smallestUnit: 'second', roundingMode: 'floor' })向下取整到整秒——比如当前时间是14:30:01.347,加 1 秒得到14:30:02.347,向下取整得到14:30:02.000。 - ③ 计算剩余毫秒数:
now.until(nextSecond)得到一个Duration对象,.total({ unit: 'millisecond' })将它换算成毫秒总数。 - ④ 递归 setTimeout:用精确的毫秒数设定下一次触发,避免
setInterval的累积漂移问题。
ℹ️ 提示 为什么不用
setInterval(fn, 1000)?
setInterval每次固定延迟 1000ms,但实际执行可能因主线程繁忙而延迟。时间一长,显示时间会和真实时间越差越远。上面的方案每次都重新获取真实时间 + 精确计算剩余毫秒,所以:
- 秒跳变对齐整秒:用户看到的秒数切换不会有大幅的偏移
- 无累积误差:即使某一次延迟了,下一次会自动修正,计算误差不超过毫秒级,保证时钟长期运行也能保持准确。
- 不可变性保障:每次循环都是全新的
now对象,不存在引用共享问题
7.3 时间格式化显示
锁屏界面显示的大号时钟,直接用 toLocaleString 格式化为本地时间格式:
<p style={{ fontSize: '10vh', fontWeight: 'bold' }}>
{time.toLocaleString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</p>
// 显示效果:"20:15:30"
这里 time 就是前面 useState 中存的 ZonedDateTime 对象,因为它自带时区信息,toLocaleString 能正确地按时区渲染时间。
7.4 这个实例用到了哪些 Temporal API?
| 知识点 | 对应章节 | 实际用法 |
|---|---|---|
Temporal.Now.plainDateTimeISO() | 章节 3.3 | 判断当前小时,切换暗黑模式 |
Temporal.Now.zonedDateTimeISO() | 章节 3.4 | 获取带时区的实时时间 |
.add() + .round() | 章节 3.1 | 计算下一个整秒时间点 |
.until().total() | 章节 4.4 | 精确计算到下一秒的毫秒数 |
.toLocaleString() | 章节 8.5 | 把时间格式化为 HH:mm:ss 显示 |
| 不可变性 | 章节 4.2 | 每次循环创建新对象,无引用共享风险 |
✅ 总结 从这个实例可以看出,Temporal 在真实项目中并不需要"全部都用"。 只需要根据场景选对类型(
PlainDateTime或ZonedDateTime),配合add()、round()、until()等少量方法,就能优雅地解决实际问题。
八、常见场景速查
8.1 获取某月有多少天
const ym = Temporal.PlainYearMonth.from('2026-02');
console.log(ym.daysInMonth); // 28
const ym2 = Temporal.PlainYearMonth.from('2024-02');
console.log(ym2.daysInMonth); // 29(闰年)
8.2 判断闰年
const date = Temporal.PlainDate.from('2024-06-15');
console.log(date.inLeapYear); // true
const date2 = Temporal.PlainDate.from('2026-06-15');
console.log(date2.inLeapYear); // false
8.3 获取星期几
const date = Temporal.PlainDate.from('2026-03-06');
console.log(date.dayOfWeek); // 5(周五,ISO 8601:周一=1,周日=7)
8.4 获取一年中的第几天
const date = Temporal.PlainDate.from('2026-03-06');
console.log(date.dayOfYear); // 65
8.5 格式化显示
Temporal 本身不内置复杂的格式化方法,推荐配合 Intl.DateTimeFormat 使用:
const date = Temporal.PlainDate.from('2026-03-06');
// 使用 toLocaleString
console.log(date.toLocaleString('zh-CN'));
// "2026/3/6"
console.log(date.toLocaleString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
}));
// "2026年3月6日星期五"
九、排错清单
当 Temporal 代码"不对劲"时,按这个顺序排查:
- 类型选对了吗? 需要时区就用
ZonedDateTime,不要用PlainDateTime"凑合" - 时区写对了吗? 必须用 IANA 格式(如
Asia/Shanghai),不能写+08:00(部分方法不支持) - 溢出策略确认了吗? 默认是
constrain静默修正,业务校验场景记得用reject - 单位指定了吗?
until()/since()默认最大单位可能不是你想要的,显式传largestUnit - 浏览器支持确认了吗? 不支持时需要
@js-temporal/polyfill
💡 技巧 调试时善用
.toString()打印完整字符串,Temporal 的字符串格式自带类型信息,一眼就能看出问题。
下一步...
如果你已经读到这里,建议按照这个节奏继续:
- 把倒计时示例手写一遍(不要复制)
- 改造成"距离某个自定义事件"的倒计时
- 试试
PlainDate写一个简单的日历网格 - 用
ZonedDateTime做一个"世界时钟"(同时显示北京、东京、纽约时间) - 参考管理系统实例,在自己的项目中实现一个精准实时时钟
当你能"选对类型、算对时间、讲清原理",Temporal 就算入门了。