0%

dayjs源码解析(二):Dayjs 类

接上篇 —— dayjs 源码解析(一):概念、locale、constant、utils —— 继续解析 dayjs 的源码。

本篇主要分析 dayjs 源码中最核心的部分,也就是 src/index.js 中的 Dayjs 类。

结构

src/index.js 文件的结构还是很清晰的,代码的骨架如下所示,按如下几步来构造:

  1. 导入 constantlocaleutils
  2. 定义全局 locale
  3. 定义实例化 Dayjs 类的方法
  4. 完善 Utils 工具函数
  5. 定义 Dayjs
  6. 丰富 Dayjsprototype
  7. 定义 Dayjs 的 静态方法
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
// 导入常量、locale 和 工具函数
import * as C from './constant.js';
import en from './locale/en.js';
import U from './utils.js';

// 全局 locale 定义
let L = 'en';
const Ls = {};
Ls[L] = en;

// 实例化 Dayjs 类的方法
const dayjs = function (date, c) {};

// 把几个工具方法同样加到 Utils 工具包中
const isDayjs = (d) => d instanceof Dayjs;
const parseLocale = (preset, object, isLocal) => {};
const wrapper = (date, instance) => {};
const Utils = U;
Utils.l = parseLocale;
Utils.i = isDayjs;
Utils.w = wrapper;

const parseDate = (cfg) => {};

// Dayjs 类
class Dayjs {
constructor(cfg) {}
parse(cfg) {}
init() {}
$utils() {}
isValid() {}
isSame(that, units) {}
isAfter(that, units) {}
isBefore(that, units) {}
$g(input, get, set) {}
unix() {}
valueOf() {}
startOf(units, startOf) {}
endOf(arg) {}
$set(units, int) {}
set(string, int) {}
get(unit) {}
add(number, units) {}
subtract(number, string) {}
format(formatStr) {}
utcOffset() {}
diff(input, units, float) {}
daysInMonth() {}
$locale() {}
locale(preset, object) {}
clone() {}
toDate() {}
toJSON() {}
toISOString() {}
toString() {}
}

// 设置 dayjs 和 Dayjs 的原型链,在 prototype 上设置各个单位的取值和设值函数
const proto = Dayjs.prototype;
dayjs.prototype = proto;
[
['$ms', C.MS],
['$s', C.S],
['$m', C.MIN],
['$H', C.H],
['$W', C.D],
['$M', C.M],
['$y', C.Y],
['$D', C.DATE],
].forEach((g) => {
proto[g[1]] = function (input) {};
});

// 下面的方法都是静态方法,挂在 Dayjs 类上
dayjs.extend = (plugin, option) => {};
dayjs.locale = parseLocale;
dayjs.isDayjs = isDayjs;
dayjs.unix = (timestamp) => {};

dayjs.en = Ls[L];
dayjs.Ls = Ls;
dayjs.p = {};
export default dayjs;

但是这里有两个不理解的地方@iamkun,猜想可能是为了缩减代码体积:

  1. 为什么要把 constant.jsutils.js 的导出缩写成 CU,感觉增大了理解成本。
1
2
3
4
// 先导入成 U
import U from './utils.js';
// 使用之前改成 Utils
const Utils = U;
  1. 为什么在 utils.js 中要把 Utils 中的方法缩写,需要专门去看才能知道简写映射的是哪个方法。
1
2
3
4
5
6
7
8
9
10
11
12
// Utils 对象中的映射
const Utils = {
s: padStart,
z: padZoneStr,
m: monthDiff,
a: absFloor,
p: prettyUnit,
u: isUndefined,
l: parseLocale;
i: isDayjs;
w: wrapper;
}

代码解析

下面正式开始分析代码。

locale 相关

全局定义

首先默认导入了 locale/en.js 英文的 locale,然后使用 L 存放当前用的 locale 名字,使用 Ls(locale Storage)存放 locale 对象

1
2
3
4
5
import en from './locale/en.js';

let L = 'en'; // 全局 locale
const Ls = {}; // 全局的已加载 locale 映射
Ls[L] = en;

工具补充

