0%

dayjs源码解析(三):插件(上)

接上篇 —— dayjs 源码解析(二):Dayjs 类 —— 继续解析 dayjs 的源码。

从本篇开始,分三篇解析 dayjs 源码中插件功能的部分,也就是 src/plugin 目录下的文件。

目录如下:

  1. dayjs 源码解析(一):概念、locale、constant、utils
  2. dayjs 源码解析(二):Dayjs 类
  3. dayjs 源码解析(三):插件(上)
  4. dayjs 源码解析(四):插件(中)
  5. dayjs 源码解析(五):插件(下)

插件加载

src/index.js 写了加载插件的方法 dayjs.extend,很显然插件是一个函数,接收三个参数 optionDayjs 类dayjs 函数对象

并给 plugin 函数对象设了个 $i 来保证单例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @description: 挂载插件
* @param {Function} plugin 插件
* @param {*} option 插件选项
* @return {dayjs function} 返回 dayjs 函数对象
*/
dayjs.extend = (plugin, option) => {
// 同一个插件只挂载一次
if (!plugin.$i) {
plugin(option, Dayjs, dayjs); //挂载
plugin.$i = true;
}
return dayjs;
};

下面是大部分插件的标准写法,可以新加方法也可以覆盖原有的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} d dayjs函数对象
*/
export default (o, c, d) => {
const proto = c.prototype;
/**
* @description: demo
* @return {Boolean}
*/
proto.demo = function (args) {};
};

is 系列

首选先从一些简单的插件开始解析。is 开头的插件都是用来判断的,返回一个 Boolean 值来表示是否。

isLeapYear

判断是否为闰年比较简单,必须同时满足以下两个条件:

  1. 能被 4 整除且不能被 100 整除;
  2. 能被 400 整除;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
*/
export default (o, c) => {
const proto = c.prototype;
/**
* @description: 返回一个 boolean 来展示一个 Day.js 对象的年份是不是闰年。
* @return {Boolean}
*/
proto.isLeapYear = function () {
// 判断闰年需要满足的两种情况,1.能被4整除且不能被100整除 2.能被400整除
return (this.$y % 4 === 0 && this.$y % 100 !== 0) || this.$y % 400 === 0;
};
};

isMoment

官网上并没有给这个方法的文档,其实想着判断是否为 Moment 的实例就不合理。所以直接判断是否为 Dayjs 的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} f dayjs函数对象
*/
export default (o, c, f) => {
/**
* @description: 指示对象是不是Dayjs的实例
* @param {Object} input
* @return {Boolean}
*/
f.isMoment = function (input) {
// 最终用的是判断是不是Dayjs的实例😂
return f.isDayjs(input);
};
};

isBetween、isSameOrAfter、isSameOrBefore

三个判断相对早晚的方法实现的原理基本一致。都是组合的 Dayjs.prototype.isSameDayjs.prototype.isBeforeDayjs.prototype.isAfter,很简单。

特别提到的是 isBetween,用的是数学上的 ()[] 来表示两个的开闭性的。

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
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
*/
export default (o, c) => {
/**
* @description: 返回一个 boolean 来实例是否和一个时间相同或在该时间之前。
* @param {Dayjs} that 另一个Dayjs实例
* @param {String} unit 时间单位
* @return {Boolean}
*/
c.prototype.isSameOrAfter = function (that, units) {
// 调用了 isSame 和 isAfter
return this.isSame(that, units) || this.isAfter(that, units);
};
};
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
*/
export default (o, c) => {
/**
* @description: 返回一个 boolean 来实例是否和一个时间相同或在该时间之后。
* @param {Dayjs} that 另一个Dayjs实例
* @param {String} unit 时间单位
* @return {Boolean}
*/
c.prototype.isSameOrBefore = function (that, units) {
// 调用了 isSame 和 isBefore
return this.isSame(that, units) || this.isBefore(that, units);
};
};
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} d dayjs函数对象
*/
export default (o, c, d) => {
/**
* @description: 返回一个 boolean 来展示一个时间是否介于两个时间之间
* @param {String|Dayjs} a 时间
* @param {String|Dayjs} b 时间
* @param {String} u unit 时间单位
* @param {String} i include 区间开闭性 () (] [] [)
* @return {Boolean} 指示时间是否介于两个时间之间
*/
c.prototype.isBetween = function (a, b, u, i) {
// 实例化两端时间
const dA = d(a);
const dB = d(b);
// 判断两端开闭性
i = i || '()';
const dAi = i[0] === '(';
const dBi = i[1] === ')';

// 利用原型上的 isAfter 和 isBefore 实现 isBetween 判断
return (
((dAi ? this.isAfter(dA, u) : !this.isBefore(dA, u)) &&
(dBi ? this.isBefore(dB, u) : !this.isAfter(dB, u))) ||
((dAi ? this.isBefore(dA, u) : !this.isAfter(dA, u)) &&
(dBi ? this.isAfter(dB, u) : !this.isBefore(dB, u)))
);
};
};

