美文网首页
三种时间处理库时间格式化方法源码阅读

三种时间处理库时间格式化方法源码阅读

作者: 拜仁的月饼 | 来源:发表于2024-01-04 18:54 被阅读0次

本文就moment, dayjs, date-fns三个JS中常用的时间处理库时间格式化方法源码解析,以探索时间库是如何处理大写的YYYY及小写yyyy的。

moment

源码地址: moment-github

源码位置:

  • ./moment.js
  • src/moment.js
  • src/lib/moment/format.js
  • src/lib/format/format.js
  • src/lib/units/era.js
  • src/lib/units/year.js

根目录下的moment.js是由分析src/moment.js的引用关系打包而来,所以直接跳到src/moment.js下观察,发现src/moment.js全部引用自src/lib/*,因此可推论src/lib/*是所有moment核心逻辑片断所在。因此,将所有与format有关的文件查找后,发现与format直接相关的是src/lib/moment/format.jssrc/lib/format/format.js

在写代码时,调用format方法的示例代码:

import moment from 'moment';

// format是在moment()实例创建后才能调用
const formattedTime = moment().format('YYYY-MM-DD');

经分析,moment().format(...)实际上调用的是src/lib/moment/format.js中的方法。源码节选如下:

//  '../format/format' 即 `src/lib/format/format.js`
import { formatMoment } from '../format/format';

export function format(inputString) {
    // ... 省略部分不太相关的逻辑
    var output = formatMoment(this, inputString);
    return this.localeData().postformat(output);
}

据此,我们可以看出format方法最为核心的逻辑是调用src/lib/format/format.js中的formatMoment方法。继续节选源码:

// format date using native date object
export function formatMoment(m, format) {
    // ... 省略部分不太相关的逻辑
    formatFunctions[format] =
        formatFunctions[format] || makeFormatFunction(format);

    return formatFunctions[format](m);
}

makeFormatFunction节选:

function makeFormatFunction(format) {
    var array = format.match(formattingTokens),
        i,
        length;

    for (i = 0, length = array.length; i < length; i++) {
        if (formatTokenFunctions[array[i]]) {
            // 核心:读取formatTokenFunctions
            array[i] = formatTokenFunctions[array[i]];
        } else {
            array[i] = removeFormattingTokens(array[i]);
        }
    }

    return function (mom) {
        var output = '',
            i;
        for (i = 0; i < length; i++) {
            output += isFunction(array[i])
                ? array[i].call(mom, format)
                : array[i];
        }
        return output;
    };
}

如果再回看src/lib/format/format.js,发现一开始的formatFunctionsformatTokenFunctions都是空的。那么,它们在哪里赋值呢?随后的addFormatToken给出了答案:

// 暂时不用特别关注细节
// 只需知道这个函数可以注册格式化函数到 formatTokenFunctions
export function addFormatToken(token, padded, ordinal, callback) {
    var func = callback;
    if (typeof callback === 'string') {
        func = function () {
            return this[callback]();
        };
    }
    if (token) {
        formatTokenFunctions[token] = func;
    }
    if (padded) {
        formatTokenFunctions[padded[0]] = function () {
            return zeroFill(func.apply(this, arguments), padded[1], padded[2]);
        };
    }
    if (ordinal) {
        formatTokenFunctions[ordinal] = function () {
            return this.localeData().ordinal(
                func.apply(this, arguments),
                token
            );
        };
    }
}

这个函数在哪里被使用了呢?继续全局搜索可以发现,均在src/lib/units/*中。继续全局搜索,可以发现:

  • src/lib/units/era.js 注册了yyyy
  • src/lib/units/year.js 注册了YYYY

代码片断如下:

// src/lib/units/era.js

addFormatToken('y', ['y', 1], 'yo', 'eraYear');
addFormatToken('y', ['yy', 2], 0, 'eraYear');
addFormatToken('y', ['yyy', 3], 0, 'eraYear');
addFormatToken('y', ['yyyy', 4], 0, 'eraYear');

根据addFormatToken函数执行逻辑可看出,yyyy注册名为eraYear的格式化方法。而通过./moment.js中源码可以看出, eraYear实际上是getEraYear函数:

// ./moment.js

// ...省略不相关代码

// line 4956
proto.eraYear = getEraYear;

那么,这个getEraYear函数是怎么执行的呢?代码节选:

export function getEraYear() {
    var i,
        l,
        dir,
        val,
        // 依赖于localeData的eras运行结果
        eras = this.localeData().eras();
    
    // 如有eras,则处理eras
    // 暂时忽略下方细节
    for (i = 0, l = eras.length; i < l; ++i) {
        dir = eras[i].since <= eras[i].until ? +1 : -1;

        // truncate time
        val = this.clone().startOf('day').valueOf();

        if (
            (eras[i].since <= val && val <= eras[i].until) ||
            (eras[i].until <= val && val <= eras[i].since)
        ) {
            return (
                (this.year() - moment(eras[i].since).year()) * dir +
                eras[i].offset
            );
        }
    }

    // 如果没有eras字段, 则直接返回this.year();
    return this.year();
}

依照相同的方式,我们看YYYY的注册及调用:

注册YYYY:

// src/lib/units/year.js

// ...省略无关代码
addFormatToken('Y', 0, 0, function () {
    var y = this.year();
    return y <= 9999 ? zeroFill(y, 4) : '+' + y;
});

addFormatToken(0, ['YY', 2], 0, function () {
    return this.year() % 100;
});

addFormatToken(0, ['YYYY', 4], 0, 'year');
addFormatToken(0, ['YYYYY', 5], 0, 'year');
addFormatToken(0, ['YYYYYY', 6, true], 0, 'year');

yearproto上的映射:

// ./moment.js

// ...省略不相关代码

// line 4957
proto.year = getSetYear;
// src/lib/units/year.js
export var getSetYear = makeGetSet('FullYear', true);

// makeGetSet函数: src/lib/moment/get-set.js
export function makeGetSet(unit, keepTime) {
    return function (value) {
        if (value != null) {
            set(this, unit, value);
            hooks.updateOffset(this, keepTime);
            return this;
        } else {
            return get(this, unit);
        }
    };
}

// getSetYear无参数调用时是获取当前年份
// 会走到 get(this, unit)分支上
// get(this, unit)两个参数的含义:
// this: moment实例
// unit: 传入makeGetSet的第一个参数。在getSetYear中为'FullYear'
// 所以,看一下相关的get函数执行逻辑
// 位置: src/lib/moment/get-set.js
export function get(mom, unit) {
    // 省略无关逻辑
    switch (unit) {
        // ... 省略无关
        case 'FullYear':
            // 因此看出, 获取年份是使用的getUTCFullYear 及 getFullYear
            return isUTC ? d.getUTCFullYear() : d.getFullYear();
    }
}

综上可得出结论:

  • yyyy视为处理eras(纪年)。如当前locale无纪年,视为处理日历年
  • YYYY是正常处理日历年的方式,底层使用getUTCFullYeargetFullYear

dayjs

源码地址: dayjs-gitee

源码位置: src/index.js

dayjs相对直观许多,节选format方法如下:

// line 262

format(formatStr) {
   // ... 省略不相关细节

// 靠其中的matches内部函数实现
const matches = (match) => {
      switch (match) {
        // 大写
        case 'YY':
          return String(this.$y).slice(-2)
        // 大写
        case 'YYYY':
          return Utils.s(this.$y, 4, '0')
        case 'M':
          return $M + 1
        case 'MM':
          return Utils.s($M + 1, 2, '0')
        // ... 省略不相关细节
        default:
          break
      }
      return null
    }

   // ... 省略不相关细节
}

可以看出,dayjs仅处理了大写的YYYY,而完全没有处理小写的yyyy。而处理年份则是靠this.$y来达成,源码节选:

// line 100

// $d即Date对象
this.$y = $d.getFullYear()

总而言之,dayjs并没有处理小写yyyy,只是将大写的YYYY处理为日历年,且底层实现是getFullYear

此外,luxon的处理策略与dayjs正好相反,仅处理yyyy而不处理YYYY, 可从luxon源码中看出,且luxon的底层是getUTCFullYear;

date-fns

切记要看v2源码,目前v3不常用 (2024/1/5)。

v2才是当前的主流版本

源码地址: date-fns v2

源码位置: src/format/index.ts

date-fns源码非常好的一点是代码中注释即运行原理文档,光看文档就知道怎么回事。以下是部分节选:

源码中的注释

从注释中可以看出, date-fns对大写YYYY小写yyyy做了明确区分。YYYY指的是基于周的年份,即容易产生跨年Bug的表示法;yyyy即日历年。

向下翻源码,节选如下(依旧屏蔽及删除关联不大的部分,只保留最核心逻辑):

  // ... 暂时屏蔽无关
import formatters from '../_lib/format/formatters/index'

// 正则
const formattingTokensRegExp = /[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g

export default function format(
  dirtyDate: Date | number,
  formatStr: string,
  options?: LocaleOptions &
    WeekStartOptions &
    FirstWeekContainsDateOptions &
    AdditionalTokensOptions
): string {
  
  // ... 暂时屏蔽无关
  const result = formatStr
     // 进行正则匹配
    .match(formattingTokensRegExp)!
    .map(substring => {
      const firstCharacter = substring[0]
      // 有删节
      
      // formatters即格式化函数表
      const formatter = formatters[firstCharacter]
      
      if (formatter) {
        // !!! 如果没有设置options.useAdditionalWeekYearTokens === true
        // !!! 并且 substring isProtectedWeekYearToken
        // !!! 那么会报错
        if (
          !options?.useAdditionalWeekYearTokens &&
          isProtectedWeekYearToken(substring)
        ) {
          throwProtectedError(substring, dirtyFormatStr, String(dirtyDate))
        }
        // 有删除

        // 用formatter函数实现格式化
        return formatter(utcDate, substring, locale.localize, formatterOptions)
      }

      return substring
    })
    .join('')

  return result
}

代码中最重要的就两段,其中一个是命中proctedWeekYearToken的报错机制,另一个是formatter函数。

首先看protectedWeekYearToken部分源码:

// src/_lib/protectedTokens/index.ts
const protectedDayOfYearTokens = ['D', 'DD']
const protectedWeekYearTokens = ['YY', 'YYYY']

export function isProtectedDayOfYearToken(token: string): boolean {
  return protectedDayOfYearTokens.indexOf(token) !== -1
}

export function isProtectedWeekYearToken(token: string): boolean {
  return protectedWeekYearTokens.indexOf(token) !== -1
}

如果options.useAdditionalWeekYearTokens === false (默认值)并且用YYYY取基于周的年份,那么会直接报错。换句话说,date-fns从机制上为用YYYY格式化上设置了壁垒。

然后看formatter源码:

// src/_lib/format/formatters/index.ts

  // Year
  // 处理小写y
  y: function (date, token, localize) {
    // Ordinal number
    if (token === 'yo') {
      // 底层使用的是getUTCFullYear
      const signedYear = date.getUTCFullYear()
      // Returns 1 for 1 BC (which is year 0 in JavaScript)
      const year = signedYear > 0 ? signedYear : 1 - signedYear
      return localize.ordinalNumber(year, { unit: 'year' })
    }

    return lightFormatters.y(date, token)
  },

  // Local week-numbering year
  // 处理大写Y
  Y: function (date, token, localize, options) {
    // !!! 忽略细节,是库自己实现的取基于周的年份的方法,不是JS原生方法
    const signedWeekYear = getUTCWeekYear(date, options)
    // Returns 1 for 1 BC (which is year 0 in JavaScript)
    const weekYear = signedWeekYear > 0 ? signedWeekYear : 1 - signedWeekYear

    // Two digit year
    if (token === 'YY') {
      const twoDigitYear = weekYear % 100
      return addLeadingZeros(twoDigitYear, 2)
    }

    // Ordinal number
    if (token === 'Yo') {
      return localize.ordinalNumber(weekYear, { unit: 'year' })
    }

    // Padding
    return addLeadingZeros(weekYear, token.length)
  },

可以得出:

  • date-fns使用的是getUTCFullYear()方法实现的取年份方法,需要格外注意时差问题
  • yyyyYYYY是不同的含义。也是唯一符合Unicode对于yyyyYYYY定义的库

相关文章

网友评论

      本文标题:三种时间处理库时间格式化方法源码阅读

      本文链接:https://www.haomeiwen.com/subject/nesxndtx.html