dayjs源码解析(三):插件(上)

632 阅读8分钟

接上篇 —— dayjs 源码解析(二):Dayjs 类 —— 继续解析 dayjs 的源码。

从本篇开始,分三篇解析 dayjs 源码中插件功能的部分,也就是 src/plugin 目录下的文件。

目录如下:

  1. dayjs 源码解析(一):概念、locale、constant、utils
  2. dayjs 源码解析(二):Dayjs 类
  3. dayjs 源码解析(三):插件(上)
  4. dayjs 源码解析(四):插件(中)
  5. dayjs 源码解析(五):插件(下)

插件加载

src/index.js 写了加载插件的方法 dayjs.extend,很显然插件是一个函数,接收三个参数 optionDayjs 类dayjs 函数对象

并给 plugin 函数对象设了个 $i 来保证单例。

/**
 * @description: 挂载插件
 * @param {Function} plugin 插件
 * @param {*} option 插件选项
 * @return {dayjs function} 返回 dayjs 函数对象
 */
dayjs.extend = (plugin, option) => {
  // 同一个插件只挂载一次
  if (!plugin.$i) {
    plugin(option, Dayjs, dayjs); //挂载
    plugin.$i = true;
  }
  return dayjs;
};

下面是大部分插件的标准写法,可以新加方法也可以覆盖原有的方法:

/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} d dayjs函数对象
 */
export default (o, c, d) => {
  const proto = c.prototype;
  /**
   * @description: demo
   * @return {Boolean}
   */
  proto.demo = function (args) {};
};

is 系列

首选先从一些简单的插件开始解析。is 开头的插件都是用来判断的,返回一个 Boolean 值来表示是否。

isLeapYear

判断是否为闰年比较简单,必须同时满足以下两个条件:

  1. 能被 4 整除且不能被 100 整除;
  2. 能被 400 整除;
/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 */
export default (o, c) => {
  const proto = c.prototype;
  /**
   * @description: 返回一个 boolean 来展示一个 Day.js 对象的年份是不是闰年。
   * @return {Boolean}
   */
  proto.isLeapYear = function () {
    // 判断闰年需要满足的两种情况,1.能被4整除且不能被100整除 2.能被400整除
    return (this.$y % 4 === 0 && this.$y % 100 !== 0) || this.$y % 400 === 0;
  };
};

isMoment

官网上并没有给这个方法的文档,其实想着判断是否为 Moment 的实例就不合理。所以直接判断是否为 Dayjs 的实例。

/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} f dayjs函数对象
 */
export default (o, c, f) => {
  /**
   * @description: 指示对象是不是Dayjs的实例
   * @param {Object} input
   * @return {Boolean}
   */
  f.isMoment = function (input) {
    // 最终用的是判断是不是Dayjs的实例😂
    return f.isDayjs(input);
  };
};

isBetween、isSameOrAfter、isSameOrBefore

三个判断相对早晚的方法实现的原理基本一致。都是组合的 Dayjs.prototype.isSameDayjs.prototype.isBeforeDayjs.prototype.isAfter,很简单。

特别提到的是 isBetween,用的是数学上的 ()[] 来表示两个的开闭性的。

/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 */
export default (o, c) => {
  /**
   * @description: 返回一个 boolean 来实例是否和一个时间相同或在该时间之前。
   * @param {Dayjs} that 另一个Dayjs实例
   * @param {String} unit 时间单位
   * @return {Boolean}
   */
  c.prototype.isSameOrAfter = function (that, units) {
    // 调用了 isSame 和 isAfter
    return this.isSame(that, units) || this.isAfter(that, units);
  };
};
/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 */
export default (o, c) => {
  /**
   * @description: 返回一个 boolean 来实例是否和一个时间相同或在该时间之后。
   * @param {Dayjs} that 另一个Dayjs实例
   * @param {String} unit 时间单位
   * @return {Boolean}
   */
  c.prototype.isSameOrBefore = function (that, units) {
    // 调用了 isSame 和 isBefore
    return this.isSame(that, units) || this.isBefore(that, units);
  };
};
/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} d dayjs函数对象
 */
