【生产问题】容易在2月份出现的日期错乱问题!!!

417 阅读1分钟

起因

早上的上班的时候以为今天又是摸鱼划水的一天,结果刚到公司,测试就发来一张截图,让我马上改好!!!

一大早,这能忍?当然我选择了忍,毕竟还是要靠工作吃饭的~~~

仔细研究了一下,发现不仅仅是12月,其他月份都有这个问题,而且是所有的日期选择器都出现了这个问题,怪不得这么着急修改,如果让用户先发现了,不得大骂产品和公司!!!反正骂不到我~~~~

言归正传,具体来研究研究这个BUG吧!!!

过程

因为是所有的日期选择器都存在这个问题,所有可以肯定是日期选择器组件出了问题

打开日前选择组件文件,先看看文件的git提交历史,发现近半年都没有代码提交,说明这个BUG是很久以前就存在的BUG(没人可以背锅)

通过打断点逐行审计代码,发现这几行的代码输出有异常~~~

把这段代码提出来进行单元测试,以2023年3月为例子

let dateList = [], year = 2023, month = 3;
for (let i = 1; i <= 31; i++) {
    let date = new Date();			// 初始化为系统默认时间:2023-02-02
    date.setFullYear(year);
    date.setDate(i);
    date.setMonth(month - 1);
    dateList.push(date);
}
console.log(dateList);
[
    "2023-03-01T13:27:20.537Z",
    "2023-03-02T13:27:20.537Z",
    "2023-03-03T13:27:20.537Z",
			....
			....
    "2023-03-27T13:27:20.537Z",
    "2023-03-28T13:27:20.537Z",
    "2023-03-01T13:27:20.537Z",
    "2023-03-02T13:27:20.537Z",
    "2023-03-03T13:27:20.537Z"
]

注:如果代码你执行是正确,那么请把电脑的系统时间设置为2月的某一天

仔细观察结果发现,在2023-03-28之前日期都是正常的,但是在之后本来应该是2023-03-29,却变成了2023-03-01,对比测试发给我的图,问题刚好就对上了,就是在2023-03-28出问题了!!!

之后又测试了2023年5月2023年6月,结果都是一样的,28号的日期都是错的~~~

找到问题,就解决了一半的BUG了~~~

解决问题

反复审计这一段代码,发现API的使用上也没什么问题,我试着把date.setDate方法放在date.setMonth方法之后,结果问题莫名其妙的解决了!!!

let dateList = [], year = 2023, month = 3;
for (let i = 1; i <= 31; i++) {
    let date = new Date();	// 初始化为系统默认时间:2023-02-02
    date.setFullYear(year);
    
    date.setMonth(month - 1);   // 和setDate方法交换位置
    date.setDate(i);
    
    dateList.push(date);
}
console.log(dateList);
[
    "2023-03-01T13:27:20.537Z",
    "2023-03-02T13:27:20.537Z",
    "2023-03-03T13:27:20.537Z",
			....
			....
    "2023-03-27T13:27:20.537Z",
    "2023-03-28T13:27:20.537Z",
    "2023-03-29T13:27:20.537Z",
    "2023-03-30T13:27:20.537Z",
    "2023-03-31T13:27:20.537Z"
]

虽然BUG解决了,但是原因还是没明白,为什么替换API位置就可以???

问题原因

我怀疑是setDate方法的问题,在MDN上找到方法的介绍,发现这样一句话:

如果 dayValue 超出了月份(getMonth的值)的合理范围,setDate 将会相应地更新 Date 对象

可能没看懂,再看看官方的例子

var theBigDay = new Date(1962, 6, 7); // 1962-07-07
theBigDay.setDate(22);  	      // 1962-07-22
theBigDay.setDate(32);  	      // 1962-08-01

theBigDay.setDate(32)的值是1962-08-01,因为1962-07没有第32天,所有直接就到下一个月了


回到我们的问题上面来,以生成2023-03-29为例子

let date = new Date();	// 因为没有传递参数,所以就是当前日期:2023-02-02
date.setFullYear(2023);	// 改变年份:2023-02-02
date.setDate(29);	// 改变天数,2月只有28天,没有第29天,所以直接就到下一个月了,更新日期:2023-03-01
date.setMonth(2);	// 改变月份:2023-03-01