isToday、isTomorrow、isYesterday

这三个判断是否在昨天、今天和明天的方法实现的原理也是一样的。都是用当前实例被比较日的实例,同时格式化成 YYYY-MM-DD 的形式,然后比较字符串是否相同。

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
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} d dayjs函数对象
*/
export default (o, c, d) => {
const proto = c.prototype;
/**
* @description: 判断当前 Day.js 实例是否是今天。
* @return {Boolean}
*/
proto.isToday = function () {
const comparisonTemplate = 'YYYY-MM-DD';
const now = d();

// 要比较的两个实例同时输出为 YYYY-MM-DD 格式字符串,相同就代表为同一天
return this.format(comparisonTemplate) === now.format(comparisonTemplate);
};
};
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} d dayjs函数对象
*/
export default (o, c, d) => {
const proto = c.prototype;
/**
* @description: 判断当前 Day.js 对象是否是明天。
* @return {Boolean}
*/
proto.isTomorrow = function () {
const comparisonTemplate = 'YYYY-MM-DD';
// 新建一个明天的实例
const tomorrow = d().add(1, 'day');

// 要比较的两个实例同时输出为 YYYY-MM-DD 格式字符串,相同就代表为同一天
return (
this.format(comparisonTemplate) === tomorrow.format(comparisonTemplate)
);
};
};
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} d dayjs函数对象
*/
export default (o, c, d) => {
const proto = c.prototype;
/**
* @description: 判断当前 Day.js 对象是否是昨天。
* @return {Boolean}
*/
proto.isYesterday = function () {
const comparisonTemplate = 'YYYY-MM-DD';
// 新建一个昨天的实例
const yesterday = d().subtract(1, 'day');

// 要比较的两个实例同时输出为 YYYY-MM-DD 格式字符串,相同就代表为同一天
return (
this.format(comparisonTemplate) === yesterday.format(comparisonTemplate)
);
};
};

week 系列

分析 week 系列的代码前,先来普及下关于“周(week)”这个单位的一些基本知识。

ISO8601 对周做了规定:本年度第一个周四所在的周为本年度的第 1 周。这句话与下面的三个说法等价:

  • 1 月 4 日所在的周四;
  • 本年度第一个至少有 4 天在同一周内的周;
  • 周一在去年 12 月 29 日至今年 1 月 4 日以内的周;

这种定义方式就会产生一个问题,每年的靠近 1 月 1 日的前后几天,在 ISO8601 的周历算法中,可能并不属于所在的那一年。

举个例子,2021 年 1 月 1 日是周五,2021 年第一个周四是 1 月 7 日,所以在 ISO 周历算法中, 1 月 4 日1 月 10 日 这一周才是 2021 年的第 1 周;也就代表着 12 月 28 日1 月 3 日 这一周是 2020 年的第 53 周

ISO8601 还把一周第一天定义为了周一。通过初始周周第一天的定义,把 1 年分为了 52 周或 53 周。

这种对周的算法主要应用于政府和商务的会计年度。

ISO week

src/plugin 中关于 iso 的插件有两个:isoWeekisoWeeksInYear

isoWeek

