接上篇 —— dayjs 源码解析(三):插件(上) —— 继续解析 dayjs
的源码。
本篇继续解析 dayjs
源码中插件功能的部分,也就是 src/plugin
目录下的文件。挑选出几个代码比较长,实现比较复杂的插件 customFormat
、duration
、objectSupport
、relativeTime
、timezone
、utc
。
在分析源码的过程中也发现了一个规律,dayjs
的文档写的比较不详细,有的插件的用途让人一眼看上去很懵。但是既然 dayjs
实现了 momentjs
的 API
,所以当看 dayjs
文档看不懂的时候,就可以去看 momentjs
的中文文档 来辅助理解。
目录如下:
dayjs 源码解析(一):概念、locale、constant、utils
dayjs 源码解析(二):Dayjs 类
dayjs 源码解析(三):插件(上)
dayjs 源码解析(四):插件(中)
dayjs 源码解析(五):插件(下)
customParseFormat
插件扩展了 dayjs
函数对象,使其能支持自定义时间格式。
第一个参数是被解析的字符串,第二个参数是解析用的模板,第三个参数是解析本地化语言的日期字符串或者布尔值或者决定使用严格模式(要求格式和输入内容完全匹配),,返回一个 Dayjs
实例。使用举例:
1 2 3 4 5 6 7 8 9 10 11 12 import dayjs from 'dayjs' ;import customParseFormat from 'dayjs/plugin/customParseFormat' ;dayjs.extend(customParseFormat); dayjs('05/02/69 1:02:03 PM -05:00' , 'MM/DD/YY H:mm:ss A Z' ); dayjs('2018 三月 15' , 'YYYY MMMM DD' , 'zh-cn' ); dayjs('1970-00-00' , 'YYYY-MM-DD' , true );
相关知识
输入
例子
详情
YY
18
两位数的年份
YYYY
2018
四位数的年份
M
1-12
月份,从 1 开始
MM
01-12
月份,两位数
MMM
Jan-Dec
缩写的月份名称
MMMM
January-December
完整的月份名称
D
1-31
月份里的一天
DD
01-31
月份里的一天,两位数
H
0-23
小时
HH
00-23
小时,两位数
h
1-12
小时, 12 小时制
hh
01-12
小时, 12 小时制, 两位数
m
0-59
分钟
mm
00-59
分钟,两位数
s
0-59
秒
ss
00-59
秒 两位数
S
0-9
毫秒,一位数
SS
00-99
毫秒,两位数
SSS
000-999
毫秒,三位数
Z
-05:00
UTC 的偏移量
ZZ
-0500
UTC 的偏移量,两位数
A
AM PM
上午 下午 大写
a
am pm
上午 下午 小写
Do
1st… 31st
带序数词的月份里的一天
源码分析 插件实现时,最关键的三个方法是 parse
、parseFormattedInput
和 makeParser
。把三个方法中最核心的部分挑出来:
parse
,解析的入口,调用 parseFormattedInput
。
parseFormattedInput
,调用 makeParser
生成对应模板的解析器,解析后返回对应 Date
对象。
makeParser
,接收模板,返回解析器。
下面是三个关键方法的代码,全部源码过长,具体分析请移步 Github 。
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 proto.parse = function (cfg ) { this .$d = parseFormattedInput(date, format, utc); this .init(); }; const parseFormattedInput = (input, format, utc ) => { const parser = makeParser(format); const { year, month, day, hours, minutes, seconds, milliseconds, zone, } = parser(input); return new Date (y, M, d, h, m, s, ms); }; function makeParser (format ) { const array = format.match(formattingTokens); for (let i = 0 ; i < length; i += 1 ) { array[i] = { regex, parser }; } return function (input ) { const time = {}; for (let i = 0 , start = 0 ; i < length; i += 1 ) { const { regex, parser } = array[i]; parser.call(time, value); input = input.replace(value, '' ); } return time; }; }
duration duration
插件用来支持时间跨度。具体的 API
可以参照Moment 的 duration
。
使用举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import duration from 'dayjs/plugin/duration' ;dayjs.extend(duration); dayjs.duration(2 , 'minutes' ); dayjs.duration({ seconds: 2 , minutes: 2 , hours: 2 , days: 2 , weeks: 2 , months: 2 , years: 2 , }); dayjs.duration(-1 , 'minutes' ).humanize(true ); dayjs.duration(1500 ).milliseconds(); dayjs.duration(1500 ).asMilliseconds(); dayjs.duration(1500 ).as('seconds' );
源码分析 duration
的实现比较复杂,它实际上是实现了一个 Duration类
,用它来返回一个 duration实例
后,在实例上处理各种方法。
最后把实例化 Duration类
的方法绑定到了 dayjs函数对象
上,实现了 dayjs.duration(arguments)
的 API
。
下面是代码的骨架,全部源码过长,具体分析请移步 Github 。
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 const isDuration = (d ) => d instanceof Duration;let $d;let $u;const wrapper = (input, instance, unit ) => new Duration(input, unit, instance.$l); const prettyUnit = (unit ) => `${$u.p(unit)} s` ;class Duration { constructor (input, unit, locale) { return this ; } calMilliseconds() {} parseFromMilliseconds() {} toISOString() {} toJSON() {} format(formatStr) {} as (unit) {} get (unit) {} add(input, unit, isSubtract) {} subtract(input, unit) {} locale(l) {} clone() {} humanize(withSuffix) {} milliseconds() {} asMilliseconds() {} seconds() {} asSeconds() {} minutes() {} asMinutes() {} hours() {} asHours() {} days() {} asDays() {} weeks() {} asWeeks() {} months() {} asMonths() {} years() {} asYears() {} } export default (option, Dayjs, dayjs) => { $d = dayjs; $u = dayjs().$utils(); dayjs.duration = function (input, unit ) {}; dayjs.isDuration = isDuration; const oldAdd = Dayjs.prototype.add; const oldSubtract = Dayjs.prototype.subtract; Dayjs.prototype.add = function (value, unit ) {}; Dayjs.prototype.subtract = function (value, unit ) {}; };
objectSupport ObjectSupport
扩展了 dayjs()
, dayjs.utc
, dayjs().set
, dayjs().add
, dayjs().subtract
的 API
以支持传入对象参数。
使用举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import objectSupport from 'dayjs/plugin/objectSupport' ;dayjs.extend(objectSupport); dayjs({ year: 2010 , month: 1 , day: 12 , }); dayjs.utc({ year: 2010 , month: 1 , day: 12 , }); dayjs().set({ year : 2010 , month : 1 , day : 12 }); dayjs().add({ M : 1 }); dayjs().subtract({ month : 1 });
源码分析 支持函数对象的这个插件实现的非常标准,基本上所有的方法都是在扩展原有的方法。
比如下述 parse
的实现,先把老版本的 parse
保存成 oldParse
,进行对象版本的 parse
处理后再执行 oldParse
,保证同时兼容对象和其他格式。
1 2 3 4 5 6 7 8 9 10 const oldParse = proto.parse;proto.parse = function (cfg ) { cfg.date = parseDate.bind(this )(cfg); oldParse.bind(this )(cfg); };
再比如 parseDate
的实现,$d
不是对象时就走默认的 date
返回,isObject(date)
为 true
时,再执行对对象形式的 date
处理。
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 const parseDate = (cfg ) => { const { date, utc } = cfg; const $d = {}; if (isObject(date)) { if (!Object .keys(date).length) { return new Date (); } const now = utc ? dayjs.utc() : dayjs(); Object .keys(date).forEach((k ) => { $d[prettyUnit(k)] = date[k]; }); const d = $d.day || (!$d.year && !($d.month >= 0 ) ? now.date() : 1 ); const y = $d.year || now.year(); const M = $d.month >= 0 ? $d.month : !$d.year && !$d.day ? now.month() : 0 ; const h = $d.hour || 0 ; const m = $d.minute || 0 ; const s = $d.second || 0 ; const ms = $d.millisecond || 0 ; if (utc) { return new Date (Date .UTC(y, M, d, h, m, s, ms)); } return new Date (y, M, d, h, m, s, ms); } return date; };
而对属性修改的几个方法,则是抽出了共同的地方封装成一个 callObject
方法,然后对老版本的方法统一修改以适应对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const callObject = function (call, argument, string, offset = 1 ) { if (argument instanceof Object ) { const keys = Object .keys(argument); let chain = this ; keys.forEach((key ) => { chain = call.bind(chain)(argument[key] * offset, key); }); return chain; } return call.bind(this )(argument * offset, string); };
调用的时候如下扩展的 add
方法所示:
1 2 3 4 5 6 7 8 9 proto.add = function (number, string ) { return callObject.bind(this )(oldAdd, number, string); };
全部源码过长,具体分析请移步 Github 。
relativeTime relativeTime
插件增加了 .from
、 .to
、 .fromNow
、 .toNow
4 个 API
来展示相对的时间 (e.g. 3
小时以前)。
使用示例:
1 2 3 4 5 6 7 8 9 10 11 import relativeTime from 'dayjs/plugin/relativeTime' ;dayjs.extend(relativeTime); dayjs(765985205000 ).from(dayjs('1990-01-01' )); dayjs(765985205000 ).fromNow(); dayjs(765985205000 ).to(dayjs('1990-01-01' )); dayjs(765985205000 ).toNow(); dayjs(765985205000 ).from(dayjs('1990-01-01' ), true );
相关知识 范围对应的输出值如下表所示,也可以在插件的 option
中配置 thresholds
进行自定义。
范围
键值
示例输出
0 到 44 秒
s
几秒前
45 到 89 秒
m
1 分钟前
90 秒 到 44 分
mm
2 分钟前 … 44 分钟前
45 到 89 分
h
1 小时前
90 分 到 21 小时
hh
2 小时前 … 21 小时前
22 到 35 小时
d
1 天前
36 小时 到 25 天
dd
2 天前 … 25 天前
26 到 45 天
M
1 个月前
46 天 到 10 月
MM
2 个月前 … 10 个月前
11 月 到 17 月
y
1 年前
18 月以上
yy
2 年前 … 20 年前
源码分析 四个新添加的相对时间的 API
都是依赖的核心方法 fromTo
。
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 const fromTo = (input, withoutSuffix, instance, isFrom ) => { const loc = instance.$locale().relativeTime || relObj; const T = o.thresholds || [ { l : 's' , r : 44 , d : C.S }, { l : 'm' , r : 89 }, { l : 'mm' , r : 44 , d : C.MIN }, { l : 'h' , r : 89 }, { l : 'hh' , r : 21 , d : C.H }, { l : 'd' , r : 35 }, { l : 'dd' , r : 25 , d : C.D }, { l : 'M' , r : 45 }, { l : 'MM' , r : 10 , d : C.M }, { l : 'y' , r : 17 }, { l : 'yy' , d : C.Y }, ]; const Tl = T.length; let result; let out; let isFuture; for (let i = 0 ; i < Tl; i += 1 ) { let t = T[i]; if (t.d) { result = isFrom ? d(input).diff(instance, t.d, true ) : instance.diff(input, t.d, true ); } const abs = (o.rounding || Math .round)(Math .abs(result)); isFuture = result > 0 ; if (abs <= t.r || !t.r) { if (abs <= 1 && i > 0 ) t = T[i - 1 ]; const format = loc[t.l]; if (typeof format === 'string' ) { out = format.replace('%d' , abs); } else { out = format(abs, withoutSuffix, t.l, isFuture); } break ; } } if (withoutSuffix) return out; const pastOrFuture = isFuture ? loc.future : loc.past; if (typeof pastOrFuture === 'function' ) { return pastOrFuture(out); } return pastOrFuture.replace('%s' , out); };
全部源码过长,具体分析请移步 Github 。
timezone Timezone
插件添加了 dayjs.tz
、 .tz
、 .tz.guess
、 .tz.setDefault
的 API
,在时区之间解析或显示。
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import timezone from 'dayjs/plugin/timezone' ;import utc from 'dayjs/plugin/utc' ;dayjs.extend(timezone); dayjs.extend(utc); dayjs.tz('2014-06-01 12:00' , 'America/New_York' ); dayjs('2014-06-01 12:00' ).tz('America/New_York' ); dayjs.tz.guess(); dayjs.tz.setDefault('America/New_York' );
相关知识 时区 要理解时区,先看下图,我们在本系列第一节的基本概念中讲到过,格林尼治平均时间(Greenwich Mean Time
,GMT
)是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时。
全球 360
° ,每 15°
划分一个理论时区
,总共 24
个,理论时区以被 15
整除的经线为中心,向东西两侧延伸 7.5
度,即每 15°
划分一个时区。理论时区的时间采用其中央经线(或标准经线)的地方时。所以每差一个时区,区时相差一个小时,相差多少个时区,就相差多少个小时。另外,为了避开国界线,有的时区的形状并不规则,而且比较大的国家以国家内部行政分界线为时区界线,这是实际时区
,即法定时区
,可见上图。
协调世界时 如果时间是以协调世界时(UTC)
表示,则在时间后面直接加上一个“Z
”(不加空格)。“Z
”是协调世界时中 0
时区的标志。因此,“09:30 UTC
”就写作“09:30Z
”或是“0930Z
”。“14:45:15 UTC
”则为“14:45:15Z
”或“144515Z
”。
UTC
时间也被叫做祖鲁时间,因为在北约音标字母中用“Zulu
”表示“Z
”。
UTC 偏移量 UTC
偏移量用以下形式表示:±[hh]:[mm]
、±[hh][mm]
、或者 ±[hh]
。如果所在区时比协调世界时早 1
个小时(例如柏林冬季时间),那么时区标识应为“+01:00
”、“+0100
”或者直接写作“+01
”。这也同上面的“Z
”一样直接加在时间后面。
“UTC+8
“表示当协调世界时(UTC
)时间为凌晨 2
点的时候,当地的时间为 2+8
点,即早上 10
点。
缩写 时区通常都用字母缩写形式来表示,例如“EST
、WST
、CST
”等。但是它们并不是 ISO 8601
标准的一部分,不应单独用它们作为时区的标识。
源码分析 这里的代码实现的比较复杂而且难以理解,主要原因是为了计算准确的 UTC
偏移量进行 bugfix,加入了 Intl 的 API
。
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 const dtfCache = {};const getDateTimeFormat = (timezone, options = {} ) => { const timeZoneName = options.timeZoneName || 'short' ; const key = `${timezone} |${timeZoneName} ` ; let dtf = dtfCache[key]; if (!dtf) { dtf = new Intl .DateTimeFormat('en-US' , { hour12: false , timeZone: timezone, year: 'numeric' , month: '2-digit' , day: '2-digit' , hour: '2-digit' , minute: '2-digit' , second: '2-digit' , timeZoneName, }); dtfCache[key] = dtf; } return dtf; };
最核心的方法是 tz
方法,这个方法返回一个设置好 UTC
偏移量的新的 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 proto.tz = function (timezone = defaultTimezone, keepLocalTime ) { const oldOffset = this .utcOffset(); const target = this .toDate().toLocaleString('en-US' , { timeZone : timezone }); const diff = Math .round((this .toDate() - new Date (target)) / 1000 / 60 ); let ins = d(target) .$set (MS, this.$ms) .utcOffset(localUtcOffset - diff, true); // 如果需要保持本地时间,就再修正偏移 if (keepLocalTime) { const newOffset = ins.utcOffset(); ins = ins.add(oldOffset - newOffset, MIN); } ins.$x.$timezone = timezone; return ins; };
全部源码过长,具体分析请移步 Github 。
utc Day.js
默认使用用户本地时区来解析和展示时间。 如果想要使用 UTC
模式来解析和展示时间,可以使用 dayjs.utc()
而不是 dayjs()
。
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import utc from 'dayjs/plugin/utc' ;dayjs.extend(utc); dayjs().format(); dayjs.utc().format(); dayjs().utc().format(); dayjs.utc().isUTC(); dayjs.utc().local().format(); dayjs.utc('2018-01-01' , 'YYYY-MM-DD' );
源码分析 代码中区分 UTC
模式和本地模式的标志就是 config
配置对象中的 utc
属性。如下述代码所示,不管是 dayjs
函数对象还是实例上的 utc
方法,都是用 utc: true
来进行区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 dayjs.utc = function (date ) { const cfg = { date, utc : true , args : arguments }; return new Dayjs(cfg); }; proto.utc = function (keepLocalTime ) { const ins = dayjs(this .toDate(), { locale : this .$L, utc : true }); if (keepLocalTime) { return ins.add(this .utcOffset(), MIN); } return ins; };
同样的原理,如果实例要启用本地时间,将 utc: false
即可。
1 2 3 4 5 6 7 8 proto.local = function ( ) { return dayjs(this .toDate(), { locale : this .$L, utc : false }); };
对于其他方法的扩展,基本上都是修正本地时间与 UTC
时间的偏移后再调用 oldProto
上的同名方法,不再赘述。
全部源码过长,具体分析请移步 Github 。
本篇内容完成,下一篇中将继续分析剩余的简单插件。
前端记事本,不定期更新,欢迎关注!