export default (o, c, d) => {
  /**
   * @description: 返回一个 boolean 来展示一个时间是否介于两个时间之间
   * @param {String|Dayjs} a 时间
   * @param {String|Dayjs} b 时间
   * @param {String} u unit 时间单位
   * @param {String} i include 区间开闭性 () (] [] [)
   * @return {Boolean} 指示时间是否介于两个时间之间
   */
  c.prototype.isBetween = function (a, b, u, i) {
    // 实例化两端时间
    const dA = d(a);
    const dB = d(b);
    // 判断两端开闭性
    i = i || '()';
    const dAi = i[0] === '(';
    const dBi = i[1] === ')';

    // 利用原型上的 isAfter 和 isBefore 实现 isBetween 判断
    return (
      ((dAi ? this.isAfter(dA, u) : !this.isBefore(dA, u)) &&
        (dBi ? this.isBefore(dB, u) : !this.isAfter(dB, u))) ||
      ((dAi ? this.isBefore(dA, u) : !this.isAfter(dA, u)) &&
        (dBi ? this.isAfter(dB, u) : !this.isBefore(dB, u)))
    );
  };
};

isToday、isTomorrow、isYesterday

这三个判断是否在昨天、今天和明天的方法实现的原理也是一样的。都是用当前实例被比较日的实例,同时格式化成 YYYY-MM-DD 的形式,然后比较字符串是否相同。

/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} d dayjs函数对象
 */
export default (o, c, d) => {
  const proto = c.prototype;
  /**
   * @description: 判断当前 Day.js 实例是否是今天。
   * @return {Boolean}
   */
  proto.isToday = function () {
    const comparisonTemplate = 'YYYY-MM-DD';
    const now = d();

    // 要比较的两个实例同时输出为 YYYY-MM-DD 格式字符串,相同就代表为同一天
    return this.format(comparisonTemplate) === now.format(comparisonTemplate);
  };
};
/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} d dayjs函数对象
 */
export default (o, c, d) => {
  const proto = c.prototype;
  /**
   * @description: 判断当前 Day.js 对象是否是明天。
   * @return {Boolean}
   */
  proto.isTomorrow = function () {
    const comparisonTemplate = 'YYYY-MM-DD';
    // 新建一个明天的实例
    const tomorrow = d().add(1, 'day');

    // 要比较的两个实例同时输出为 YYYY-MM-DD 格式字符串,相同就代表为同一天
    return (
      this.format(comparisonTemplate) === tomorrow.format(comparisonTemplate)
    );
  };
};
/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} d dayjs函数对象
 */
export default (o, c, d) => {
  const proto = c.prototype;
  /**
   * @description: 判断当前 Day.js 对象是否是昨天。
   * @return {Boolean}
   */
  proto.isYesterday = function () {
    const comparisonTemplate = 'YYYY-MM-DD';
    // 新建一个昨天的实例
    const yesterday = d().subtract(1, 'day');

    // 要比较的两个实例同时输出为 YYYY-MM-DD 格式字符串,相同就代表为同一天
    return (
      this.format(comparisonTemplate) === yesterday.format(comparisonTemplate)
    );
  };
};

week 系列

分析 week 系列的代码前,先来普及下关于“周(week)”这个单位的一些基本知识。

ISO8601 对周做了规定:本年度第一个周四所在的周为本年度的第 1 周。这句话与下面的三个说法等价:

  • 1 月 4 日所在的周四;
  • 本年度第一个至少有 4 天在同一周内的周;
  • 周一在去年 12 月 29 日至今年 1 月 4 日以内的周;

这种定义方式就会产生一个问题,每年的靠近 1 月 1 日的前后几天,在 ISO8601 的周历算法中,可能并不属于所在的那一年。

举个例子,2021 年 1 月 1 日是周五,2021 年第一个周四是 1 月 7 日,所以在 ISO 周历算法中, 1 月 4 日1 月 10 日 这一周才是 2021 年的第 1 周;也就代表着 12 月 28 日1 月 3 日 这一周是 2020 年的第 53 周

ISO8601 还把一周第一天定义为了周一。通过初始周周第一天的定义,把 1 年分为了 52 周或 53 周。

这种对周的算法主要应用于政府和商务的会计年度。

ISO week

src/plugin 中关于 iso 的插件有两个:isoWeekisoWeeksInYear

isoWeek

isoWeek 插件是一个比较大的插件,它在 Dayjs.prototype 上添加和拓展了四个方法:

  • isoWeekYear: 获取实例所在的 ISO 周所在的年;
  • isoWeek: 获取或设置年度的第 ISO 周数
  • isoWeekday: 获取或设置一周的第 ISO 日,范围是 1-7
  • startOf: 扩展 .startOf .endOfAPIs,使其支持单位 isoWeek
