1 概述
- 最近被Safari瀏覽坑了兩次:new Date('2020-05-30 15:18:30.254') -> Invalid Date;
- 咨詢公司里的前端大佬,發現他們前端都用Moment.js做日期轉換;
- 為什么Moment.js能夠實現任意日期字符串格式轉換呢? 先上結論:底層使用new Date(年,月,日,時,分,秒,毫秒)函數,這個函數基本上所有瀏覽器都實現了。
注:Moment.js很重(源碼為4600行左右),所以有很多替代方案的,如:Dayjs、miment等,甚至根據瀏覽器的兼容情況自行寫個輕量級的庫也是可行的。
2 使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="http://cdn.staticfile.org/moment.js/2.24.0/moment.js"></script>
<title>Learn MomentJs</title>
</head>
<body>
<script>
var moment1 = moment("2020-05-30 14:08:35","YYYY-MM-DD HH:mm:ss");
var date = moment1._d;
console.log(date);
</script>
</body>
</html>
3 源碼跟蹤
- 1 初始化moment():返回createLocal函數;
- 2 初始化配置類:調用createLocal函數 -> createLocalOrUTC函數 ;
- 3 完善配置信息並校驗:prepareConfig函數 -> configFromStringAndFormat函數 -> configFromArray函數 -> checkOverflow函數;
- 4 根據配置信息創建Moment對象:Moment構造函數。
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
global.moment = factory()
}(this, (function () { 'use strict';
var hookCallback;
function hooks () {
return hookCallback.apply(null, arguments);
}
function setHookCallback (callback) {
hookCallback = callback;
}
// 2 初始化配置類
// 2.1 ex: input="2020-05-30 14:08:35",format="YYYY-MM-DD HH:mm:ss",locale=null,strict=null
function createLocal (input, format, locale, strict) {
return createLocalOrUTC(input, format, locale, strict, false);
}
// 2.2 創建Local或者UTC Moment對象
function createLocalOrUTC (input, format, locale, strict, isUTC) {
var c = {};
// 檢驗input字符串
if ((isObject(input) && isObjectEmpty(input)) ||
(isArray(input) && input.length === 0)) {
input = undefined;
}
// 配置初始化
c._useUTC = c._isUTC = isUTC;
c._l = locale;
c._i = input;
c._f = format;
c._strict = strict;
return createFromConfig(c);
}
// 4 通過配置類創建Moment對象
function createFromConfig (config) {
var res = new Moment(checkOverflow(prepareConfig(config)));
if (res._nextDay) {
res.add(1, 'd');
res._nextDay = undefined;
}
return res;
}
// 3 完善配置信息
function prepareConfig (config) {
var input = config._i,
format = config._f;
config._locale = config._locale || getLocale(config._l);
if (input === null || (format === undefined && input === '')) {
return createInvalid({nullInput: true});
}
if (typeof input === 'string') {
config._i = input = config._locale.preparse(input);
}
// 支持Moment對象、日期對象、數組對象、字符串格式、配置對象格式
if (isMoment(input)) {
return new Moment(checkOverflow(input));
} else if (isDate(input)) {
config._d = input;
} else if (isArray(format)) {
configFromStringAndArray(config);
} else if (format) {
// 以此為例
configFromStringAndFormat(config);
} else {
configFromInput(config);
}
if (!isValid(config)) {
config._d = null; // _d為日期對象,下面講解。
}
return config;
}
// 3.1 通過字符串模板創建Moment對象中的日期對象(_d)
function configFromStringAndFormat(config) {
// 創建Date對象用的數組,如:[年,月,日,時,分,秒]
config._a = [];
getParsingFlags(config).empty = true;
var string = '' + config._i,
i, parsedInput, tokens, token, skipped,
stringLength = string.length,
totalParsedInputLength = 0;
// tokens=['YYYY','-','MM','-','DD',' ','HH',':','mm',':','ss']
tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];
for (i = 0; i < tokens.length; i++) {
token = tokens[i]; // 首次為YYYY
parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; // 首次為2020
if (parsedInput) {
skipped = string.substr(0, string.indexOf(parsedInput));
if (skipped.length > 0) {
getParsingFlags(config).unusedInput.push(skipped);
}
string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
totalParsedInputLength += parsedInput.length;
}
if (formatTokenFunctions[token]) {
if (parsedInput) {
getParsingFlags(config).empty = false;
}
else {
getParsingFlags(config).unusedTokens.push(token);
}
// 將parsedInput添加到config._a數組中
addTimeToArrayFromToken(token, parsedInput, config);
}
else if (config._strict && !parsedInput) {
getParsingFlags(config).unusedTokens.push(token);
}
}
// add remaining unparsed input length to the string
getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
if (string.length > 0) {
getParsingFlags(config).unusedInput.push(string);
}
// clear _12h flag if hour is <= 12
if (config._a[HOUR] <= 12 &&
getParsingFlags(config).bigHour === true &&
config._a[HOUR] > 0) {
getParsingFlags(config).bigHour = undefined;
}
getParsingFlags(config).parsedDateParts = config._a.slice(0);
getParsingFlags(config).meridiem = config._meridiem;
// handle meridiem
config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem);
configFromArray(config);
checkOverflow(config);
}
// 3.2 當config._a日期相關數組完善后
function configFromArray (config) {
var i, date, input = [], currentDate, expectedWeekday, yearToUse;
// 年月日
currentDate = currentDateArray(config);
// ...
// 簡單描述:將config._a數組中的元素暫存至input數組中用於調用createDate方法。
// Default to current date.
// * if no year, month, day of month are given, default to today
// * if day of month is given, default month and year
// * if month is given, default only year
// * if year is given, don't default anything
for (i = 0; i < 3 && config._a[i] == null; ++i) {
config._a[i] = input[i] = currentDate[i];
}
// Zero out whatever was not defaulted, including time
for (; i < 7; i++) {
config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
}
// Check for 24:00:00.000
if (config._a[HOUR] === 24 &&
config._a[MINUTE] === 0 &&
config._a[SECOND] === 0 &&
config._a[MILLISECOND] === 0) {
config._nextDay = true;
config._a[HOUR] = 0;
}
// 實際創建日期的方法,前面已經把
config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input);
expectedWeekday = config._useUTC ? config._d.getUTCDay() : config._d.getDay();
if (config._tzm != null) {
config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
}
if (config._nextDay) {
config._a[HOUR] = 24;
}
if (config._w && typeof config._w.d !== 'undefined' && config._w.d !== expectedWeekday) {
getParsingFlags(config).weekdayMismatch = true;
}
}
// ex: y=2020,m=4,d=30,h=14,M=8,s=35,ms=0
function createDate (y, m, d, h, M, s, ms) {
var date;
// the date constructor remaps years 0-99 to 1900-1999
if (y < 100 && y >= 0) {
date = new Date(y + 400, m, d, h, M, s, ms);
if (isFinite(date.getFullYear())) {
date.setFullYear(y);
}
} else {
// 最終調用通用的日期創建方法(這個方法所有瀏覽器都實現了)
date = new Date(y, m, d, h, M, s, ms);
}
return date;
}
// 4 創建Moment對象
function Moment(config) {
copyConfig(this, config);
this._d = new Date(config._d != null ? config._d.getTime() : NaN);
if (!this.isValid()) {
this._d = new Date(NaN);
}
// Prevent infinite loop in case updateOffset creates new moment
// objects.
if (updateInProgress === false) {
updateInProgress = true;
hooks.updateOffset(this);
updateInProgress = false;
}
}
// 中間省略億點細節
hooks.version = '2.24.0';
// 設置hooks為createLocal
setHookCallback(createLocal);
// currently HTML5 input type only supports 24-hour formats
hooks.HTML5_FMT = {
DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // <input type="datetime-local" />
DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // <input type="datetime-local" step="1" />
DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // <input type="datetime-local" step="0.001" />
DATE: 'YYYY-MM-DD', // <input type="date" />
TIME: 'HH:mm', // <input type="time" />
TIME_SECONDS: 'HH:mm:ss', // <input type="time" step="1" />
TIME_MS: 'HH:mm:ss.SSS', // <input type="time" step="0.001" />
WEEK: 'GGGG-[W]WW', // <input type="week" />
MONTH: 'YYYY-MM' // <input type="month" />
};
// 1. 返回hooks,實際返回createLocal函數
return hooks;
})));
