一个关于时区的线上问题

大家好,我是 前端架构师 - 大卫

更多优质内容请关注微信公众号 @程序员大卫

初心为助前端人🚀,进阶路上共星辰✨,

您的点赞与关注❤️,是我笔耕不辍的灯💡。

一、背景

我们有一个较老的全球化项目,前端框架使用的是 Vue 2 + Element UI (v1.4.13)

在加拿大等北美地区测试时,发现一个非常诡异的问题: 当选择日期为 2025-10-06 时,实际显示却成了 2025-10-05

另外,在监听 onChange 事件时,还触发了内存溢出(死循环),导致浏览器卡死。 本文将结合实际调试过程,解释问题根源,并提供完整解决方案。

二、问题一:日期显示提前一天

1. Chrome 模拟时区复现

在 Chrome 控制台中按下 ESC,打开下方工具栏:

点击左下角三点图标,选择 Sensors

Location 一栏中,选择 "Mountain View"(美国山景城),即北美西海岸时区:

接着在控制台执行以下代码:

sql 复制代码
new Date('2025-09-26')
// Thu Sep 25 2025 17:00:00 GMT-0700 (Pacific Daylight Time)

new Date('2025-09-26T00:00:00')
Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)

可以看到,两种写法结果不同:第一个比预期早了一天。

2. 原因分析

这是由 JavaScript Date 构造函数的解析规则时区换算机制 共同导致的。

new Date('2025-09-26')

  • 当只传入 "YYYY-MM-DD" 时,JavaScript 会默认按 UTC 零点解析 : 即 2025-09-26T00:00:00.000Z
  • 若系统时区为北美 PDT(UTC-7),则要减去 7 小时:
ini 复制代码
2025-09-26 00:00:00 UTC = 2025-09-25 17:00:00 PDT
  • 因此结果显示为「9 月 25 日傍晚」,也就是你看到的「昨天」。

new Date('2025-09-26T00:00:00')

  • 当字符串中包含 T 和时间部分时,JavaScript 会按 本地时区 解析(而不是当作 UTC)。
  • 因此该写法得到正确的本地日期:2025-09-26 00:00:00 PDT

3. 解决方法

方法一:传入完整时间字符串(推荐)

js 复制代码
// 推荐,ISO标准
new Date('2025-09-26T00:00:00') // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)

// 浏览器也能识别,但不标准
new Date('2025-09-26 00:00:00') // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)

方法二:使用 moment / moment-timezone

1)使用 momenttoDate() 方法

js 复制代码
import moment from "moment";

moment("2025-09-26").toDate(); // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)

2)指定时区的写法

js 复制代码
import moment from "moment-timezone";

moment.tz("2025-09-26", "YYYY-MM-DD", "America/Los_Angeles").toDate(); // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)

3)格式化输出

js 复制代码
moment("2025-09-26").format("YYYY-MM-DDTHH:mm:ss") // 2025-09-26T00:00:00

方法三:数字构造方式

js 复制代码
// 注意:月份从 0 开始,8 = 九月
new Date(2025, 8, 26) // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)

✅ Element UI 2.x 之后改用数字构造,因此天然避免了该问题。

三、问题二:onChange 死循环与内存溢出

1. 复现代码

安装旧版本:

bash 复制代码
pnpm i element-ui@1.4.13

main.js 引入 ElementUI

javascript 复制代码
// ...
import ElementUI from "element-ui";
import "element-ui/lib/theme-default/index.css";

Vue.use(ElementUI);

编写 App.vue

html 复制代码
<script>
export default {
	data() {
		return {
			value: "",
		};
	},
	methods: {
		onChange(val) {
			if (!this.jjj) {
				this.jjj = 0;
			}
			this.jjj++;
			if (this.jjj > 100) {
				console.error("内存溢出");
				this.jjj = 0;
				return;
			}
			this.value = val;
		},
	},
};
</script>

<template>
	<div>
		<el-date-picker
			v-model="value"
			@change="onChange"
			type="date"
			placeholder="选择日期范围"
			clearable
		>
		</el-date-picker>
	</div>
</template>

onChange 会被反复触发,造成内存溢出,控制台打印日志:内存溢出

