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

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

作者: 拜仁的月饼 | 来源:发表于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