isoWeek 插件是一个比较大的插件,它在 Dayjs.prototype 上添加和拓展了四个方法:

  • isoWeekYear: 获取实例所在的 ISO 周所在的年;
  • isoWeek: 获取或设置年度的第 ISO 周数
  • isoWeekday: 获取或设置一周的第 ISO 日,范围是 1-7
  • startOf: 扩展 .startOf .endOfAPIs,使其支持单位 isoWeek
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
import { D, W, Y } from '../../constant';

const isoWeekPrettyUnit = 'isoweek';

/**
* @description: plugin ISO-8601 基于周的日历
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} d dayjs函数对象
*/
export default (o, c, d) => {
/**
* @description: 获取指定年的第一个星期四
* @param {Number} year 年
* @param {Boolean} isUtc 是否使用UTC模式
* @return {Dayjs}
*/
const getYearFirstThursday = (year, isUtc) => {
const yearFirstDay = (isUtc ? d.utc : d)().year(year).startOf(Y);
// 4 减 一月一号的星期几
let addDiffDays = 4 - yearFirstDay.isoWeekday();
if (yearFirstDay.isoWeekday() > 4) {
addDiffDays += 7;
}
// 获得了指定年的第一个星期四的实例
return yearFirstDay.add(addDiffDays, D);
};

/**
* @description: 获取离实例日期最近的星期四
* @param {Dayjs} ins 实例
* @return {Dayjs} 返回新实例
*/
// 4 减 今天的星期几
const getCurrentWeekThursday = (ins) => ins.add(4 - ins.isoWeekday(), D);

const proto = c.prototype;

/**
* @description: 获取实例所在的 ISO 周所在的年
* @return {Number}
*/
proto.isoWeekYear = function () {
// 获取最近的星期四所在的年
const nowWeekThursday = getCurrentWeekThursday(this);
return nowWeekThursday.year();
};

/**
* @description: 获取或设置年度的第 ISO 周数。
* @param {Number} week ISO周数
* @return {Number|Dayjs}
*/
proto.isoWeek = function (week) {
// setter 算出周差,再加上
if (!this.$utils().u(week)) {
return this.add((week - this.isoWeek()) * 7, D);
}
// getter
// 最近周四的实例
const nowWeekThursday = getCurrentWeekThursday(this);
// 今年第一个周四的实例
const diffWeekThursday = getYearFirstThursday(this.isoWeekYear(), this.$u);
// 算出周差后加一就是周数
return nowWeekThursday.diff(diffWeekThursday, W) + 1;
};

/**
* @description: 获取或设置一周的第 ISO 日,范围是 1-7
* @param {Number} week 有值则为setter,无值则为getter
* @return {Number|Dayjs}
*/
proto.isoWeekday = function (week) {
// setter时,用this.day()除7取余
if (!this.$utils().u(week)) {
return this.day(this.day() % 7 ? week : week - 7);
}
// getter时,直接返回 this.day(), 0的时候就是7
return this.day() || 7;
};

const oldStartOf = proto.startOf;
/**
* @description: 扩展 .startOf .endOf APIs 支持单位 isoWeek
* @param {String} units 单位
* @param {Boolean} startOf 标志,true:startOf, false: endOf
* @return {Dayjs} 返回新的 Dayjs 实例,cfg与原实例相同
*/
proto.startOf = function (units, startOf) {
const utils = this.$utils();
const isStartOf = !utils.u(startOf) ? startOf : true;
// 处理下单位 isoWeek
const unit = utils.p(units);
if (unit === isoWeekPrettyUnit) {
// 获取本周一的开始
return isStartOf
? this.date(this.date() - (this.isoWeekday() - 1)).startOf('day')
: // 获取本周末的结束
this.date(this.date() - 1 - (this.isoWeekday() - 1) + 7).endOf('day');
}
// 普通情况还是用老版本的 oldStartOf 处理
return oldStartOf.bind(this)(units, startOf);
};
};

isoWeeksInYear

这个方法就比较简单,计算实例所在年的 ISO 周总数,5253

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} d dayjs函数对象
*/
export default (o, c) => {
const proto = c.prototype;
proto.isoWeeksInYear = function () {
const isLeapYear = this.isLeapYear();
const last = this.endOf('y');
const day = last.day();
if (day === 4 || (isLeapYear && day === 5)) {
return 53;
}
return 52;
};
};