import { D, W, Y } from '../../constant';

const isoWeekPrettyUnit = 'isoweek';

/**
 * @description: plugin ISO-8601 基于周的日历
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} d dayjs函数对象
 */
export default (o, c, d) => {
  /**
   * @description: 获取指定年的第一个星期四
   * @param {Number} year 年
   * @param {Boolean} isUtc 是否使用UTC模式
   * @return {Dayjs}
   */
  const getYearFirstThursday = (year, isUtc) => {
    const yearFirstDay = (isUtc ? d.utc : d)().year(year).startOf(Y);
    // 4 减 一月一号的星期几
    let addDiffDays = 4 - yearFirstDay.isoWeekday();
    if (yearFirstDay.isoWeekday() > 4) {
      addDiffDays += 7;
    }
    // 获得了指定年的第一个星期四的实例
    return yearFirstDay.add(addDiffDays, D);
  };

  /**
   * @description: 获取离实例日期最近的星期四
   * @param {Dayjs} ins 实例
   * @return {Dayjs} 返回新实例
   */
  // 4 减 今天的星期几
  const getCurrentWeekThursday = (ins) => ins.add(4 - ins.isoWeekday(), D);

  const proto = c.prototype;

  /**
   * @description: 获取实例所在的 ISO 周所在的年
   * @return {Number}
   */
  proto.isoWeekYear = function () {
    // 获取最近的星期四所在的年
    const nowWeekThursday = getCurrentWeekThursday(this);
    return nowWeekThursday.year();
  };

  /**
   * @description: 获取或设置年度的第 ISO 周数。
   * @param {Number} week ISO周数
   * @return {Number|Dayjs}
   */
  proto.isoWeek = function (week) {
    // setter 算出周差,再加上
    if (!this.$utils().u(week)) {
      return this.add((week - this.isoWeek()) * 7, D);
    }
    // getter
    // 最近周四的实例
    const nowWeekThursday = getCurrentWeekThursday(this);
    // 今年第一个周四的实例
    const diffWeekThursday = getYearFirstThursday(this.isoWeekYear(), this.$u);
    // 算出周差后加一就是周数
    return nowWeekThursday.diff(diffWeekThursday, W) + 1;
  };

  /**
   * @description: 获取或设置一周的第 ISO 日,范围是 1-7
   * @param {Number} week 有值则为setter,无值则为getter
   * @return {Number|Dayjs}
   */
  proto.isoWeekday = function (week) {
    // setter时,用this.day()除7取余
    if (!this.$utils().u(week)) {
      return this.day(this.day() % 7 ? week : week - 7);
    }
    // getter时,直接返回 this.day(), 0的时候就是7
    return this.day() || 7;
  };

  const oldStartOf = proto.startOf;
  /**
   * @description: 扩展 .startOf .endOf APIs 支持单位 isoWeek
   * @param {String} units 单位
   * @param {Boolean} startOf 标志,true:startOf, false: endOf
   * @return {Dayjs} 返回新的 Dayjs 实例,cfg与原实例相同
   */
  proto.startOf = function (units, startOf) {
    const utils = this.$utils();
    const isStartOf = !utils.u(startOf) ? startOf : true;
    // 处理下单位 isoWeek
    const unit = utils.p(units);
    if (unit === isoWeekPrettyUnit) {
      // 获取本周一的开始
      return isStartOf
        ? this.date(this.date() - (this.isoWeekday() - 1)).startOf('day')
        : // 获取本周末的结束
          this.date(this.date() - 1 - (this.isoWeekday() - 1) + 7).endOf('day');
    }
    // 普通情况还是用老版本的 oldStartOf 处理
    return oldStartOf.bind(this)(units, startOf);
  };
};

isoWeeksInYear

这个方法就比较简单,计算实例所在年的 ISO 周总数,5253

/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} d dayjs函数对象
 */
export default (o, c) => {
  const proto = c.prototype;
  proto.isoWeeksInYear = function () {
    const isLeapYear = this.isLeapYear();
    const last = this.endOf('y');
    const day = last.day();
    if (day === 4 || (isLeapYear && day === 5)) {
      return 53;
    }
    return 52;
  };
};

