一、背景
我們有一個較老的全球化項目,前端框架使用的是 Vue 2 + Element UI (v1.4.13)
。
在加拿大等北美地區(qū)測試時,發(fā)現(xiàn)一個非常詭異的問題: 當(dāng)選擇日期為 2025-10-06
時,實際顯示卻成了 2025-10-05
。
另外,在監(jiān)聽 onChange
事件時,還觸發(fā)了內(nèi)存溢出(死循環(huán)),導(dǎo)致瀏覽器卡死。 本文將結(jié)合實際調(diào)試過程,解釋問題根源,并提供完整解決方案。
二、問題一:日期顯示提前一天
1. Chrome 模擬時區(qū)復(fù)現(xiàn)
在 Chrome 控制臺中按下 ESC
,打開下方工具欄:
點擊左下角三點圖標(biāo),選擇 Sensors
:
在 Location
一欄中,選擇 “Mountain View”(美國山景城),即北美西海岸時區(qū):
接著在控制臺執(zhí)行以下代碼:
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)
可以看到,兩種寫法結(jié)果不同:第一個比預(yù)期早了一天。
2. 原因分析
這是由 JavaScript Date 構(gòu)造函數(shù)的解析規(guī)則 和 時區(qū)換算機制 共同導(dǎo)致的。
? new Date('2025-09-26')
- 當(dāng)只傳入
"YYYY-MM-DD"
時,JavaScript 會默認(rèn)按 UTC 零點解析: 即2025-09-26T00:00:00.000Z
。 - 若系統(tǒng)時區(qū)為北美 PDT(UTC-7),則要減去 7 小時:
2025-09-26 00:00:00 UTC = 2025-09-25 17:00:00 PDT
- 因此結(jié)果顯示為「9 月 25 日傍晚」,也就是你看到的「昨天」。
? new Date('2025-09-26T00:00:00')
- 當(dāng)字符串中包含
T
和時間部分時,JavaScript 會按 本地時區(qū) 解析(而不是當(dāng)作 UTC)。 - 因此該寫法得到正確的本地日期:
2025-09-26 00:00:00 PDT
。
3. 解決方法
方法一:傳入完整時間字符串(推薦)
// 推薦,ISO標(biāo)準(zhǔn)
new Date('2025-09-26T00:00:00') // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)
// Chrome 執(zhí)行正常,IE11 執(zhí)行變成 '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)指定時區(qū)的寫法
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
方法三:數(shù)字構(gòu)造方式
// 注意:月份從 0 開始,8 = 九月
new Date(2025, 8, 26) // Fri Sep 26 2025 00:00:00 GMT-0700 (Pacific Daylight Time)
? Element UI 2.x 之后改用數(shù)字構(gòu)造,因此天然避免了該問題。
三、問題二:onChange
死循環(huán)與內(nèi)存溢出
1. 復(fù)現(xiàn)代碼
安裝舊版本:
pnpm i element-ui@1.4.13
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("內(nèi)存溢出");
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
會被反復(fù)觸發(fā),造成內(nèi)存溢出,控制臺打印日志:內(nèi)存溢出。
2. 解決方案:封裝兼容組件
新建一個 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">
<!-- 默認(rèn)插槽轉(zhuǎn)發(fā) -->
<slot />
<!-- 作用域插槽轉(zhuǎn)發(fā) -->
<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}$/;
// 把傳進(jìn)來的字符串里所有 正則特殊字符(比如 . * + ? ^ $ { } ( ) | [ ] \)都加上反斜杠轉(zhuǎn)義。
// 舉例: '*' => '\*'
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 沒匹配上,再嘗試用一個寬松規(guī)則
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: 優(yōu)先匹配實際分隔符
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 是什么?
它們都是美國西海岸(包括加州、華盛頓州等地)的時區(qū):
- PDT = Pacific Daylight Time(太平洋夏令時間)
- PST = Pacific Standard Time(太平洋標(biāo)準(zhǔn)時間)
2. 兩者的區(qū)別
名稱 | 全稱 | 與 UTC 的時差 | 使用時間 | 舉例城市 |
---|---|---|---|---|
PST | Pacific Standard Time | UTC ? 8 小時 | 冬季使用(約 11 月初到次年 3 月中旬) | 洛杉磯、舊金山、山景城、西雅圖 |
PDT | Pacific Daylight Time | UTC ? 7 小時 | 夏季使用(約 3 月中旬到 11 月初) | 同上 |
?? 簡單記法:
夏天用 PDT(慢 7 小時),
冬天用 PST(慢 8 小時)。
舉個例子:
假設(shè) UTC 時間是:2025-09-26 00:00:00
- PDT(夏天)下:是
2025-09-25 17:00:00
- PST(冬天)下:是
2025-09-25 16:00:00
3. 為什么要區(qū)分?
美國實行 夏令時制度 (Daylight Saving Time, DST)
, 目的是讓人們在夏天“更晚天黑”,充分利用日照。 所以每年春天會:
- 把時間撥快一小時(進(jìn)入 PDT);
- 到秋天再撥回一小時(回到 PST)。
4.2 介紹下 ISO
1. 什么是 ISO 標(biāo)準(zhǔn)(ISO 8601)
ISO 是國際標(biāo)準(zhǔn)化組織(International Organization for Standardization)的簡稱。 ISO 8601 是它為“日期與時間的表示法”制定的國際標(biāo)準(zhǔn)。
? 目的:讓全世界的計算機系統(tǒng)用同一種方式理解時間,避免歧義。
比如:
- ???? 美國人習(xí)慣寫
09/26/2025
(月/日/年) - ???? 中國人習(xí)慣寫
2025-09-26
(年-月-日) - ???? 法國人可能寫
26/09/2025
(日/月/年)
這些格式人能分辨,但程序會混淆。 所以 ISO 8601 統(tǒng)一規(guī)定寫成:
YYYY-MM-DDTHH:mm:ssZ
2. 字符串里的 T
是什么意思?
在 ISO 8601 中,T
是一個固定分隔符,意思是:
「Time」的縮寫,用來分隔日期和時間部分。
舉個例子:
2025-09-26T00:00:00
可以理解為:
日期: 2025-09-26
時間: 00:00:00
中間的 T
就相當(dāng)于寫成 "2025-09-26 00:00:00"
的空格,只不過更標(biāo)準(zhǔn)化、機器可解析。
3. 常見 ISO 格式舉例
格式 | 含義 | 備注 |
---|---|---|
2025-09-26 | 僅日期(UTC 解析) | 容易產(chǎn)生時區(qū)偏移 ?? |
2025-09-26T00:00:00 | 本地時間零點 | ? 推薦用于 JS 本地時間 |
2025-09-26T00:00:00Z | UTC 時間零點(Z = Zulu = UTC) | ? 推薦用于跨時區(qū)傳輸 |
2025-09-26T00:00:00+08:00 | 北京時間(UTC+8) | 明確指定時區(qū) |
五、總結(jié)
new Date('YYYY-MM-DD')
會被當(dāng)作 UTC 零點,導(dǎo)致北美等地區(qū)顯示提前一天。推薦始終使用:
new Date('YYYY-MM-DDT00:00:00')
- 或通過
moment
/moment-timezone
明確時區(qū)解析。
對于舊版 Element UI(1.4.x),建議:
- 封裝自定義
ElDatePickerTimezone
; - 或升級至 2.x 以上版本,避免死循環(huán)與時區(qū) Bug。
- 封裝自定義