定义了一个工具方法 parseLocale。这个方法很有意思,分为以下几种情况来处理:

  • 如果不带参数使用,返回当前使用的 locale 键;
  • 如果带参数使用,
    • 如果 preset 参数是个对象,就解析对象,给 Ls 中添加或修改对应键值对,并把当前使用的 L 设为解析出来的 name;
    • 如果 preset 参数是个字符串,
      • 如果 object 参数不存在,就把 L 设为 preset;
      • 如果 object 参数存在,就在 Ls 中添加或修改对应键值对,再把 L 设为 preset
  • 最后都返回当前使用的 locale 名,也就是 L

再把定义好的 parseLocale 方法补充到 Utils 中。

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
/**
* @description: 给全局locale映射添加一个键值对或修改已有键值对
* @param {String|Object} preset 预设的local对象或名称字符串
* @param {Object} object locale对象
* @param {Boolean} isLocal 是否为本地的 locale
* @return {String} 返回键名
*/
const parseLocale = (preset, object, isLocal) => {
let l;
// 不带参数时,返回当前使用的locale键
if (!preset) return L;
// 不管preset是对象还是字符串,都去映射键值对
if (typeof preset === 'string') {
if (Ls[preset]) {
l = preset;
}
if (object) {
Ls[preset] = object;
l = preset;
}
} else {
const { name } = preset;
Ls[name] = preset;
l = name;
}
// 如果不用本地的locale,就修改L,最后返回l
if (!isLocal && l) L = l;
return l || (!isLocal && L);
};

Utils.l = parseLocale;

相关方法

Dayjs 类中,关于 locale 的方法就是下面两个,实例私有方法 $locale 是用来返回当前使用的 locale 对象;实例方法 locale 本质上就是调用了 parseLocale 方法,但是最后返回的是新的改变了 localeDayjs 实例

注意:在 dayjs 中,很多操作都使用到了 clone() 方法来返回Dayjs 实例,这也是这个库的优点之一。

最后同样把 parseLocale 方法补充到 Dayjs 类 的静态方法中。

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
class Dayjs {
constructor(cfg) {
this.$L = parseLocale(cfg.locale, null, true); // $L 存放当前 locale
}

/**
* @description: 当前使用的 locale 对象
* @return {Object} locale 对象
*/
$locale() {
return Ls[this.$L];
}

/**
* @description: 无参数时返回当前使用的locale对象,有参数时改变并设置locale对象
* @param {String} preset locale 对象名
* @param {Object} object locale 对象
* @return {Object|Dayjs}
*/
locale(preset, object) {
// 无参数时返回当前使用的locale对象
if (!preset) return this.$L;
const that = this.clone();
const nextLocaleName = parseLocale(preset, object, true);
// 有参数时改变 locale(并设置 locale 对象),后返回新 locale 的 Dayjs 实例
if (nextLocaleName) that.$L = nextLocaleName;
return that;
}
}

// ......
// ......
// ......

dayjs.locale = parseLocale;

补充 Utils

上一节和前文中已经分析了一些 Util 工具,这里把它补充完整:

注意:这些工具方法没有统一定义在 utils.js 文件的原因是用到了 index.js 作用域中的一些变量。

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
import U from './utils.js';
/**
* @description: 判断对象是否为Dayjs的实例
* @param {object} d 对象
* @return {Boolean}
*/
const isDayjs = (d) => d instanceof Dayjs; // eslint-disable-line no-use-before-define

// 已经解析
const parseLocale = (preset, object, isLocal) => {};

/**
* @description: 实例化Dayjs的方法,如果参数已经是Dayjs的实例,就直接返回
* @param {Date|Dayjs} date Date或者Dayjs对象
* @param {Object} c Date或者Dayjs对象
* @return {Dayjs} 返回一个Dayjs实例
*/
const dayjs = function (date, c) {
if (isDayjs(date)) {
return date.clone();
}
const cfg = typeof c === 'object' ? c : {};
cfg.date = date;
cfg.args = arguments;
// cfg: {date, args: arguments}
return new Dayjs(cfg);
};

/**
* @description: 封装器,根据Date对象和Dayjs实例封装出一个新实例
* @param {Date} date Date对象
* @param {Dayjs} instance 已存在的Dayjs实例
* @return {Dayjs}
*/
const wrapper = (date, instance) =>
dayjs(date, {
locale: instance.$L,
utc: instance.$u,
x: instance.$x,
$offset: instance.$offset,
});

