Moment.js:moment(string,format)源码解析

1 概述

  • 最近被Safari浏览坑了两次:new Date('2020-05-30 15:18:30.254') -> Invalid Date;
  • 咨询公司里的前端大佬,发现他们前端都用Moment.js做日期转换;
  • 为什么Moment.js能够实现任意日期字符串格式转换呢? 先上结论:底层使用new Date(年,月,日,时,分,秒,毫秒)函数,这个函数基本上所有浏览器都实现了。

注:Moment.js很重(源码为4600行左右),所以有很多替代方案的,如:Dayjs、miment等,甚至根据浏览器的兼容情况自行写个轻量级的库也是可行的。

2 使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="http://cdn.staticfile.org/moment.js/2.24.0/moment.js"></script>
    <title>Learn MomentJs</title>
</head>
<body>
    <script>
        var moment1 = moment("2020-05-30 14:08:35","YYYY-MM-DD HH:mm:ss");
        var date = moment1._d;
        console.log(date);
    </script>
</body>
</html>

3 源码跟踪

  • 1 初始化moment():返回createLocal函数;
  • 2 初始化配置类:调用createLocal函数 -> createLocalOrUTC函数 ;
  • 3 完善配置信息并校验:prepareConfig函数 -> configFromStringAndFormat函数 -> configFromArray函数 -> checkOverflow函数;
  • 4 根据配置信息创建Moment对象:Moment构造函数。
