插播一个新系列:时间库 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
了一份:https://github.com/MageeLin/dayjs-source-code-analysis 。
时间是 2020 年 12 月 7 日,commitID
是 eb5fbc4c
。解析时从 master
分支拉了一个新分支 analysis
。
打算分五章完成,目录如下:
dayjs 源码解析(一):概念、locale、constant、utils
dayjs 源码解析(二):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 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
。
引用一个通用的语言列表 :
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 语言代码 国家|地区 "" (空字符串) 无变化的文化 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
是按需加载的,所以在使用某种语言前需要提前引入:
1 2 3 4 import 'dayjs/locale/zh-cn' ;dayjs.locale('zh-cn' ); dayjs().locale('zh-cn' ).format();
其实 dayjs/locale/xxx.js
种保存的是对应语言的各种模板和配置,以 zh-cn.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 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 import dayjs from 'dayjs' ;const locale = { name: 'zh' , weekdays: '星期日_星期一_星期二_星期三_星期四_星期五_星期六' .split('_' ), weekdaysShort: '周日_周一_周二_周三_周四_周五_周六' .split('_' ), weekdaysMin: '日_一_二_三_四_五_六' .split('_' ), months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月' .split( '_' ), monthsShort: '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月' .split('_' ), ordinal: (number, period ) => { switch (period) { case 'W' : return `${number} 周` ; default : return `${number} 日` ; } }, weekStart: 1 , 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' , }, 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 年' , }, 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 '晚上' ; }, }; dayjs.locale(locale, null , true ); export default locale;
除了配置以外,可以发现,最后两步中首先把 locale
对象加载并保存,然后把 locale
对象默认导出。所以虽然官方没明说,但是也可以如下导入:
1 2 3 4 5 import dayjs from 'dayjs' ;import zhCN from 'dayjs/locale/zh-cn' ;dayjs.locale(zhCN); dayjs().locale(zhCN).format();
constant src/constant.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 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;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' ;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
对象里共同管理。
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 import * as C from './constant.js' ;const padStart = (string, length, pad ) => { const s = String (string); if (!s || s.length >= length) return string; return `${Array (length + 1 - s.length).join(pad)} ${string} ` ; }; const padZoneStr = (instance ) => { 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' )} ` ;}; const monthDiff = (a, b ) => { 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 ); }; const absFloor = (n ) => (n < 0 ? Math .ceil(n) || 0 : Math .floor(n));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 ( special[u] || String (u || '' ) .toLowerCase() .replace(/s$/ , '' ) ); }; 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
类的实现。
前端记事本,不定期更新,欢迎关注!