广瀚云办公
55.99MB · 2025-10-09
大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @程序员大卫。
我们有一个较老的全球化项目,前端框架使用的是 Vue 2 + Element UI (v1.4.13)
。
在加拿大等北美地区测试时,发现一个非常诡异的问题:
当选择日期为 2025-10-06
时,实际显示却成了 2025-10-05
。
另外,在监听 onChange
事件时,还触发了内存溢出(死循环),导致浏览器卡死。
本文将结合实际调试过程,解释问题根源,并提供完整解决方案。
在 Chrome 控制台中按下 ESC
,打开下方工具栏:
点击左下角三点图标,选择 Sensors
:
在 Location
一栏中,选择 “Mountain View”(美国山景城),即北美西海岸时区:
接着在控制台执行以下代码:
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)
可以看到,两种写法结果不同:第一个比预期早了一天。
这是由 JavaScript Date 构造函数的解析规则 和 时区换算机制 共同导致的。
new Date('2025-09-26')
"YYYY-MM-DD"
时,JavaScript 会默认按 UTC 零点解析:
即 2025-09-26T00:00:00.000Z
。2025-09-26 00:00:00 UTC = 2025-09-25 17:00:00 PDT
new Date('2025-09-26T00:00:00')
T
和时间部分时,JavaScript 会按 本地时区 解析(而不是当作 UTC)。2025-09-26 00:00:00 PDT
。// 推荐,ISO标准
new Date('2025-09-26T00:00:00') // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)
// Chrome 执行正常,IE11 执行变成 'Invalid Date'
new Date('2025-09-26 00:00:00') // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)
moment
/ moment-timezone
1)使用 moment
的 toDate()
方法
import moment from "moment";
moment("2025-09-26").toDate(); // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)
2)指定时区的写法
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)格式化输出
moment("2025-09-26").format("YYYY-MM-DDTHH:mm:ss") // 2025-09-26T00:00:00
// 注意:月份从 0 开始,8 = 九月
new Date(2025, 8, 26) // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)
onChange
死循环与内存溢出安装旧版本:
pnpm i [email protected]
main.js
引入 ElementUI
:
// ...
import ElementUI from "element-ui";
import "element-ui/lib/theme-default/index.css";
Vue.use(ElementUI);
编写 App.vue
:
<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
会被反复触发,造成内存溢出,控制台打印日志:内存溢出。
新建一个 ElDatePickerTimezone.vue
,将所有日期组件替换为该封装版本。
App.vue
使用方式:
<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
组件封装如下:
<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>
它们都是美国西海岸(包括加州、华盛顿州等地)的时区:
名称 | 全称 | 与 UTC 的时差 | 使用时间 | 举例城市 |
---|---|---|---|---|
PST | Pacific Standard Time | UTC − 8 小时 | 冬季使用(约 11 月初到次年 3 月中旬) | 洛杉矶、旧金山、山景城、西雅图 |
PDT | Pacific Daylight Time | UTC − 7 小时 | 夏季使用(约 3 月中旬到 11 月初) | 同上 |
简单记法:
举个例子:
假设 UTC 时间是:2025-09-26 00:00:00
2025-09-25 17:00:00
2025-09-25 16:00:00
美国实行 夏令时制度 (Daylight Saving Time, DST)
,
目的是让人们在夏天“更晚天黑”,充分利用日照。
所以每年春天会:
ISO 是国际标准化组织(International Organization for Standardization)的简称。 ISO 8601 是它为“日期与时间的表示法”制定的国际标准。
比如:
09/26/2025
(月/日/年)2025-09-26
(年-月-日)26/09/2025
(日/月/年)这些格式人能分辨,但程序会混淆。 所以 ISO 8601 统一规定写成:
YYYY-MM-DDTHH:mm:ssZ
T
是什么意思?在 ISO 8601 中,T
是一个固定分隔符,意思是:
举个例子:
2025-09-26T00:00:00
可以理解为:
日期: 2025-09-26
时间: 00:00:00
中间的 T
就相当于写成 "2025-09-26 00:00:00"
的空格,只不过更标准化、机器可解析。
格式 | 含义 | 备注 |
---|---|---|
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
;