插播一个新系列:时间库 dayjs
的源码解析。
用官方的描述 “Day.js
是 Moment.js
的 2kB 轻量化方案,拥有同样强大的 API
”。优点是如下三个:
- 简易:
Day.js
是一个轻量的处理时间和日期的JavaScript
库,和Moment.js
的API
设计保持完全一样。 - 不可变:所有的
API
操作都将返回一个新的Dayjs
实例。这种设计能避免bug
产生,节约调试时间。 - 国际化:
Day.js
对国际化支持良好。但除非手动加载,多国语言默认是不会被打包到工程里的。
总的来说,dayjs
的优点就是 plugin
和 locale
手动按需加载,减少打包体积。
dayjs
是饿了么的大佬 iamkun 开发维护的,大佬同时也是 ElementUI
的开发者。解析之前先从dayjs 源代码仓库 fork
了一份:github.com/MageeLin/da… 。
时间是 2020 年 12 月 7 日,commitID
是 eb5fbc4c
。解析时从 master
分支拉了一个新分支 analysis
。
打算分五章完成,目录如下:
- dayjs 源码解析(一):概念、locale、constant、utils
- dayjs 源码解析(二):Dayjs 类
- dayjs 源码解析(三):插件(上)
- dayjs 源码解析(四):插件(中)
- dayjs 源码解析(五):插件(下)
代码结构
目录结构
源代码的目录结构如下所示:
dayjs
│ .editorconfig // 编辑器配置
│ .eslintrc.json // ESLint配置
│ .gitignore // git忽略配置
│ .npmignore // npm发布忽略配置
│ .travis.yml // 持续集成配置
│ babel.config.js // babel配置
│ CHANGELOG.md // 更新日志
│ CONTRIBUTING.md // 共建指南
│ karma.sauce.conf.js // karma测试配置
│ LICENSE // 许可声明
│ package.json
│ prettier.config.js // prettier 格式化配置
│ README.md
│
├─.github // github的一些配置
├─build // 构建打包
├─docs // 各语言的说明文档
├─src
│ │ constant.js // 常量
│ │ index.js // 主入口,定义Dayjs类
│ │ utils.js // 工具函数
│ │
│ ├─locale // 国际化
│ └─plugin // 插件
├─test // 测试
└─types // TypeScript
依赖结构
入口 src/index.js
的依赖如下所示:
可以发现依赖链特别简单,没有依赖到 locale
和 plugin
目录下的语言包和插件。这也就是 dayjs
的核心优点。
基础概念
在分析源码之前,先理解下一些相关的基础概念。
时间标准
几种时间标准的解释来自维基百科。
GMT
格林尼治平均时间(Greenwich Mean Time,GMT
)是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时,因为本初子午线被定义为通过那里的经线。
自 1924 年 2 月 5 日开始,格林尼治天文台负责每隔一小时向全世界发放调时信息。
格林尼治标准时间的正午是指当平太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治平时基于天文观测本身的缺陷,已经被原子钟报时的协调世界时(UTC
)所取代。
UTC
协调世界时(英语:Coordinated Universal Time
,法语:Temps Universel Coordonné
,简称 UTC
)是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林威治标准时间。
协调世界时是世界上调节时钟和时间的主要时间标准,它与 0
度经线的平太阳时相差不超过 1
秒,并不遵守夏令时。
现行的协调世界时根据国际电信联盟的建议《Standard-frequency and time-signal emissions》(ITU-R TF.460-6)所确定。UTC
基于国际原子时,并在必要时通过不规则的加入闰秒来抵消地球自转变慢的影响。
如果本地时间比 UTC
时间快,例如中国、蒙古、菲律宾、新加坡、马来西亚、澳大利亚西部的时间比 UTC
快 8
小时,就会写作 UTC+8
,俗称东八区
。相反,如果本地时间比 UTC
时间慢,例如夏威夷的时间比 UTC
时间慢 10
小时,就会写作 UTC-10
,俗称西十区
。
ISO
国际标准 ISO 8601,是国际标准化组织的日期和时间的表示
方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。目前是 2004 年 12 月 1 日发行的第三版“ISO8601:2004”
在 Javascript 中的 Date.prototype.toISOString()
中,返回的是 YYYY-MM-DDTHH:mm:ss.sssZ
格式的字符串,时区总是 UTC(协调世界时),加一个后缀“Z”标识。
Date 对象输出时间的格式
Javascript 的 Date.prototype 上有很多种方式可以输出时间,以时间戳 1607561462990
为例,在 Chrome87 中返回值如下表:
方法 | 格式 | 输出 | valueOf | 时间戳 | 1607561462990 | getTime | GMT 时间戳 | 1607561462990 | toString | 英语格式的本地时间字符串 | Thu Dec 10 2020 08:51:02 GMT+0800 (中国标准时间) | toUTCString | 英语格式的 UTC 时间字符串 | Thu, 10 Dec 2020 00:51:02 GMT | toGMTString(标准已废弃) | 英语格式的 GMT 时间字符串 | Thu, 10 Dec 2020 00:51:02 GMT | toISOString | ISO 格式的 UTC 时间字符串 | 2020-12-10T00:51:02.990Z | toLocaleString | 字符串格式因不同语言而不同 | 2020/12/10 上午 8:51:02 | toJSON | 与 toISOString 相同 | 2020-12-10T00:51:02.990Z |
---|
语言(文化)代码
不同语言对事物的描述方式肯定不同,即使同一种语言由于文化地区差异,对相同事物的描述也有区别,所以国际上就形成了一套标准来识别各种语言。
先放一篇 Hax 的回答和 BCP47 规范,对于汉语代码来说,按照标准应该使用 zh-cmn-Hans-CN
、zh-cmn-Hant-HK
、zh-cmn-Hans-SG
、zh-cmn-Hant-TW
。但是由于历史的原因,广泛应用的是zh-CN
、zh-HK
、zh-SG
、zh-TW
。
引用一个通用的语言列表:
语言代码
国家|地区
"" (空字符串) 无变化的文化
af 公用荷兰语
af-ZA 公用荷兰语 - 南非
sq 阿尔巴尼亚
sq-AL 阿尔巴尼亚 -阿尔巴尼亚
ar 阿拉伯语
ar-DZ 阿拉伯语 -阿尔及利亚
ar-BH 阿拉伯语 -巴林
ar-EG 阿拉伯语 -埃及
ar-IQ 阿拉伯语 -伊拉克
ar-JO 阿拉伯语 -约旦
ar-KW 阿拉伯语 -科威特
ar-LB 阿拉伯语 -黎巴嫩
ar-LY 阿拉伯语 -利比亚
ar-MA 阿拉伯语 -摩洛哥
ar-OM 阿拉伯语 -阿曼
ar-QA 阿拉伯语 -卡塔尔
ar-SA 阿拉伯语 - 沙特阿拉伯
ar-SY 阿拉伯语 -叙利亚共和国
ar-TN 阿拉伯语 -北非的共和国
ar-AE 阿拉伯语 - 阿拉伯联合酋长国
ar-YE 阿拉伯语 -也门
hy 亚美尼亚
hy-AM 亚美尼亚的 -亚美尼亚
az Azeri
az-AZ-Cyrl Azeri-(西里尔字母的) 阿塞拜疆
az-AZ-Latn Azeri(拉丁文)- 阿塞拜疆
eu 巴斯克
eu-ES 巴斯克 -巴斯克
be Belarusian
be-BY Belarusian-白俄罗斯
bg 保加利亚
bg-BG 保加利亚 -保加利亚
ca 嘉泰罗尼亚
ca-ES 嘉泰罗尼亚 -嘉泰罗尼亚
zh-HK 中国 -香港
zh-MO 中国 -澳门
zh-CN 中国 -中国
zh-CHS 中国 (单一化)
zh-SG 中国 -新加坡
zh-TW 中国 -台湾
zh-CHT 中国 (传统的)
hr 克罗埃西亚
hr-HR 克罗埃西亚 -克罗埃西亚
cs 捷克
cs-CZ 捷克 - 捷克
da 丹麦文
da-DK 丹麦文 -丹麦
div Dhivehi
div-MV Dhivehi-马尔代夫
nl 荷兰
nl-BE 荷兰 -比利时
nl-NL 荷兰 - 荷兰
en 英国
en-AU 英国 -澳洲
en-BZ 英国 -伯利兹
en-CA 英国 -加拿大
en-CB 英国 -加勒比海
en-IE 英国 -爱尔兰
en-JM 英国 -牙买加
en-NZ 英国 - 新西兰
en-PH 英国 -菲律宾共和国
en-ZA 英国 - 南非
en-TT 英国 - 千里达托贝哥共和国
en-GB 英国 - 英国
en-US 英国 - 美国
en-ZW 英国 -津巴布韦
et 爱沙尼亚
et-EE 爱沙尼亚的 -爱沙尼亚
fo Faroese
fo-FO Faroese- 法罗群岛
fa 波斯语
fa-IR 波斯语 -伊朗王国
fi 芬兰语
fi-FI 芬兰语 -芬兰
fr 法国
fr-BE 法国 -比利时
fr-CA 法国 -加拿大
fr-FR 法国 -法国
fr-LU 法国 -卢森堡
fr-MC 法国 -摩纳哥
fr-CH 法国 -瑞士
gl 加利西亚
gl-ES 加利西亚 -加利西亚
ka 格鲁吉亚州
ka-GE 格鲁吉亚州 -格鲁吉亚州
de 德国
de-AT 德国 -奥地利
de-DE 德国 -德国
de-LI 德国 -列支敦士登
de-LU 德国 -卢森堡
de-CH 德国 -瑞士
el 希腊
el-GR 希腊 -希腊
gu Gujarati
gu-IN Gujarati-印度
he 希伯来
he-IL 希伯来 -以色列
hi 北印度语
hi-IN 北印度的 -印度
hu 匈牙利
hu-HU 匈牙利的 -匈牙利
is 冰岛语
is-IS 冰岛的 -冰岛
id 印尼
id-ID 印尼 -印尼
it 意大利
it-IT 意大利 -意大利
it-CH 意大利 -瑞士
ja 日本
ja-JP 日本 -日本
kn 卡纳达语
kn-IN 卡纳达语 -印度
kk Kazakh
kk-KZ Kazakh-哈萨克
kok Konkani
kok-IN Konkani-印度
ko 韩国
ko-KR 韩国 -韩国
ky Kyrgyz
ky-KZ Kyrgyz-哈萨克
lv 拉脱维亚
lv-LV 拉脱维亚的 -拉脱维亚
lt 立陶宛
lt-LT 立陶宛 -立陶宛
mk 马其顿
mk-MK 马其顿 -FYROM
ms 马来
ms-BN 马来 -汶莱
ms-MY 马来 -马来西亚
mr 马拉地语
mr-IN 马拉地语 -印度
mn 蒙古
mn-MN 蒙古 -蒙古
no 挪威
nb-NO 挪威 (Bokm?l) - 挪威
nn-NO 挪威 (Nynorsk)- 挪威
pl 波兰
pl-PL 波兰 -波兰
pt 葡萄牙
pt-BR 葡萄牙 -巴西
pt-PT 葡萄牙 -葡萄牙
pa Punjab 语
pa-IN Punjab 语 -印度
ro 罗马尼亚语
ro-RO 罗马尼亚语 -罗马尼亚
ru 俄国
ru-RU 俄国 -俄国
sa 梵文
sa-IN 梵文 -印度
sr-SP-Cyrl 塞尔维亚 -(西里尔字母的) 塞尔
sr-SP-Latn 塞尔维亚 (拉丁文)- 塞尔维亚共
sk 斯洛伐克
sk-SK 斯洛伐克 -斯洛伐克
sl 斯洛文尼亚
sl-SI 斯洛文尼亚 -斯洛文尼亚
es 西班牙
es-AR 西班牙 -阿根廷
es-BO 西班牙 -玻利维亚
es-CL 西班牙 -智利
es-CO 西班牙 -哥伦比亚
es-CR 西班牙 - 哥斯达黎加
es-DO 西班牙 - 多米尼加共和国
es-EC 西班牙 -厄瓜多尔
es-SV 西班牙 - 萨尔瓦多
es-GT 西班牙 -危地马拉
es-HN 西班牙 -洪都拉斯
es-MX 西班牙 -墨西哥
es-NI 西班牙 -尼加拉瓜
es-PA 西班牙 -巴拿马
es-PY 西班牙 -巴拉圭
es-PE 西班牙 -秘鲁
es-PR 西班牙 - 波多黎各
es-ES 西班牙 -西班牙
es-UY 西班牙 -乌拉圭
es-VE 西班牙 -委内瑞拉
sw Swahili
sw-KE Swahili-肯尼亚
sv 瑞典
sv-FI 瑞典 -芬兰
sv-SE 瑞典 -瑞典
syr Syriac
syr-SY Syriac-叙利亚共和国
ta 坦米尔
ta-IN 坦米尔 -印度
tt Tatar
tt-RU Tatar-俄国
te Telugu
te-IN Telugu-印度
th 泰国
th-TH 泰国 -泰国
tr 土耳其语
tr-TR 土耳其语 -土耳其
uk 乌克兰
uk-UA 乌克兰 -乌克兰
ur Urdu
ur-PK Urdu-巴基斯坦
uz Uzbek
uz-UZ-Cyrl Uzbek-(西里尔字母的) 乌兹别克
uz-UZ-Latn Uzbek(拉丁文)- 乌兹别克斯坦
vi 越南
vi-VN 越南 -越南
locale
对于 day.js
来说,同样也是实现了很多种语言的国际化,都放置在 src/locale
目录下,跟语言代码稍微有点不同的就是命名全部小写。
由于 day.js
是按需加载的,所以在使用某种语言前需要提前引入:
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn'); // 全局使用
dayjs().locale('zh-cn').format(); // 当前实例使用
其实 dayjs/locale/xxx.js
种保存的是对应语言的各种模板和配置,以 zh-cn.js
为例:
// Chinese [zh]
import dayjs from 'dayjs';
// locale 对象
const locale = {
name: 'zh', // 对象的名,关键
// 数组都是用 split 实现
// weekdays 数组
weekdays: '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
// 可选,简写 weekdays 数组,没有就用前 3 个字符
weekdaysShort: '周日_周一_周二_周三_周四_周五_周六'.split('_'),
// 可选,最简写 weekdays 数组,没有就用前 2 个字符
weekdaysMin: '日_一_二_三_四_五_六'.split('_'),
// months 数组
months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split(
'_'
),
// 可选,简写 months 数组,没有就用前 3 个字符
monthsShort: '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
/**
* @description: 返回例如3周,2日
* @param {Number} number 第几个
* @param {String} period 单位标志
* @return {String}
*/
ordinal: (number, period) => {
switch (period) {
case 'W':
return `${number}周`;
default:
return `${number}日`;
}
},
// 可选,设置一周的开始,默认周日,1 代表周一
weekStart: 1,
// 可选,设置一年的开始周,包含1月4日的那一周作为第一周
yearStart: 4,
// 格式化模板
formats: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'YYYY/MM/DD',
LL: 'YYYY年M月D日',
LLL: 'YYYY年M月D日Ah点mm分',
LLLL: 'YYYY年M月D日ddddAh点mm分',
// 小写或者简写
l: 'YYYY/M/D',
ll: 'YYYY年M月D日',
lll: 'YYYY年M月D日 HH:mm',
llll: 'YYYY年M月D日dddd HH:mm',
},
// 相对时间的格式化模板,保正 %s %d 相同
relativeTime: {
future: '%s后',
past: '%s前',
s: '几秒',
m: '1 分钟',
mm: '%d 分钟',
h: '1 小时',
hh: '%d 小时',
d: '1 天',
dd: '%d 天',
M: '1 个月',
MM: '%d 个月',
y: '1 年',
yy: '%d 年',
},
/**
* @description: 根据时和分返回当前的时间阶段
* @param {Number} hour 时
* @param {Number} minute 分
* @return {String} 时间阶段
*/
meridiem: (hour, minute) => {
const hm = hour * 100 + minute;
if (hm < 600) {
return '凌晨';
} else if (hm < 900) {
return '早上';
} else if (hm < 1130) {
return '上午';
} else if (hm < 1230) {
return '中午';
} else if (hm < 1800) {
return '下午';
}
return '晚上';
},
};
// 把 locale 对象加载到locale的 Ls 中
dayjs.locale(locale, null, true);
export default locale;
除了配置以外,可以发现,最后两步中首先把 locale
对象加载并保存,然后把 locale
对象默认导出。所以虽然官方没明说,但是也可以如下导入:
import dayjs from 'dayjs';
import zhCN from 'dayjs/locale/zh-cn';
dayjs.locale(zhCN); // 全局使用
dayjs().locale(zhCN).format(); // 当前实例使用
constant
src/constant.js
中存放的是一些常量和正则表达式。包括不同单位包含的秒数和毫秒数、标准的时间单位表达、默认格式化模板、无效时间和两个正则表达式。
// 计算几个常量
// 包含的秒数
export const SECONDS_A_MINUTE = 60;
export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60;
export const SECONDS_A_DAY = SECONDS_A_HOUR * 24;
export const SECONDS_A_WEEK = SECONDS_A_DAY * 7;
// 包含的毫秒数
export const MILLISECONDS_A_SECOND = 1e3;
export const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND;
export const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND;
export const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND;
export const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND;
// 标准的 unit 写法
export const MS = 'millisecond';
export const S = 'second';
export const MIN = 'minute';
export const H = 'hour';
export const D = 'day';
export const W = 'week';
export const M = 'month';
export const Q = 'quarter';
export const Y = 'year';
export const DATE = 'date';
// 默认时间格式是ISO 2020-12-06T20:11:43Z
export const FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ';
// 无效时间
export const INVALID_DATE_STRING = 'Invalid Date';
// 正则表达式
export const REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?.?(\d+)?$/;
export const REGEX_FORMAT = /\[([^\]]+)]|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;
这两个正则表达式比较有意思,第一个正则表达式 REGEX_PARSE
是用来解析字符串格式的时间,便于生成 Dayjs
实例关联的 Date
对象;第二个正则表达式 REGEX_FORMAT
用于解析 format
参数,返回想要的时间格式。
utils
src/utils.js
中存放的是一些工具函数。其实在 index.js
中也放置了很多工具函数,只不过那些工具函数需要用到一些 index.js
的全局变量,所以不能定义在 utils.js
中。但是在 index.js
中最后还是把它们放在了一个 Utils
对象里共同管理。
// C 是定义的常量 constant
import * as C from './constant.js';
/**
* @description: 在 string 的开头补充 pad,直到长度为 length,相当于`string.padStart(length, pad)`
* @param {String} string 被补充的字符串
* @param {Number} length 最后的长度
* @param {String} pad 填充的内容
* @return {String} 补充后的字符串
*/
const padStart = (string, length, pad) => {
const s = String(string);
if (!s || s.length >= length) return string;
// 前面的数组join更简单的表示就是 `pad.repeat(length - string.length)`
return `${Array(length + 1 - s.length).join(pad)}${string}`;
};
/**
* @description: 返回实例的UTC偏移量(分钟)转化成的 [+|-]HH:mm的格式
* @param {Dayjs} instance Dayjs的实例
* @return {String} UTC偏移量 格式:[+|-]HH:mm
*/
const padZoneStr = (instance) => {
// 这里感觉用Number(instance.utcOffset())会更易读
const negMinutes = -instance.utcOffset();
const minutes = Math.abs(negMinutes);
const hourOffset = Math.floor(minutes / 60);
const minuteOffset = minutes % 60;
return `${negMinutes <= 0 ? '+' : '-'}${padStart(
hourOffset,
2,
'0'
)}:${padStart(minuteOffset, 2, '0')}`;
};
/**
* @description: 求两个实例的月份差
* @param {Dayjs} a Dayjs的实例
* @param {Dayjs} b Dayjs的实例
* @return {Number} 返回两个实例的月份差
*/
const monthDiff = (a, b) => {
// 来自moment.js的函数,保证能返回相同的结果
if (a.date() < b.date()) return -monthDiff(b, a);
const wholeMonthDiff = (b.year() - a.year()) * 12 + (b.month() - a.month());
const anchor = a.clone().add(wholeMonthDiff, C.M);
const c = b - anchor < 0;
const anchor2 = a.clone().add(wholeMonthDiff + (c ? -1 : 1), C.M);
return +(
-(
wholeMonthDiff +
(b - anchor) / (c ? anchor - anchor2 : anchor2 - anchor)
) || 0
);
};
/**
* @description: 向 0 取整
* @param {Number} n 要取整的数字
* @return {Number} 取整后的数字
*/
const absFloor = (n) => (n < 0 ? Math.ceil(n) || 0 : Math.floor(n));
/**
* @description: 返回 u 对应的单位,能自动适配标准格式和缩写格式
* @param {String} u M(month) y(year) w(week) d(day) D(date) h(hour) m(minute) s(second) ms(millisecond) Q(quarter) 或 其他字符串
* @return {String} u 对应的单位
*/
const prettyUnit = (u) => {
const special = {
M: C.M,
y: C.Y,
w: C.W,
d: C.D,
D: C.DATE,
h: C.H,
m: C.MIN,
s: C.S,
ms: C.MS,
Q: C.Q,
};
return (
// 返回 u 对应的单位
special[u] ||
// 或者是把 u 结尾的 字符s 删除,当作单位
String(u || '')
.toLowerCase()
.replace(/s$/, '')
);
};
/**
* @description: 判断是否为 undefined
* @param {Any} s
* @return {Boolean} true: 是, false: 否
*/
const isUndefined = (s) => s === undefined;
export default {
s: padStart,
z: padZoneStr,
m: monthDiff,
a: absFloor,
p: prettyUnit,
u: isUndefined,
};
本篇内容完成,下一篇文章来分析 day.js
的核心 src/index.js
文件,学习 Dayjs
类的实现。
前端记事本,不定期更新,欢迎关注!
- 微信公众号: 林景宜的记事本
- 博客:林景宜的记事本
- 掘金专栏:林景宜的记事本
- 知乎专栏: 林景宜的记事本
- Github: MageeLin
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!