普通 week

ISO8601 统一了对于周的历法,但是在各个国家和地区的传统中,对于周历中,每年的初始周和每周的初始日定义都各不相同。在 dayjs 中,把这两种设置在了 locale 中。

以汉语举例:

1
2
3
4
5
6
const locale = {
// 可选,设置一周的开始,默认周日,1 代表周一
weekStart: 1,
// 可选,设置一年的开始周,包含1月4日的那一周作为第一周
yearStart: 4,
};

可以发现,汉语环境下周历法的设置与 ISO8601 周历法一致。普通的 week 插件是如下三个:

  • weekday:在当前语言环境下,获取或设置实例是本周的第几天;
  • weekOfYear:在当前语言环境下,获取或设置实例是年中第几周;
  • weekYear:在当前语言环境下,获取的按周历算,实例所在的年份;

weekday

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
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
*/
export default (o, c) => {
const proto = c.prototype;
/**
* @description: 获取或设置当前语言环境下本周的第几天。
* @param {Number} input
* @return {Number|Dayjs} getter时返回本周的第几天,setter时返回新实例
*/
proto.weekday = function (input) {
// locale 中设置的一周开始,中文是星期一开始,所以是weekStart = 1
const weekStart = this.$locale().weekStart || 0;
// 今天是周三,所以是$W = 3
const { $W } = this;
// 周三的 weekday = 3 - 1 = 2
const weekday = ($W < weekStart ? $W + 7 : $W) - weekStart;
if (this.$utils().u(input)) {
return weekday;
}
// 减去2天,是周一,加上input天,就代表这设为了本周的第input天
return this.subtract(weekday, 'day').add(input, 'day');
};
};

weekOfYear

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 { MS, Y, D, W } from '../../constant';

/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
* @param {Function} d dayjs函数对象
*/
export default (o, c, d) => {
const proto = c.prototype;
/**
* @description: 在当前语言环境下,返回一个 number 来get实例是年中第几周,或者通过参数来set实例。默认的原型中是没有 week 这个单位的getter/setter的。
* @param {Number} week set时的参数,设为第 week 周
* @return {Number|setter} setter时返回新实例,getter时返回周数
*/
proto.week = function (week = null) {
// 有参数,也就是setter,此时就计算week差,然后add上
if (week !== null) {
return this.add((week - this.week()) * 7, D);
}
// getter
// 获取设置的yearStart
const yearStart = this.$locale().yearStart || 1;
// 如果下一年的开始早于本周的最后,就代表是下一年的第一周,返回 1
if (this.month() === 11 && this.date() > 25) {
// d(this) is for badMutable
const nextYearStartDay = d(this).startOf(Y).add(1, Y).date(yearStart);
const thisEndOfWeek = d(this).endOf(W);
if (nextYearStartDay.isBefore(thisEndOfWeek)) {
return 1;
}
}
// diffInWeek 小于 0,就返回本周第一天的week数
const yearStartDay = d(this).startOf(Y).date(yearStart);
const yearStartWeek = yearStartDay.startOf(W).subtract(1, MS);
const diffInWeek = this.diff(yearStartWeek, W, true);
if (diffInWeek < 0) {
return d(this).startOf('week').week();
}
// 普通情况就直接返回 diffInWeek
return Math.ceil(diffInWeek);
};

proto.weeks = function (week = null) {
return this.week(week);
};
};

weekYear

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @description: plugin
* @param {Object} o option
* @param {Class} c Dayjs类
*/
export default (o, c) => {
const proto = c.prototype;
/**
* @description: 在当前语言环境下,获取的按周历算,实例所在的年份。大部分语言环境下包含1月4日的那一周作为一年的第一周。所以就会出现虽然是上一年,按周算却是下一年的情况
* @return {Number} 返回按周计算的年数
*/
proto.weekYear = function () {
const month = this.month();
const weekOfYear = this.week();
const year = this.year();
// 月数为 12 ,但周数却为1,说明要算到下一年中,就给返回年数加1
if (weekOfYear === 1 && month === 11) {
return year + 1;
}
return year;
};
};

下篇继续分析插件。


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


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