1 为什么要学习dayjs
dayjs
作为当前最流行的日期处理javascript
库之一。
无论是ElementUI
、AntDesign
都选择它作为日期组件的默认库,足以得见它在大家心目中的地位!
废话不多说,进入正题。
2 文件目录分析
打开代码的src文件夹,映入眼帘的是如下精简的目录:
locale
和plugin
分别代表多语言和插件,有使用需求才引入,所以可以先当做不存在。
剩下的三个文件就是dayjs
的核心源码,我真的是一点都没有标题党!!
src
│ constant.js // 常量文件
│ index.js // 入口文件
│ utils.js // 工具类库
│
├─locale
└─plugin
3 源码分析
鉴于该项目源码真的不多,我就把这三个文件逐一分析一下。
3.1 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
// 英文的单位简写
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时间格式,中间带T,结尾带Z
// T表示时间的起始点,Z表示标准时区
export const FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ'
// 当new Date('error') 传入非标准日期参数时
// 会返回Invalid Date
export const INVALID_DATE_STRING = 'Invalid Date'
// 正则表达式
// 这个是用来字符串转日期的正则表达式,
// 用括号包裹的部分分别是年、月、日、时、分、秒、毫秒
export const REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\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
正则表达式可能大家看的会有点懵,下面举两个简单的例子,大家可能就明白了:
const REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/
const matches='2023-10-01T10:10:10.100'.match(REGEX_PARSE);
console.log(matches);
// 返回的matches是一个数组
[
'2023-10-01T10:10:10.100', //第一个元素代表参与匹配的字符串
'2023', // 这个是正则中第一个括号匹配到的内容,也就是年,下面同理
'10', // 这个是月
'01', // 这个是日
'10', // 这个是时
'10', // 这个是分
'10', // 这个是秒
'100', // 这个是毫秒
index: 0,
input: '2023-10-01T10:10:10.100',
groups: undefined
]
// 有了这些信息,就能够生成一个dayjs对象了
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
const matches='[YY]YYYY-MM-DDTHH:mm:ss.SSS'.match(REGEX_FORMAT)
console.log(matches);
// 返回的结果是一个数组,
// 代表的是哪些片段符合该正则
// dayjs中逻辑是遇到中括号包裹的内容,
// 会将中括号去掉,里面的内容原样返回
// 其余的就会转成对应的年月日等信息
[
'[YY]', 'YYYY',
'MM', 'DD',
'HH', 'mm',
'ss', 'SSS'
]
console.log(dayjs().format('[YY]YYYY-MM-DDTHH:mm:ss.SSS'));
//输出 YY2023-10-04T17:46:41.184
3.2 utils.js
import * as C from './constant.js'
// 给字符串补充前置内容
// 比如给月份补充0,
// padStart(1,2,'0')--->'01'
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}`
}
// 返回时区字符串
// 入参是一个dayjs对象
// 如果是中国时区,会
// 返回+08:00这样的结果
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$/, '')
)
}
// 判断是否undefined
const isUndefined = s => s === undefined
export default {
s: padStart,
z: padZoneStr,
m: monthDiff,
a: absFloor,
p: prettyUnit,
u: isUndefined
}
3.3 index.js
接下来就是最重要的入口文件了,让我们来看看吧!
由于多语言不是我们关心的重点,部分代码我会省略掉
import * as C from './constant.js'
import en from './locale/en.js'
import U from './utils.js'
// 默认语言为英文
let L = 'en'
const Ls = {}
// 设置多语言翻译内容
Ls[L] = en
// 函数,用来判断是不是Dayjs对象
const isDayjs = d => d instanceof Dayjs
// 获取语言环境信息:
// 返回'en','zh'这样的字符串
const parseLocale = (preset, object, isLocal) => {
// ....省略
}
// 函数:创建dayjs实例
// 我们最常用的dayjs()就是这个函数
const dayjs = function(date, c) {
// date如果传入的是dayjs实例
// 就返回一个新实例
// clone是用来复制dayjs
if (isDayjs(date)) {
return date.clone()
}
// 否则new一个dayjs实例
// 具体的看下面解析Dayjs类
const cfg = typeof c === 'object' ? c : {}
cfg.date = date
cfg.args = arguments
return new Dayjs(cfg)
}
// 也是创建一个dayjs实例
// 不同的是在第二个参数传入一个dayjs实例
// 会使用第二个参数的语言时区等信息
const wrapper = (date, instance) =>
dayjs(date, {
locale: instance.$L,
utc: instance.$u,
x: instance.$x,
$offset: instance.$offset
})
// 封装工具类,也可以给插件使用
const Utils = U
Utils.l = parseLocale
Utils.i = isDayjs
Utils.w = wrapper
// 返回一个Date
// 可以根据字符串,时间戳等信息
// 创建Date对象
const parseDate = cfg => {
const { date, utc } = cfg
// null是无效的
if (date === null) return new Date(NaN)
// 不传默认是当天
if (Utils.u(date)) return new Date()
// 传入Date则返回一个新的一样的Date
if (date instanceof Date) return new Date(date)
// 传入的是字符串,并且不带'Zz'
// 会根据REGEX_PARSE正则获取
// 字符串的年月日等信息,
// 新建一个Date
if (typeof date === 'string' && !/Z$/i.test(date)) {
const d = date.match(C.REGEX_PARSE)
if (d) {
const m = d[2] - 1 || 0
const ms = (d[7] || '0').substring(0, 3)
if (utc) {
return new Date(
Date.UTC(d[1], m, d[3] || 1, d[4] || 0, d[5] || 0, d[6] || 0, ms)
)
}
return new Date(d[1], m, d[3] || 1, d[4] || 0, d[5] || 0, d[6] || 0, ms)
}
}
// 其他情况,比如时间戳,带Zz的字符串
return new Date(date)
}
// 重点,Dayjs类
class Dayjs {
// 构造函数
// 确定实例的语言环境
constructor(cfg) {
// $L会是en,zh这样的字符串,
this.$L = parseLocale(cfg.locale, null, true)
// 见下一行
this.parse(cfg)
}
parse(cfg) {
// $d存放的是Date对象
this.$d = parseDate(cfg)
this.$x = cfg.x || {}
// 见下一行
this.init()
}
// 初始化,dayjs其实就是
// 这一个个部分组成的
init() {
// $d就是一个Date对象
// 下面其实就是把
// 年月日时分秒各个部分取出来
// 方便其他方法使用
const { $d } = this
this.$y = $d.getFullYear()
this.$M = $d.getMonth()
this.$D = $d.getDate()
this.$W = $d.getDay()
this.$H = $d.getHours()
this.$m = $d.getMinutes()
this.$s = $d.getSeconds()
this.$ms = $d.getMilliseconds()
}
// 到这里为止,
// dayjs实例的属性都已经初始化好了
// 其实就这些:
// $L 语言
// $d Date对象
// $y 年
// $M 月
// $D 日
// $W 周几
// $H 时
// $m 分
// $s 秒
// $ms 毫秒
// console.log(dayjs()); 大家可以打印看看
// 剩下的部分都是dayjs的方法
// 比如 dayjs().format()
// dayjs().isBefore()
// 这里没写到的方法,
// dayjs都通过插件的形式提供
// 获取工具函数
$utils() {
return Utils
}
// 是否是有效的日期对象
isValid() {
return !(this.$d.toString() === C.INVALID_DATE_STRING)
}
// 是否相同,
// 可以指定单位
// 只对比到单位
isSame(that, units) {
const other = dayjs(that)
return this.startOf(units) <= other && other <= this.endOf(units)
}
// 是否在传入的日期之后
isAfter(that, units) {
return dayjs(that) < this.startOf(units)
}
// 是否在传入的日期之前
isBefore(that, units) {
return this.endOf(units) < dayjs(that)
}
// 日期的get和set函数
// 举个例子:
// 获取秒数:$g(,'second') =dayjs().second()
// 设置秒数:$g(1,'','second') =dayjs().second(1)
$g(input, get, set) {
if (Utils.u(input)) return this[get]
return this.set(set, input)
}
// 返回毫秒时间戳
unix() {
return Math.floor(this.valueOf() / 1000)
}
// 返回微秒时间戳
valueOf() {
return this.$d.getTime()
}
// 获取对应单位的最近时间
// 单位传月份,返回本月第一天
// 单位传天,返回今天零点
// 单位传小时,返回当前小时零分零秒
startOf(units, startOf) {
// 第二个参数startOf传false代表取endOf
const isStartOf = !Utils.u(startOf) ? startOf : true
// 单位转换:比如M转为month
const unit = Utils.p(units)
// 创建只精确到日的dayjs对象,
// 参数是日和月
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)
}
// 用来设置日期的时分秒毫秒信息
// method 是方法名:比如setHours
// slice 是argument截取起始点
// 连起来就是setHours(0,0,0,0)
// setMinutes(59,59,999)
const instanceFactorySet = (method, slice) => {
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
)
}
const { $W, $M, $D } = this
const utcPad = `set${this.$u ? 'UTC' : ''}`
switch (unit) {
case C.Y:
// 如果是年,返回1月1日,否则返回12月31日
return isStartOf ? instanceFactory(1, 0) : instanceFactory(31, 11)
case C.M:
// 如果是月,返回1号,否则返回本月最后一天
return isStartOf ? instanceFactory(1, $M) : instanceFactory(0, $M + 1)
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)
}
case C.D:
case C.DATE:
// 设置时分秒毫秒
return instanceFactorySet(`${utcPad}Hours`, 0)
case C.H:
// 设置分秒毫秒
return instanceFactorySet(`${utcPad}Minutes`, 1)
case C.MIN:
// 设置秒毫秒
return instanceFactorySet(`${utcPad}Seconds`, 2)
case C.S:
// 设置时毫秒
return instanceFactorySet(`${utcPad}Milliseconds`, 3)
default:
return this.clone()
}
}
// 与startOf相对
// 获取对应单位的最远时间
// 单位传月份,返回本月最后一天
// 单位传天,返回今天23:59:59
// 单位传小时,返回当前小时59:59
endOf(arg) {
return this.startOf(arg, false)
}
// 给内部使用的set方法
$set(units, int) {
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
if (unit === C.M || unit === C.Y) {
// 设置年月
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
}
// 设置
set(string, int) {
return this.clone().$set(string, int)
}
// 获取
get(unit) {
return this[Utils.p(unit)]()
}
// 给日期增加
add(number, units) {
number = Number(number)
const unit = Utils.p(units)
const instanceFactorySet = n => {
const d = dayjs(this)
return Utils.w(d.date(d.date() + Math.round(n * number)), this)
}
// 年月 使用set方法相加
if (unit === C.M) {
return this.set(C.M, this.$M + number)
}
// 年月 使用set方法相加
if (unit === C.Y) {
return this.set(C.Y, this.$y + number)
}
// 天、周使用instanceFactorySet相加
if (unit === C.D) {
return instanceFactorySet(1)
}
// 天、周使用instanceFactorySet相加
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)
}
// 减少日期
subtract(number, string) {
return this.add(number * -1, string)
}
// 格式化日期
// 这里是用到string.replace(regExp,func)
// 使用正则匹配要替换的项,然后替换
// 比如正则匹配到YYYY,替换成年份
format(formatStr) {
const locale = this.$locale()
//判断是否有效
if (!this.isValid()) return locale.invalidDate || C.INVALID_DATE_STRING
// 默认格式为YYYY-MM-DDTHH:mm:ssZ
const str = formatStr || C.FORMAT_DEFAULT
// 时区内容
const zoneStr = Utils.z(this)
const { $H, $m, $M } = this
const { weekdays, months, meridiem } = locale
const getShort = (arr, index, full, length) =>
(arr && (arr[index] || arr(this, str))) || full[index].slice(0, length)
const get$H = num => Utils.s($H % 12 || 12, num, '0')
//判断上下午
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: Utils.s(this.$y, 4, '0'),
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
}
// 核心其实就这一句,替换文本
return str.replace(
C.REGEX_FORMAT,
(match, $1) => $1 || matches[match] || zoneStr.replace(':', '')
)
}
// 时区偏移分钟数
// 东八区就是480分钟
utcOffset() {
return -Math.round(this.$d.getTimezoneOffset() / 15) * 15
}
// 日期差,float代表是否保留小数
diff(input, units, float) {
const unit = Utils.p(units)
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)
}
// 本月有几天
daysInMonth() {
return this.endOf(C.M).$D
}
// 获取多语言配置
$locale() {
return Ls[this.$L]
}
// 设置多语言环境
locale(preset, object) {
if (!preset) return this.$L
const that = this.clone()
const nextLocaleName = parseLocale(preset, object, true)
if (nextLocaleName) that.$L = nextLocaleName
return that
}
// 复制
clone() {
return Utils.w(this.$d, this)
}
// 转为Date对象
toDate() {
return new Date(this.valueOf())
}
// 转ISO字符串
toJSON() {
return this.isValid() ? this.toISOString() : null
}
// 转ISO字符串
toISOString() {
return this.$d.toISOString()
}
// 转UTC字符串
toString() {
return this.$d.toUTCString()
}
}
// 让dayjs函数拥有Dayjs类里的方法
const proto = Dayjs.prototype
dayjs.prototype = proto
// 提供修改年月日时分秒的函数
// 比如dayjs().second()获取秒数
// dayjs().second(10)设置秒数
;[
['$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) {
return this.$g(input, g[0], g[1])
}
})
// 扩展插件
// 可以看出来插件都是一个函数
// 插件的回调参数有Dayjs, dayjs
dayjs.extend = (plugin, option) => {
// 防止重复安装插件,用$i标记
if (!plugin.$i) {
plugin(option, Dayjs, dayjs)
plugin.$i = true
}
return dayjs
}
// dayjs的一些属性
dayjs.locale = parseLocale
dayjs.isDayjs = isDayjs
dayjs.unix = timestamp => dayjs(timestamp * 1e3)
dayjs.en = Ls[L]
dayjs.Ls = Ls
dayjs.p = {}
export default dayjs
这个入口文件还是比较长的,总结一下:
dayjs
其实没什么特别的,它只是重新封装了日期类,并且附上常用的日期处理方法,另外提供了扩展插件的方法。
4 结束
希望大家看完本篇文章能够有所收获!
如有疑问,欢迎评论区沟通交流!