前言

上一篇 dayjs 源码解析(二)(目录结构)介绍了 dayjs 的源码目录结构。接下来,本篇将解读一下 index.js 中的 dayjs 构造函数。

dayjs 构造函数


// 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()。
即 “解析类中的构造克隆

clipboard.png

dayjs 函数的参数 date 和 c

参数 date

首先看参数 date。在上面的 api 中,我们可以看到,date 参数可以有五种类型,这五种类型可以分为两类:

一、非 Dayjs 实例对象:

  • 不传参,即 date 为undefined
  • date 为 ISO 8601 标准格式的字符串
  • date 为 unix 时间戳
  • date 为 JavaScript 原生的 Date 实例对象

二、Dayjs 实例对象

  • date 为 Dayjs 实例对象

当 date 参数为 “非 Dayjs 实例对象” 时


// 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() 函数的返回值给返回了。

所以,最终 dayjs() 函数返回的是一个 Dayjs 实例对象。

当 date 参数为 “Dayjs 实例对象” 时


// 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() 方法,然后进行调用):

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 类 中会讲到)。

然后将这个新的 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() 函数的参数为两个:

  1. date(新的原生的 JavaScript Date 实例对象)
  2. c(包含 locale 属性的对象,locale 的值为上一个 Dayjs 实例对象所用的语言,是一个字符串类型)

此时和 date 参数为 “非 Dayjs 实例对象” 时是一样的执行,只不过多了一个参数 c 罢了。

参数 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()
  }
}

下一篇:dayjs 源码解析(四)(Dayjs 类)


hileix
246 声望16 粉丝