普通 week

ISO8601 统一了对于周的历法,但是在各个国家和地区的传统中,对于周历中,每年的初始周和每周的初始日定义都各不相同。在 dayjs 中,把这两种设置在了 locale 中。

以汉语举例:

const locale = {
  // 可选,设置一周的开始,默认周日,1 代表周一
  weekStart: 1,
  // 可选,设置一年的开始周,包含1月4日的那一周作为第一周
  yearStart: 4,
};

可以发现,汉语环境下周历法的设置与 ISO8601 周历法一致。普通的 week 插件是如下三个:

  • weekday:在当前语言环境下,获取或设置实例是本周的第几天;
  • weekOfYear:在当前语言环境下,获取或设置实例是年中第几周;
  • weekYear:在当前语言环境下,获取的按周历算,实例所在的年份;

weekday

/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 */
export default (o, c) => {
  const proto = c.prototype;
  /**
   * @description: 获取或设置当前语言环境下本周的第几天。
   * @param {Number} input
   * @return {Number|Dayjs} getter时返回本周的第几天,setter时返回新实例
   */
  proto.weekday = function (input) {
    // locale 中设置的一周开始,中文是星期一开始,所以是weekStart = 1
    const weekStart = this.$locale().weekStart || 0;
    // 今天是周三,所以是$W = 3
    const { $W } = this;
    // 周三的 weekday = 3 - 1 = 2
    const weekday = ($W < weekStart ? $W + 7 : $W) - weekStart;
    if (this.$utils().u(input)) {
      return weekday;
    }
    // 减去2天,是周一,加上input天,就代表这设为了本周的第input天
    return this.subtract(weekday, 'day').add(input, 'day');
  };
};

weekOfYear

import { MS, Y, D, W } from '../../constant';

/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 * @param {Function} d dayjs函数对象
 */
export default (o, c, d) => {
  const proto = c.prototype;
  /**
   * @description: 在当前语言环境下,返回一个 number 来get实例是年中第几周,或者通过参数来set实例。默认的原型中是没有 week 这个单位的getter/setter的。
   * @param {Number} week set时的参数,设为第 week 周
   * @return {Number|setter} setter时返回新实例,getter时返回周数
   */
  proto.week = function (week = null) {
    // 有参数,也就是setter,此时就计算week差,然后add上
    if (week !== null) {
      return this.add((week - this.week()) * 7, D);
    }
    // getter
    // 获取设置的yearStart
    const yearStart = this.$locale().yearStart || 1;
    // 如果下一年的开始早于本周的最后,就代表是下一年的第一周,返回 1
    if (this.month() === 11 && this.date() > 25) {
      // d(this) is for badMutable
      const nextYearStartDay = d(this).startOf(Y).add(1, Y).date(yearStart);
      const thisEndOfWeek = d(this).endOf(W);
      if (nextYearStartDay.isBefore(thisEndOfWeek)) {
        return 1;
      }
    }
    // diffInWeek 小于 0,就返回本周第一天的week数
    const yearStartDay = d(this).startOf(Y).date(yearStart);
    const yearStartWeek = yearStartDay.startOf(W).subtract(1, MS);
    const diffInWeek = this.diff(yearStartWeek, W, true);
    if (diffInWeek < 0) {
      return d(this).startOf('week').week();
    }
    // 普通情况就直接返回 diffInWeek
    return Math.ceil(diffInWeek);
  };

  proto.weeks = function (week = null) {
    return this.week(week);
  };
};

weekYear

/**
 * @description: plugin
 * @param {Object} o option
 * @param {Class} c Dayjs类
 */
export default (o, c) => {
  const proto = c.prototype;
  /**
   * @description: 在当前语言环境下,获取的按周历算,实例所在的年份。大部分语言环境下包含1月4日的那一周作为一年的第一周。所以就会出现虽然是上一年,按周算却是下一年的情况
   * @return {Number} 返回按周计算的年数
   */
  proto.weekYear = function () {
    const month = this.month();
    const weekOfYear = this.week();
    const year = this.year();
    // 月数为 12 ,但周数却为1,说明要算到下一年中,就给返回年数加1
    if (weekOfYear === 1 && month === 11) {
      return year + 1;
    }
    return year;
  };
};

下篇继续分析插件。


前端记事本,不定期更新,欢迎关注!