2. 解决方案:封装兼容组件

新建一个 ElDatePickerTimezone.vue,将所有日期组件替换为该封装版本。

App.vue 使用方式:

html 复制代码
<script>
import ElDatePickerTimezone from "./components/ElDatePickerTimezone.vue";

export default {
	components: { ElDatePickerTimezone },
	data() {
		return {
			value: "",
		};
	},
	methods: {
		onChange(val) {
			this.value = val;
		},
	},
};
</script>

<template>
	<div>
		<ElDatePickerTimezone
			v-model="value"
			@change="onChange"
			type="date"
			placeholder="选择日期范围"
			clearable
		>
		</ElDatePickerTimezone>
	</div>
</template>

ElDatePickerTimezone.vue 组件封装如下:

html 复制代码
<template>
	<el-date-picker v-bind="attrs" v-on="listeners" :value="proxyValue">
		<!-- 默认插槽转发 -->
		<slot />
		<!-- 作用域插槽转发 -->
		<template v-for="(_, name) in $scopedSlots" v-slot:[name]="slotProps">
			<slot :name="name" v-bind="slotProps" />
		</template>
	</el-date-picker>
</template>

<script>
import moment from "moment";

const YMD_RE = /^\d{4}-\d{2}-\d{2}$/;

// 把传进来的字符串里所有 正则特殊字符(比如 . * + ? ^ $ { } ( ) | [ ] \)都加上反斜杠转义。
// 举例: '*' => '\*'
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

export default {
	name: "ElDatePickerTimezone",
	inheritAttrs: false,

	props: {
		value: {
			type: [String, Number, Date, Array],
			default: null,
		},
	},

	computed: {
		listeners() {
			return this.$listeners;
		},
		attrs() {
			const { value, ...rest } = this.$attrs;
			return rest;
		},
		proxyValue() {
			return this.normalizeIn(this.value);
		},
		rangeSeparator() {
			return (
				this.$attrs["range-separator"] || this.$attrs.rangeSeparator || " - "
			);
		},
		rangeRegex() {
			return new RegExp(
				"^\\s*(\\d{4}-\\d{2}-\\d{2})\\s*" +
					esc(this.rangeSeparator) +
					"\\s*(\\d{4}-\\d{2}-\\d{2})\\s*$"
			);
		},
		// 如果 rangeRegex 没匹配上,再尝试用一个宽松规则
		hyphenFallbackRegex() {
			return /^\s*(\d{4}-\d{2}-\d{2})\s*-\s*(\d{4}-\d{2}-\d{2})\s*$/;
		},
	},
	methods: {
		normalizeIn(val) {
			const toDate = (x) => {
				if (x === "" || x === null || x === undefined) return null;
				if (x instanceof Date) return isNaN(x.getTime()) ? null : x;

				if (typeof x === "string") {
					const s = x.trim();

					// range: 优先匹配实际分隔符
					let m = this.rangeRegex.exec(s);
					if (!m) m = this.hyphenFallbackRegex.exec(s);
					if (m) {
						const [, a, b] = m;
						const m1 = moment(a, "YYYY-MM-DD", true);
						const m2 = moment(b, "YYYY-MM-DD", true);
						return [
							m1.isValid() ? m1.toDate() : null,
							m2.isValid() ? m2.toDate() : null,
						];
					}

					// 单个 YYYY-MM-DD
					if (YMD_RE.test(s)) {
						const md = moment(s, "YYYY-MM-DD", true);
						return md.isValid() ? md.toDate() : null;
					}
				}

				const any = moment(x);
				return any.isValid() ? any.toDate() : null;
			};

			return Array.isArray(val) ? val.map(toDate) : toDate(val);
		},
	},
};
</script>

四、补充知识

4.1 介绍下 PDT 和 PST

1. PDT 和 PST 是什么?

它们都是美国西海岸(包括加州、华盛顿州等地)的时区:

  • PDT = Pacific Daylight Time(太平洋夏令时间)
  • PST = Pacific Standard Time(太平洋标准时间)

2. 两者的区别

