上一篇 dayjs 源碼解析(二)(目錄結構)介紹了 dayjs 的源碼目錄結構。接下來,本篇將解讀一下 index.js 中的 dayjs 構造函數。javascript
// d 是否爲 Dayjs 的實例對象 const isDayjs = d => d instanceof Dayjs // dayjs 函數,用於返回新的 Dayjs 實例對象的函數 const dayjs = (date, c) => { // 若date 爲 Dayjs 的實例對象,則返回克隆的 Dayjs 實例對象(immutable) if (isDayjs(date)) { return date.clone() } const cfg = c || {} cfg.date = date return new Dayjs(cfg) } // Dayjs 類 class Dayjs { //... }
翻看 第一篇 中介紹的 api,有哪些 api 會用到 dayjs() 構造函數呢?在構造一個新的 Dayjs 實例對象的時候會用到 dayjs()。
即 「解析類中的構造和克隆」java
首先看參數 date。在上面的 api 中,咱們能夠看到,date 參數能夠有五種類型,這五種類型能夠分爲兩類:segmentfault
1、非 Dayjs 實例對象:api
2、Dayjs 實例對象app
// d 是否爲 Dayjs 的實例對象 const isDayjs = d => d instanceof Dayjs // dayjs 函數,用於返回新的 Dayjs 實例對象的函數 const dayjs = (date, c) => { // 若date 爲 Dayjs 的實例對象,則返回克隆的 Dayjs 實例對象(immutable) if (isDayjs(date)) { return date.clone() } const cfg = c || {} cfg.date = date return new Dayjs(cfg) } // Dayjs 類 class Dayjs { //... } // 調用 dayjs 函數 dayjs('2018-7-1')
此時,c 參數(後面會講 c 參數的做用)爲空,因此 cfg 變量被賦值爲一個 空對象{}
。函數
而後將傳入的 date 參數賦值給 cfg 對象的 date 屬性。最後將 cfg 傳入 Dayjs 類的構造函數,生成一個 Dayjs 對象,做爲 dayjs() 函數的返回值給返回了。this
因此,最終 dayjs() 函數返回的是一個 Dayjs 實例對象。spa
// d 是否爲 Dayjs 的實例對象 const isDayjs = d => d instanceof Dayjs // dayjs 函數,用於返回新的 Dayjs 實例對象的函數 const dayjs = (date, c) => { // 若date 爲 Dayjs 的實例對象,則返回克隆的 Dayjs 實例對象(immutable) if (isDayjs(date)) { return date.clone() } const cfg = c || {} cfg.date = date return new Dayjs(cfg) } // Dayjs 類 class Dayjs { //... } // 調用 dayjs 函數 dayjs(dayjs())
由於傳入的 date 參數爲 Dayjs 實例對象,因此 isDayjs(date) 返回 true,而後調用 date.clone() 方法。
經過閱讀 Dayjs 類的代碼,可知道,clone() 不是掛載到 Dayjs 實例對象上的,而是掛載到 Dayjs 的原型對象上的(date 經過原型鏈找到 clone() 方法,而後進行調用):eslint
class Dayjs { //...other code clone() { return wrapper(this.toDate(), this) } // 轉換爲新的原生的 JavaScript Date 對象 toDate() { return new Date(this.$d) } //...other code }
調用 clone() 方法時,會先調用 this.toDate() 方法。this.toDate() 方法返回一個新的 JavaScript 原生的 Date 實例對象(其中的 this.$d 爲 date 參數中的 JavaScript 原生的 Date 實例對象,在下一篇 Dayjs 類 中會講到)。unix
而後將這個新的 Date 實例對象以及 this(date 參數)做爲 wrapper 的參數,調用 wrapper() 函數:
// date 爲 JavaScript 原生的 Date 對象;instance 爲 Dayjs 實例對象 const wrapper = (date, instance) => dayjs(date, { locale: instance.$L })
在 wrapper() 函數中,又反過來調用 dayjs() 函數。在這裏,傳入了 date(date 爲 JavaScript 原生的 Date 實例對象)和 c(c 爲一個帶有 locale 屬性的對象,locale 的值爲 Dayjs 實例對象的 $L 的值)
最後再返回來看 dayjs() 函數:
// dayjs 函數,用於返回新的 Dayjs 實例對象的函數 const dayjs = (date, c) => { // 若date 爲 Dayjs 的實例對象,則返回克隆的 Dayjs 實例對象(immutable) if (isDayjs(date)) { return date.clone() } const cfg = c || {} cfg.date = date return new Dayjs(cfg) } 此時傳入的 date 參數爲 JavaScript 原生的 Date 實例對象,c 爲帶有 locale 屬性的對象。 最後,dayjs() 調用,返回了一個新的 Dayjs 實例對象。
因此,當 date 參數爲 Dayjs 實例對象時,在 dayjs() 函數內部,最後又會調用 dayjs() 函數,此時傳入 dayjs() 函數的參數爲兩個:
此時和 date 參數爲 「非 Dayjs 實例對象」 時是同樣的執行,只不過多了一個參數 c 罷了。
經過上面對參數 date 的分析知道了參數 c 實際上是當 date 參數爲 Dayjs 實例對象時,最後又會調用 dayjs() 函數,此時纔會傳入參數 c。
參數 c 爲一個包含 locale 屬性的對象(locale 的值爲上一個 Dayjs 實例對象所用的語言,是一個字符串類型)
const dayjs = (date, c) => { if (isDayjs(date)) { return date.clone() } const cfg = c || {} cfg.date = date return new Dayjs(cfg) // eslint-disable-line no-use-before-define } const wrapper = (date, instance) => dayjs(date, { locale: instance.$L }) class Dayjs { constructor(cfg) { this.parse(cfg) // for plugin } parse(cfg) { this.$d = parseDate(cfg.date) this.init(cfg) } init(cfg) { this.$y = this.$d.getFullYear() this.$M = this.$d.getMonth() this.$D = this.$d.getDate() this.$W = this.$d.getDay() this.$H = this.$d.getHours() this.$m = this.$d.getMinutes() this.$s = this.$d.getSeconds() this.$ms = this.$d.getMilliseconds() this.$L = this.$L || parseLocale(cfg.locale, null, true) || L } // eslint-disable-next-line class-methods-use-this $utils() { return Utils } isValid() { return !(this.$d.toString() === 'Invalid Date') } isLeapYear() { return ((this.$y % 4 === 0) && (this.$y % 100 !== 0)) || (this.$y % 400 === 0) } $compare(that) { return this.valueOf() - dayjs(that).valueOf() } isSame(that) { return this.$compare(that) === 0 } isBefore(that) { return this.$compare(that) < 0 } isAfter(that) { return this.$compare(that) > 0 } year() { return this.$y } month() { return this.$M } day() { return this.$W } date() { return this.$D } hour() { return this.$H } minute() { return this.$m } second() { return this.$s } millisecond() { return this.$ms } unix() { return Math.floor(this.valueOf() / 1000) } valueOf() { // timezone(hour) * 60 * 60 * 1000 => ms return this.$d.getTime() } startOf(units, startOf) { // startOf -> endOf const isStartOf = !Utils.isUndefined(startOf) ? startOf : true const unit = Utils.prettyUnit(units) const instanceFactory = (d, m) => { const ins = wrapper(new Date(this.$y, m, d), this) return isStartOf ? ins : ins.endOf(C.D) } const instanceFactorySet = (method, slice) => { const argumentStart = [0, 0, 0, 0] const argumentEnd = [23, 59, 59, 999] return wrapper(this.toDate()[method].apply( // eslint-disable-line prefer-spread this.toDate(), isStartOf ? argumentStart.slice(slice) : argumentEnd.slice(slice) ), this) } switch (unit) { case C.Y: return isStartOf ? instanceFactory(1, 0) : instanceFactory(31, 11) case C.M: return isStartOf ? instanceFactory(1, this.$M) : instanceFactory(0, this.$M + 1) case C.W: return isStartOf ? instanceFactory(this.$D - this.$W, this.$M) : instanceFactory(this.$D + (6 - this.$W), this.$M) case C.D: case C.DATE: return instanceFactorySet('setHours', 0) case C.H: return instanceFactorySet('setMinutes', 1) case C.MIN: return instanceFactorySet('setSeconds', 2) case C.S: return instanceFactorySet('setMilliseconds', 3) default: return this.clone() } } endOf(arg) { return this.startOf(arg, false) } $set(units, int) { // private set const unit = Utils.prettyUnit(units) switch (unit) { case C.DATE: this.$d.setDate(int) break case C.M: this.$d.setMonth(int) break case C.Y: this.$d.setFullYear(int) break case C.H: this.$d.setHours(int) break case C.MIN: this.$d.setMinutes(int) break case C.S: this.$d.setSeconds(int) break case C.MS: this.$d.setMilliseconds(int) break default: break } this.init() return this } set(string, int) { return this.clone().$set(string, int) } add(number, units) { number = Number(number) // eslint-disable-line no-param-reassign const unit = Utils.prettyUnit(units) const instanceFactory = (u, n) => { const date = this.set(C.DATE, 1).set(u, n + number) return date.set(C.DATE, Math.min(this.$D, date.daysInMonth())) } if (unit === C.M) { return instanceFactory(C.M, this.$M) } if (unit === C.Y) { return instanceFactory(C.Y, this.$y) } let step switch (unit) { case C.MIN: step = C.MILLISECONDS_A_MINUTE break case C.H: step = C.MILLISECONDS_A_HOUR break case C.D: step = C.MILLISECONDS_A_DAY break case C.W: step = C.MILLISECONDS_A_WEEK break case C.S: step = C.MILLISECONDS_A_SECOND break default: // ms step = 1 } const nextTimeStamp = this.valueOf() + (number * step) return wrapper(nextTimeStamp, this) } subtract(number, string) { return this.add(number * -1, string) } format(formatStr) { const str = formatStr || C.FORMAT_DEFAULT const zoneStr = Utils.padZoneStr(this.$d.getTimezoneOffset()) const locale = this.$locale() const { weekdays, months } = locale const getShort = (arr, index, full, length) => ( (arr && arr[index]) || full[index].substr(0, length) ) return str.replace(C.REGEX_FORMAT, (match) => { if (match.indexOf('[') > -1) return match.replace(/\[|\]/g, '') switch (match) { case 'YY': return String(this.$y).slice(-2) case 'YYYY': return String(this.$y) case 'M': return String(this.$M + 1) case 'MM': return Utils.padStart(this.$M + 1, 2, '0') case 'MMM': return getShort(locale.monthsShort, this.$M, months, 3) case 'MMMM': return months[this.$M] case 'D': return String(this.$D) case 'DD': return Utils.padStart(this.$D, 2, '0') case 'd': return String(this.$W) case 'dd': return getShort(locale.weekdaysMin, this.$W, weekdays, 2) case 'ddd': return getShort(locale.weekdaysShort, this.$W, weekdays, 3) case 'dddd': return weekdays[this.$W] case 'H': return String(this.$H) case 'HH': return Utils.padStart(this.$H, 2, '0') case 'h': case 'hh': if (this.$H === 0) return 12 return Utils.padStart(this.$H < 13 ? this.$H : this.$H - 12, match === 'hh' ? 2 : 1, '0') case 'a': return this.$H < 12 ? 'am' : 'pm' case 'A': return this.$H < 12 ? 'AM' : 'PM' case 'm': return String(this.$m) case 'mm': return Utils.padStart(this.$m, 2, '0') case 's': return String(this.$s) case 'ss': return Utils.padStart(this.$s, 2, '0') case 'SSS': return Utils.padStart(this.$ms, 3, '0') case 'Z': return zoneStr default: // 'ZZ' return zoneStr.replace(':', '') } }) } diff(input, units, float) { const unit = Utils.prettyUnit(units) const that = dayjs(input) const diff = this - that let result = Utils.monthDiff(this, that) switch (unit) { case C.Y: result /= 12 break case C.M: break case C.Q: result /= 3 break case C.W: result = diff / C.MILLISECONDS_A_WEEK break case C.D: result = diff / C.MILLISECONDS_A_DAY break case C.H: result = diff / C.MILLISECONDS_A_HOUR break case C.MIN: result = diff / C.MILLISECONDS_A_MINUTE break case C.S: result = diff / C.MILLISECONDS_A_SECOND break default: // milliseconds result = diff } return float ? result : Utils.absFloor(result) } daysInMonth() { return this.endOf(C.M).$D } $locale() { // get locale object return Ls[this.$L] } locale(preset, object) { const that = this.clone() that.$L = parseLocale(preset, object, true) return that } clone() { return wrapper(this.toDate(), this) } toDate() { return new Date(this.$d) } toArray() { return [ this.$y, this.$M, this.$D, this.$H, this.$m, this.$s, this.$ms ] } toJSON() { return this.toISOString() } toISOString() { // ie 8 return // new Dayjs(this.valueOf() + this.$d.getTimezoneOffset() * 60000) // .format('YYYY-MM-DDTHH:mm:ss.SSS[Z]') return this.toDate().toISOString() } toObject() { return { years: this.$y, months: this.$M, date: this.$D, hours: this.$H, minutes: this.$m, seconds: this.$s, milliseconds: this.$ms } } toString() { return this.$d.toUTCString() } }