// 把上面写的几个方法同样加到 Utils 工具包中
const Utils = U;
Utils.l = parseLocale;
Utils.i = isDayjs;
Utils.w = wrapper;

这里需要特别关注的是 wrapper 方法,在 Dayjs 类中大量应用了该方法,其实是通过 date原实例封装了一个新实例,新实例和原实例的主要区别就是关联的时间不同。

Dayjs 类

Dayjs 类是整个 dayjs 库的核心,可以给它定义的实例方法分个类,也可以去查看官网的文档分类。

  • 初始化: parseinit
  • 取赋值: getset
  • 操作: addsubtractstartOfendOfutcOffset
  • 显示: formattoDatetoJSONtoISOStringtoString
  • 查询: isValidisSameisAfterisBeforedaysInMonthdiffunixvalueOf

解析都写在了代码的注释里:

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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
class Dayjs {
constructor(cfg) {
this.$L = parseLocale(cfg.locale, null, true); // $L 存放当前 locale
this.parse(cfg); // for plugin
}

/**
* @description: 解析cfg
* @param {Object} cfg config 配置对象
*/
parse(cfg) {
this.$d = parseDate(cfg); // $d 存放 Date 对象
this.$x = cfg.x || {}; // $x 存放 {$timezone, $localOffset} 时区有关信息
this.init();
}

/**
* @description: 初始化内部变量
*/
init() {
const { $d } = this;
this.$y = $d.getFullYear(); // 2020
this.$M = $d.getMonth(); // 11
this.$D = $d.getDate(); // 8
this.$W = $d.getDay(); // 2
this.$H = $d.getHours(); // 7
this.$m = $d.getMinutes(); // 6
this.$s = $d.getSeconds(); // 1
this.$ms = $d.getMilliseconds(); // 425
}

/**
* @description: 返回完备的工具库
* @return {Object} Utils对象
* @private
*/
$utils() {
return Utils;
}

/**
* @description: 返回 Date 对象是否合规
* @return {Boolean}
*/
isValid() {
return !(this.$d.toString() === C.INVALID_DATE_STRING);
}

/**
* @description: 本实例是否和that提供的日期时间相同
* @param {Dayjs|String} that 另一个Dayjs实例或者时间字符串
* @param {String} units 单位
* @return {Boolean} 只要在单位的时间段内,就算相同
*/
isSame(that, units) {
const other = dayjs(that);
// 用给定单位内本实例的 start 和 end 来夹逼 that 实例
return this.startOf(units) <= other && other <= this.endOf(units);
}

/**
* @description: 给定单位下,本实例的时间是否晚于that提供的时间
* @param {Dayjs|String} that 另一个Dayjs实例或者时间字符串
* @param {String} units 单位
* @return {Boolean}
*/
isAfter(that, units) {
return dayjs(that) < this.startOf(units);
}

/**
* @description: 给定单位下,本实例的时间是否早于that提供的时间
* @param {Dayjs|String} that 另一个Dayjs实例或者时间字符串
* @param {String} units 单位
* @return {Boolean}
*/
isBefore(that, units) {
return this.endOf(units) < dayjs(that);
}

/**
* @description: 获取或设置单位
* @param {Number} input 要设置的值
* @return {Number|Dayjs} 有input时,设置值并返回实例;无input时,返回对应单位的数值
*/
$g(input, get, set) {
// 无参数就get,有参数就set
if (Utils.u(input)) return this[get];
return this.set(set, input);
}

/**
* @description: 返回秒做单位的 Unix 时间戳 (10 位数字)
* @return {Number} 十位时间戳
*/
unix() {
return Math.floor(this.valueOf() / 1000);
}

/**
* @description: 根据实例关联的Date对象返回13位时间戳 ms
* @return {Number} 时间戳 eg.1607404331806
*/
valueOf() {
// timezone(hour) * 60 * 60 * 1000 => ms
return this.$d.getTime();
}

/**
* @description: 根据单位将实例设置到一个时间段的开始
* @param {String} units 单位
* @param {Boolean} startOf 标志,true:startOf, false: endOf
* @return {Dayjs} 返回新的 Dayjs 实例,cfg与原实例相同
*/
startOf(units, startOf) {
// 用于切换 startOf 和 endOf
const isStartOf = !Utils.u(startOf) ? startOf : true;
const unit = Utils.p(units);
/**
* @description: 实例工厂函数,根据月日(参数)和年份(实例)创建新的Dayjs实例
* @param {Number} d 日
* @param {Number} m 月
* @return {Dayjs} 返回新实例,如果 isStartOf 为 false,返回当天的 endOf
*/
const instanceFactory = (d, m) => {
const ins = Utils.w(
this.$u ? Date.UTC(this.$y, m, d) : new Date(this.$y, m, d),
this
);
return isStartOf ? ins : ins.endOf(C.D);
};
/**
* @description: 根据传入的method来返回Dayjs新实例
* @param {String} method 例如 setHours、setUTCHours
* @param {Number} slice 截断参数数组
* @return {Dayjs} 返回新实例,
*/
const instanceFactorySet = (method, slice) => {
// 传递给apply的参数数组
const argumentStart = [0, 0, 0, 0];
const argumentEnd = [23, 59, 59, 999];
return Utils.w(
this.toDate()[method].apply(
this.toDate('s'),
(isStartOf ? argumentStart : argumentEnd).slice(slice)
),
this
);
};

// 获取day、month、date
const { $W, $M, $D } = this;
const utcPad = `set${this.$u ? 'UTC' : ''}`;
switch (unit) {
// year,返回1月1日或者11月31日的Dayjs实例
case C.Y:
return isStartOf ? instanceFactory(1, 0) : instanceFactory(31, 11);
// month,返回{month+1}月1日或本月最后一天
case C.M:
return isStartOf ? instanceFactory(1, $M) : instanceFactory(0, $M + 1); // 0, $M + 1可获得上月的最后一天,避免29 30 31的区别
// week 返回周的第一天或最后一天
case C.W: {
const weekStart = this.$locale().weekStart || 0;
const gap = ($W < weekStart ? $W + 7 : $W) - weekStart;
return instanceFactory(isStartOf ? $D - gap : $D + (6 - gap), $M);
}
// day date 返回一天的第一个小时或者最后一个小时
case C.D:
case C.DATE:
return instanceFactorySet(`${utcPad}Hours`, 0);
// hour 返回一小时的第一分钟或最后一分钟
case C.H:
return instanceFactorySet(`${utcPad}Minutes`, 1);
// minute 返回一分钟的第一秒或最后一秒
case C.MIN:
return instanceFactorySet(`${utcPad}Seconds`, 2);
// second 返回一秒钟的第一毫秒或最后一毫秒
case C.S:
return instanceFactorySet(`${utcPad}Milliseconds`, 3);
// 默认直接返回本实例
default:
return this.clone();
}
}

/**
* @description: 根据单位将实例设置到一个时间段的结束
* @param {String} units 单位
* @return {Dayjs} 返回新的 Dayjs 实例,cfg与原实例相同
*/
endOf(arg) {
return this.startOf(arg, false);
}

/**
* @description: 私有 setter,两个参数分别是要更新的单位和数值,调用后会返回一个修改后的新实例。
* @param {String} units 单位
* @param {Number} int 值
* @return {Dayjs} 返回修改后的实例
* @private
*/
$set(units, int) {
// 根据 units 处理函数名
const unit = Utils.p(units);
const utcPad = `set${this.$u ? 'UTC' : ''}`;
const name = {
[C.D]: `${utcPad}Date`,
[C.DATE]: `${utcPad}Date`,
[C.M]: `${utcPad}Month`,
[C.Y]: `${utcPad}FullYear`,
[C.H]: `${utcPad}Hours`,
[C.MIN]: `${utcPad}Minutes`,
[C.S]: `${utcPad}Seconds`,
[C.MS]: `${utcPad}Milliseconds`,
}[unit];
const arg = unit === C.D ? this.$D + (int - this.$W) : int;

// 把 $d,也就是关联的 Date 对象设为对应 int
if (unit === C.M || unit === C.Y) {
// clone is for badMutable plugin
const date = this.clone().set(C.DATE, 1);
date.$d[name](arg);
date.init();
this.$d = date.set(C.DATE, Math.min(this.$D, date.daysInMonth())).$d;
} else if (name) this.$d[name](arg);

// 重新初始化
this.init();
return this;
}

/**
* @description: 通用的 setter,两个参数分别是要更新的单位和数值,调用后会返回一个修改后的新实例。
* @param {String} string 单位
* @param {Number} int 值
* @return {Dayjs} 返回修改后的实例
*/
set(string, int) {
return this.clone().$set(string, int);
}

/**
* @description: 从实例中获取相应信息的通用 getter。
* @param {String} unit
* @return {Number} 返回对应单位的值
*/
get(unit) {
return this[Utils.p(unit)]();
}

/**
* @description: 根据单位和值,给当前关联的Date对象增加时间
* @param {Number} number 增加的数值
* @param {String} units 单位
* @return {Dayjs} 返回新的Dayjs对象
*/
add(number, units) {
number = Number(number); // eslint-disable-line no-param-reassign
const unit = Utils.p(units);
/**
* @description: 专门给 week 和 date 用的增加时间的工具函数
* @param {Number} n 基础单位的天数 例如 week 是 7
* @return {Dayjs} 返回新的 Dayjs 对象
*/
const instanceFactorySet = (n) => {
const d = dayjs(this);
return Utils.w(d.date(d.date() + Math.round(n * number)), this);
};
// 月
if (unit === C.M) {
return this.set(C.M, this.$M + number);
}
// 年
if (unit === C.Y) {
return this.set(C.Y, this.$y + number);
}
// 日
if (unit === C.D) {
return instanceFactorySet(1);
}
// 周
if (unit === C.W) {
return instanceFactorySet(7);
}
const step =
{
[C.MIN]: C.MILLISECONDS_A_MINUTE, // 分钟
[C.H]: C.MILLISECONDS_A_HOUR, // 小时
[C.S]: C.MILLISECONDS_A_SECOND, // 秒
}[unit] || 1; // ms

const nextTimeStamp = this.$d.getTime() + number * step;
return Utils.w(nextTimeStamp, this);
}

/**
* @description: 根据单位和值,给当前关联的Date对象减少时间
* @param {Number} number 减少的数值
* @param {String} units 单位
* @return {Dayjs} 返回新的Dayjs对象
*/
subtract(number, string) {
return this.add(number * -1, string);
}

/**
* @description: 根据模板返回对应格式的时间字符串
* @param {String} formatStr 模板字符串
* @return {String} 对应格式的时间字符串
*/
format(formatStr) {
if (!this.isValid()) return C.INVALID_DATE_STRING;

// 整理出所需的各种变量和方法
const str = formatStr || C.FORMAT_DEFAULT;
const zoneStr = Utils.z(this);
const locale = this.$locale();
const { $H, $m, $M } = this;
const { weekdays, months, meridiem } = locale;

/**
* @description: 返回对应缩写的字符串,可自适应
* @param {Array} arr 几月或者周几的缩写数组 ["1月", "2月", "3月"...]
* @param {Number} index 索引
* @param {Array} full 几月或者周几的非缩写数组 ["一月", "二月", "三月"...]
* @param {Number} length 返回结果的字符数
* @return {String} 对应缩写的字符串
*/
const getShort = (arr, index, full, length) =>
(arr && (arr[index] || arr(this, str))) || full[index].substr(0, length);

/**
* @description: 获取固定长度的小时表示
* @param {Number} num 小时的长度
* @return {String} 固定长度的小时表示
*/
const get$H = (num) => Utils.s($H % 12 || 12, num, '0');

/**
* @description: 根据时和分区分时间段(上午、下午)
* @param {Number} hour 时
* @param {Number} minute 分
* @param {Boolean} isLowercase 是否小写,默认false
* @return {String} 时间段 例如 AM
*/
const meridiemFunc =
meridiem ||
((hour, minute, isLowercase) => {
const m = hour < 12 ? 'AM' : 'PM';
return isLowercase ? m.toLowerCase() : m;
});

// 不同的模板对应的格式转换
const matches = {
YY: String(this.$y).slice(-2),
YYYY: this.$y,
M: $M + 1,
MM: Utils.s($M + 1, 2, '0'),
MMM: getShort(locale.monthsShort, $M, months, 3),
MMMM: getShort(months, $M),
D: this.$D,
DD: Utils.s(this.$D, 2, '0'),
d: String(this.$W),
dd: getShort(locale.weekdaysMin, this.$W, weekdays, 2),
ddd: getShort(locale.weekdaysShort, this.$W, weekdays, 3),
dddd: weekdays[this.$W],
H: String($H),
HH: Utils.s($H, 2, '0'),
h: get$H(1),
hh: get$H(2),
a: meridiemFunc($H, $m, true),
A: meridiemFunc($H, $m, false),
m: String($m),
mm: Utils.s($m, 2, '0'),
s: String(this.$s),
ss: Utils.s(this.$s, 2, '0'),
SSS: Utils.s(this.$ms, 3, '0'),
Z: zoneStr, // 'ZZ' logic below
};

// /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g
// 最后进行了整个字符串模板的内容替换
return str.replace(
C.REGEX_FORMAT,
(match, $1) => $1 || matches[match] || zoneStr.replace(':', '')
); // 'ZZ'
}

/**
* @description: 返回分钟级的时区偏移量 (精度15分钟)
* @return {Number} 时区偏移量 分钟
*/
utcOffset() {
// 由于 FF24 bug,我们把时区偏移近似到 15 分钟
// https://github.com/moment/moment/pull/1871
return -Math.round(this.$d.getTimezoneOffset() / 15) * 15;
}

/**
* @description: 返回指定单位下两个日期时间之间的差异。
* @param {String} input 输入的时间
* @param {String} units 单位
* @param {Boolean} float 是否需要取整
* @return {Number} 返回对应单位下的时间差
*/
diff(input, units, float) {
const unit = Utils.p(units);
// input封装成实例
const that = dayjs(input);
const zoneDelta =
(that.utcOffset() - this.utcOffset()) * C.MILLISECONDS_A_MINUTE;
// 毫秒差
const diff = this - that;
// 月份差
let result = Utils.m(this, that);

// 区分不同的单位
result =
{
[C.Y]: result / 12,
[C.M]: result,
[C.Q]: result / 3,
[C.W]: (diff - zoneDelta) / C.MILLISECONDS_A_WEEK,
[C.D]: (diff - zoneDelta) / C.MILLISECONDS_A_DAY,
[C.H]: diff / C.MILLISECONDS_A_HOUR,
[C.MIN]: diff / C.MILLISECONDS_A_MINUTE,
[C.S]: diff / C.MILLISECONDS_A_SECOND,
}[unit] || diff; // milliseconds

// 是否取整
return float ? result : Utils.a(result);
}

/**
* @description: 返回实例所在月的总天数
* @return {Number} 天数
*/
daysInMonth() {
return this.endOf(C.M).$D;
}

/**
* @description: 当前使用的 locale 对象
* @return {Object} locale 对象
*/
$locale() {
return Ls[this.$L];
}

/**
* @description: 无参数时返回当前使用的locale对象,有参数时改变并设置locale对象
* @param {String} preset locale 对象名
* @param {Object} object locale 对象
* @return {Object|Dayjs}
*/
locale(preset, object) {
// 无参数时返回当前使用的locale对象
if (!preset) return this.$L;
const that = this.clone();
const nextLocaleName = parseLocale(preset, object, true);
// 有参数时改变 locale(并设置 locale 对象),后返回新 locale 的 Dayjs 实例
if (nextLocaleName) that.$L = nextLocaleName;
return that;
}

/**
* @description: 克隆本实例并返回
* @return {Dayjs} 返回新的 Dayjs 实例
*/
clone() {
return Utils.w(this.$d, this);
}

/**
* @description: 返回实例对应的 Date 对象
* @return {Date} Date 对象
*/
toDate() {
return new Date(this.valueOf());
}

/**
* @description: 返回ISO格式的字符串(YYYY-MM-DDTHH:mm:ss.sssZ),不太懂
* @return {String} UTC(协调世界时)例如 2020-12-09T05:14:04.670Z
*/
toJSON() {
return this.isValid() ? this.toISOString() : null;
}

/**
* @description: 返回ISO格式的字符串(YYYY-MM-DDTHH:mm:ss.sssZ)
* @return {String} UTC(协调世界时)例如 2020-12-09T05:14:04.670Z
*/
toISOString() {
// ie 8 return
// new Dayjs(this.valueOf() + this.$d.getTimezoneOffset() * 60000)
// .format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')
return this.$d.toISOString();
}

/**
* @description: 返回一个字符串
* @return {String} 例如"Wed, 09 Dec 2020 05:16:39 GMT"
*/
toString() {
return this.$d.toUTCString();
}
}