;(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    global.moment = factory()
}(this, (function () { 'use strict';
    var hookCallback;

    function hooks () {
        return hookCallback.apply(null, arguments);
    }

    function setHookCallback (callback) {
        hookCallback = callback;
    }
     
    // 2 初始化配置类                  
    // 2.1 ex: input="2020-05-30 14:08:35",format="YYYY-MM-DD HH:mm:ss",locale=null,strict=null                 
    function createLocal (input, format, locale, strict) {
        return createLocalOrUTC(input, format, locale, strict, false);
    }
                      
    // 2.2 创建Local或者UTC Moment对象
	function createLocalOrUTC (input, format, locale, strict, isUTC) {
        var c = {};
		// 检验input字符串
        if ((isObject(input) && isObjectEmpty(input)) ||
                (isArray(input) && input.length === 0)) {
            input = undefined;
        }
        // 配置初始化
        c._useUTC = c._isUTC = isUTC;
        c._l = locale;
        c._i = input;
        c._f = format;
        c._strict = strict;
        return createFromConfig(c);
    }
    
    // 4 通过配置类创建Moment对象                  
    function createFromConfig (config) {
        var res = new Moment(checkOverflow(prepareConfig(config)));
        if (res._nextDay) {
            res.add(1, 'd');
            res._nextDay = undefined;
        }
        return res;
    }
                      
    // 3 完善配置信息
    function prepareConfig (config) {
        var input = config._i,
            format = config._f;
        config._locale = config._locale || getLocale(config._l);
        if (input === null || (format === undefined && input === '')) {
            return createInvalid({nullInput: true});
        }
        if (typeof input === 'string') {
            config._i = input = config._locale.preparse(input);
        }
        // 支持Moment对象、日期对象、数组对象、字符串格式、配置对象格式
        if (isMoment(input)) {
            return new Moment(checkOverflow(input));
        } else if (isDate(input)) {
            config._d = input;
        } else if (isArray(format)) {
            configFromStringAndArray(config);
        } else if (format) {
            // 以此为例
            configFromStringAndFormat(config);
        }  else {
            configFromInput(config);
        }
        if (!isValid(config)) {
            config._d = null; // _d为日期对象,下面讲解。
        }
        return config;
    }
                      
    // 3.1 通过字符串模板创建Moment对象中的日期对象(_d)
    function configFromStringAndFormat(config) {
        // 创建Date对象用的数组,如:[年,月,日,时,分,秒]
        config._a = [];
        getParsingFlags(config).empty = true;
        var string = '' + config._i,
            i, parsedInput, tokens, token, skipped,
            stringLength = string.length,
            totalParsedInputLength = 0;
        // tokens=['YYYY','-','MM','-','DD',' ','HH',':','mm',':','ss']
        tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];

        for (i = 0; i < tokens.length; i++) {
            token = tokens[i]; // 首次为YYYY
            parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; // 首次为2020
            if (parsedInput) {
                skipped = string.substr(0, string.indexOf(parsedInput));
                if (skipped.length > 0) {
                    getParsingFlags(config).unusedInput.push(skipped);
                }
                string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
                totalParsedInputLength += parsedInput.length;
            }
            
            if (formatTokenFunctions[token]) {
                if (parsedInput) {
                    getParsingFlags(config).empty = false;
                }
                else {
                    getParsingFlags(config).unusedTokens.push(token);
                }
                // 将parsedInput添加到config._a数组中
                addTimeToArrayFromToken(token, parsedInput, config);
            }
            else if (config._strict && !parsedInput) {
                getParsingFlags(config).unusedTokens.push(token);
            }
        }

        // add remaining unparsed input length to the string
        getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
        if (string.length > 0) {
            getParsingFlags(config).unusedInput.push(string);
        }

        // clear _12h flag if hour is <= 12
        if (config._a[HOUR] <= 12 &&
            getParsingFlags(config).bigHour === true &&
            config._a[HOUR] > 0) {
            getParsingFlags(config).bigHour = undefined;
        }

        getParsingFlags(config).parsedDateParts = config._a.slice(0);
        getParsingFlags(config).meridiem = config._meridiem;
        // handle meridiem
        config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem);

        configFromArray(config);
        checkOverflow(config);
    }
                      
    // 3.2 当config._a日期相关数组完善后                  
    function configFromArray (config) {
        var i, date, input = [], currentDate, expectedWeekday, yearToUse;
        
        // 年月日
        currentDate = currentDateArray(config);

        // ...
        
        // 简单描述:将config._a数组中的元素暂存至input数组中用于调用createDate方法。
        
        // Default to current date.
        // * if no year, month, day of month are given, default to today
        // * if day of month is given, default month and year
        // * if month is given, default only year
        // * if year is given, don't default anything
        for (i = 0; i < 3 && config._a[i] == null; ++i) {
            config._a[i] = input[i] = currentDate[i];
        }

        // Zero out whatever was not defaulted, including time
        for (; i < 7; i++) {
            config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
        }

        // Check for 24:00:00.000
        if (config._a[HOUR] === 24 &&
                config._a[MINUTE] === 0 &&
                config._a[SECOND] === 0 &&
                config._a[MILLISECOND] === 0) {
            config._nextDay = true;
            config._a[HOUR] = 0;
        }

        // 实际创建日期的方法,前面已经把
        config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input);
        expectedWeekday = config._useUTC ? config._d.getUTCDay() : config._d.getDay();

        if (config._tzm != null) {
            config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
        }

        if (config._nextDay) {
            config._a[HOUR] = 24;
        }

        if (config._w && typeof config._w.d !== 'undefined' && config._w.d !== expectedWeekday) {
            getParsingFlags(config).weekdayMismatch = true;
        }
    }
                      
    // ex: y=2020,m=4,d=30,h=14,M=8,s=35,ms=0                  
    function createDate (y, m, d, h, M, s, ms) {
        var date;
        // the date constructor remaps years 0-99 to 1900-1999
        if (y < 100 && y >= 0) {
            date = new Date(y + 400, m, d, h, M, s, ms);
            if (isFinite(date.getFullYear())) {
                date.setFullYear(y);
            }
        } else {
            // 最终调用通用的日期创建方法(这个方法所有浏览器都实现了)
            date = new Date(y, m, d, h, M, s, ms);
        }
        return date;
    }
          
    // 4 创建Moment对象                  
    function Moment(config) {
        copyConfig(this, config);
        this._d = new Date(config._d != null ? config._d.getTime() : NaN);
        if (!this.isValid()) {
            this._d = new Date(NaN);
        }
        // Prevent infinite loop in case updateOffset creates new moment
        // objects.
        if (updateInProgress === false) {
            updateInProgress = true;
            hooks.updateOffset(this);
            updateInProgress = false;
        }
    }

                      
    // 中间省略亿点细节
                      
    hooks.version = '2.24.0';

    // 设置hooks为createLocal                  
    setHookCallback(createLocal);

    // currently HTML5 input type only supports 24-hour formats
    hooks.HTML5_FMT = {
        DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm',             // <input type="datetime-local" />
        DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss',  // <input type="datetime-local" step="1" />
        DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS',   // <input type="datetime-local" step="0.001" />
        DATE: 'YYYY-MM-DD',                             // <input type="date" />
        TIME: 'HH:mm',                                  // <input type="time" />
        TIME_SECONDS: 'HH:mm:ss',                       // <input type="time" step="1" />
        TIME_MS: 'HH:mm:ss.SSS',                        // <input type="time" step="0.001" />
        WEEK: 'GGGG-[W]WW',                             // <input type="week" />
        MONTH: 'YYYY-MM'                                // <input type="month" />
    };

    // 1. 返回hooks,实际返回createLocal函数
    return hooks;
})));

3 参考

ECMAScript® Language Specification

invalid date in safari

posted @ 2020-05-31 10:17  月下小魔王  阅读(371)  评论(0编辑  收藏