moment.js是用来做日期转换和格式化的一个很好的js库,能够实现任意日期字符串格式转换。但是啊,moment.js很重(≈4600行),如果想使用更轻便的库也可以尝试下Dayjs、miment等。

源码流程

  1. 初始化moment(),最终返回的是createLocal函数
  2. 初始化配置类,依次调用createLocal函数 -> createLocalOrUTC函数
  3. 完善配置信息并校验,涉及到的函数有prepareConfig函数 -> configFromStringAndFormat函数 -> configFromArray函数 -> checkOverflow函数
  4. 根据配置信息创建Moment对象
moment1

具体解析:

  1. 初始化moment,从源码可见返回的是hooks,而hooks返回的是hookCallback的调用,hookCallback设置的调用函数是createLocal,因此最终返回的是createLocal函数,如以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;(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;
}
setHookCallback(createLocal);
// 返回hooks,实际返回createLocal函数
return hooks;
})));
}
  1. 初始化配置类,调用createLocal函数,返回的是createLocalOrUTC函数的调用,此函数返回的根据配置信息创建的Moment对象moment2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createLocal(input, format, locale, strict) {
return createLocalOrUTC(input, format, locale, strict, false);
}

// 创建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);
}
  1. 完善配置信息并校验,依次经历:prepareConfig函数 -> configFromStringAndFormat函数 -> configFromArray函数 -> checkOverflow函数
moment3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 完善配置信息
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));//使用到了checkOverflow来检查输入
} 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;
}
return config;
}

// 通过字符串模板创建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);
}
}

getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
if (string.length > 0) {
getParsingFlags(config).unusedInput.push(string);
}

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;
config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem);

configFromArray(config);
checkOverflow(config);
}

function configFromArray(config) {
var i, date, input = [],
currentDate, expectedWeekday, yearToUse;
currentDate = currentDateArray(config);
// 将config._a数组中的元素暂存至input数组中用于调用createDate方法。
for (i = 0; i < 3 && config._a[i] == null; ++i) {
config._a[i] = input[i] = currentDate[i];
}
for (; i < 7; i++) {
config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
}
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;
}
}

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;
}
  1. 最后根据配置信息创建Moment对象:Moment构造函数
1
2
3
4
5
6
7
8
9
// 通过配置类创建Moment对象                  
function createFromConfig(config) {
var res = new Moment(checkOverflow(prepareConfig(config)));
if (res._nextDay) {
res.add(1, 'd');
res._nextDay = undefined;
}
return res;
}

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
;(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 初始化配置类
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 = {};
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;
}
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 = expandFormat(config._f, config._locale).match(formattingTokens) || [];

for (i = 0; i < tokens.length; i++) {
token = tokens[i];
parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
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);
}
addTimeToArrayFromToken(token, parsedInput, config);
}
else if (config._strict && !parsedInput) {
getParsingFlags(config).unusedTokens.push(token);
}
}

getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
if (string.length > 0) {
getParsingFlags(config).unusedInput.push(string);
}

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;
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);
for (i = 0; i < 3 && config._a[i] == null; ++i) {
config._a[i] = input[i] = currentDate[i];
}

for (; i < 7; i++) {
config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
}

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;
}
}

function createDate (y, m, d, h, M, s, ms) {
var date;
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);
}
if (updateInProgress === false) {
updateInProgress = true;
hooks.updateOffset(this);
updateInProgress = false;
}
}


// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……
// 省略……

hooks.version = '2.24.0';

// 设置hooks为createLocal
setHookCallback(createLocal);

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;
})));