名称 全称 与 UTC 的时差 使用时间 举例城市
PST Pacific Standard Time UTC − 8 小时 冬季使用(约 11 月初到次年 3 月中旬) 洛杉矶、旧金山、山景城、西雅图
PDT Pacific Daylight Time UTC − 7 小时 夏季使用(约 3 月中旬到 11 月初) 同上

👉 简单记法:

夏天用 PDT(慢 7 小时)

冬天用 PST(慢 8 小时)

举个例子:

假设 UTC 时间是:2025-09-26 00:00:00

  • PDT(夏天)下:是 2025-09-25 17:00:00
  • PST(冬天)下:是 2025-09-25 16:00:00

3. 为什么要区分?

美国实行 夏令时制度 (Daylight Saving Time, DST), 目的是让人们在夏天"更晚天黑",充分利用日照。 所以每年春天会:

  • 把时间拨快一小时(进入 PDT);
  • 到秋天再拨回一小时(回到 PST)。

4.2 介绍下 ISO

1. 什么是 ISO 标准(ISO 8601)

ISO 是国际标准化组织(International Organization for Standardization )的简称。 ISO 8601 是它为"日期与时间的表示法"制定的国际标准。

✅ 目的:让全世界的计算机系统用同一种方式理解时间,避免歧义。

比如:

  • 🇺🇸 美国人习惯写 09/26/2025(月/日/年)
  • 🇨🇳 中国人习惯写 2025-09-26(年-月-日)
  • 🇫🇷 法国人可能写 26/09/2025(日/月/年)

这些格式人能分辨,但程序会混淆。 所以 ISO 8601 统一规定写成:

ruby 复制代码
YYYY-MM-DDTHH:mm:ssZ

2. 字符串里的 T 是什么意思?

在 ISO 8601 中,T 是一个固定分隔符,意思是:

Time」的缩写,用来分隔日期和时间部分。

举个例子:

js 复制代码
2025-09-26T00:00:00

可以理解为:

makefile 复制代码
日期: 2025-09-26
时间: 00:00:00

中间的 T 就相当于写成 "2025-09-26 00:00:00" 的空格,只不过更标准化、机器可解析

3. 常见 ISO 格式举例

格式 含义 备注
2025-09-26 仅日期(UTC 解析) 容易产生时区偏移 ⚠️
2025-09-26T00:00:00 本地时间零点 ✅ 推荐用于 JS 本地时间
2025-09-26T00:00:00Z UTC 时间零点(Z = Zulu = UTC) ✅ 推荐用于跨时区传输
2025-09-26T00:00:00+08:00 北京时间(UTC+8) 明确指定时区

五、总结

  • new Date('YYYY-MM-DD') 会被当作 UTC 零点,导致北美等地区显示提前一天。

  • 推荐始终使用:

    • new Date('YYYY-MM-DDT00:00:00')
    • 或通过 moment / moment-timezone 明确时区解析。
  • 对于旧版 Element UI(1.4.x),建议:

    • 封装自定义 ElDatePickerTimezone
    • 或升级至 2.x 以上版本,避免死循环与时区 Bug。
相关推荐
whltaoin3 小时前
中秋赏月互动页面:用前端技术演绎传统节日之美
前端·javascript·html·css3·中秋主题前端
IT派同学3 小时前
TableWiz诞生记:一个被表格合并逼疯的程序员如何自救
前端·vue.js
西洼工作室5 小时前
CSS高效开发三大方向
前端·css
昔人'5 小时前
css`font-variant-numeric: tabular-nums` 用来控制数字的样式。
前端·css
铅笔侠_小龙虾6 小时前
动手实现简单Vue.js ,探索Vue原理
前端·javascript·vue.js
sniper_fandc8 小时前
Axios快速上手
vue.js·axios
哟哟耶耶8 小时前
Starting again-02
开发语言·前端·javascript
Apifox.8 小时前
Apifox 9 月更新| AI 生成接口测试用例、在线文档调试能力全面升级、内置更多 HTTP 状态码、支持将目录转换为模块
前端·人工智能·后端·http·ai·测试用例·postman
Kitasan Burakku8 小时前
Typescript return type
前端·javascript·typescript