原型链

正常来说,定义在实例中的方法就应该在原型链上,但是有几个与时间有关的 setter/getter 方法比较相似,所以就单独拿出了原型链写在了上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const proto = Dayjs.prototype;
dayjs.prototype = proto;

// 在prototype上设置各个单位的取值和设值函数
[
['$ms', C.MS], // Dayjs.prototype.millisecond
['$s', C.S], // Dayjs.prototype.second
['$m', C.MIN], // Dayjs.prototype.minute
['$H', C.H], // Dayjs.prototype.hour
['$W', C.D], // Dayjs.prototype.day
['$M', C.M], // Dayjs.prototype.month
['$y', C.Y], // Dayjs.prototype.year
['$D', C.DATE], // Dayjs.prototype.date
].forEach((g) => {
proto[g[1]] = function (input) {
// g[0]是实例上的值,g[1]是字符串(例如date)
return this.$g(input, g[0], g[1]);
};
});

这几个方法全都是不传参数就 getter,传参数就 setter

静态属性

还有一些方法和属性挂在了 dayjs 函数对象上,最核心的是加载插件和加载 locale 的方法,几个方法的用法都能在官方文档中找到。

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
// 下面的方法都是静态方法,挂在dayjs 函数对象上
/**
* @description: 挂载插件
* @param {*} plugin 插件
* @param {*} option 插件选项
* @return {dayjs function} 返回 dayjs 函数对象
*/
dayjs.extend = (plugin, option) => {
// 同一个插件只挂载一次
if (!plugin.$i) {
plugin(option, Dayjs, dayjs); //挂载
plugin.$i = true;
}
return dayjs;
};