console.log(date);	// 2023-03-01

有没有一种恍然大悟的感觉,我们再交换一下setDatesetMonth的位置

let date = new Date();	// 因为没有传递参数,所以就是当前日期:2023-02-02
date.setFullYear(2023);	// 改变年份:2023-02-02
date.setMonth(2);	// 改变月份:2023-03-02
date.setDate(29);	// 改变天数,3月有第29天,不改变月份,更新日期:2023-03-29

console.log(date);	// 2023-03-29

已经完全可以解释为什么交换setMonthsetDate方法的位置就可以解决这个问题了!!!

我还发现Day.js第三方中,也存在这个问题,一样不能先改天数,再改日期!!!

let d = dayjs('2023-02-02')
d = d.date(29)
d = d.month(2)

d.format('YYYY-MM-DD')  // 本应该是:2023-02-29,但是现在是错误日期:2023-03-01

解决问题的办法也是一样的,交换*d.date(29)d.month(2)*的位置就可以解决问题了!!!

在设置日期时,必须先设置月份,再设置天数

是不是认为只要记住设置顺序就可以高枕无忧了喃!!!

NO!NO!NO!NO!!!请客官继续往下看

setMonth方法

我又去深入研究了setMonth方法,发现setMonth有着和setDate的特性:

如果有一个指定的参数超出了合理范围,setMonth 会相应地更新日期对象中的日期信息

var date = new Date(2023, 6, 31); // 2023-07-31
date.setMonth(1);  // 本意设置为:2023-02-31,但是日期超出了合理范围,被修改为:2023-03-03
date.setDate(28);  // 2023-03-28

很显然,我们想把2023-06-31改为2023-02-28的计划泡汤了!!!

这里的解决方案:setMonthsetDate交换顺序~~~

是不是觉得很无语了,这setMonthsetDate到底按照怎样的顺序使用呀!!!

首先无论setMonthsetDate的顺序如何,我都强烈建议:在设置日期时,必须先设置月份,再设置天数

最终解决方案

  1. 不使用setMonthsetDate,直接通过setFullYear直接设置年月日
var date = new Date(2023, 6, 31); // 2023-07-31
date.setFullYear(2023, 1, 28)     // 2023-02-28
  1. 设置月份时,将日期设为1
var date = new Date(2023, 6, 31); // 2023-07-31
date.setMonth(1, 1);              // 2023-02-01 日期合理,不会被更新
date.setDate(28);                 // 2023-02-28
  1. 使用第三方模块Day.js
var date = dayjs('2023-07-31');   // 2023-07-31
date = date.month(1)              // 2023-02-28
date.date(28);                    // 2023-02-28

探究Day.js原理

Day.js中的date方法(修改天数)并没有扩展,这里只讨论month(修改月份)方法

废话不说,上源码~~~

还是以2023-06-31改为2023-02-28为例

// date.month(2)
if (unit === C.M || unit === C.Y) {
    // 修改月和年走这里
    const date = this.clone().set(C.DATE, 1)		// 先将日期的天数设置为1:2023-06-1
    date.$d[name](arg)					// 修改月份:2023-02-1
    date.init()
    // this.$D 是最初天数,是31
    // date.daysInMonth() 是2月最大的天数,是28
    let initDay = Math.min(this.$D, date.daysInMonth())	// Math.min(31,28) => 28
    this.$d = date.set(C.DATE, initDay).$d	// 重新设置天数:修改月份:2023-02-28
} else if {
  	// 修改天数、星期等.... 
    (name) this.$d[name](arg)
}

其实逻辑还是很简单的,转为我们能看懂的伪代码就是:

var date = new Date(2023, 6, 31); // 2023-07-31

let initDay = date.getDate()	 // 保存原来的天数:31
date.setDate(1)			 // 先将日期的天数设置为1:2023-06-1
date.setMonth(1);             	 // 更新月份:2023-02-1
let initDay = Math.min(initDay,2月的总天数是28)	// initDay => 28
date.setDate(initDay)			// 恢复最初的日期 2023-02-28

date.setDate(28);                // 2023-02-28

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情