dayjs.locale = parseLocale;

dayjs.isDayjs = isDayjs;

/**
* @description: 解析传入的一个秒做单位的 Unix 时间戳 (10 位数字),返回一个 Dayjs 实例
* @param {Number} timestamp 10位的时间戳
* @return {Dayjs} 返回一个 Dayjs 实例
*/
dayjs.unix = (timestamp) => dayjs(timestamp * 1e3);

dayjs.en = Ls[L];
dayjs.Ls = Ls;
dayjs.p = {};

如果看到这里对 dayjs 函数对象Dayjs 类原型的关系很懵,可以参考下图,最后形成的关系如下图所示:

关系

总结

如果不看插件部分的话,其实 dayjs 库的核心已经解析完成,看一下默认生成 dayjs 实例长什么样子:

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
{
$D: 12,
$H: 16,
$L: "en",
$M: 11,
$W: 6,
$d: Tue Dec 12 2020 16:33:49 GMT+0800 (中国标准时间) {},
$m: 33,
$ms: 0,
$s: 49,
$x: {},
$y: 2020,
__proto__: {
date: ƒ (input),
day: ƒ (input),
hour: ƒ (input),
millisecond: ƒ (input),
minute: ƒ (input),
month: ƒ (input),
second: ƒ (input),
year: ƒ (input),
$g: ƒ $g(input, get, set),
$locale: ƒ $locale(),
$set: ƒ $set(units, int),
$utils: ƒ $utils(),
add: add(number, units),
clone: ƒ clone(),
constructor: class Dayjs,
daysInMonth: ƒ daysInMonth(),
diff: ƒ diff(input, units, float),
endOf: ƒ endOf(arg),
format: format(formatStr),
get: ƒ get(unit),
init: ƒ init(),
isAfter: ƒ isAfter(that, units),
isBefore: ƒ isBefore(that, units),
isSame: ƒ isSame(that, units),
isValid: ƒ isValid(),
locale: ƒ locale(preset, object),
parse: ƒ parse(cfg),
set: ƒ set(string, int),
startOf: startOf(units, startOf),
subtract: ƒ subtract(number, string),
toDate: ƒ toDate(),
toISOString: ƒ toISOString(),
toJSON: ƒ toJSON(),
toString: ƒ toString(),
unix: ƒ unix(),
utcOffset: ƒ utcOffset(),
valueOf: valueOf(),
}
}

实例本身的属性是一些与时间相关的属性,各种操作方法都在原型 __proto__ 上。

本节完成,下一节开始解析 dayjs 的插件。


前端记事本,不定期更新,欢迎关注!


👆 全文结束,棒槌时间到 👇