起因
早上的上班的时候以为今天又是摸鱼划水的一天,结果刚到公司,测试就发来一张截图,让我马上改好!!!
一大早,这能忍?当然我选择了忍,毕竟还是要靠工作吃饭的~~~
仔细研究了一下,发现不仅仅是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
有没有一种恍然大悟的感觉,我们再交换一下setDate和setMonth的位置
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
已经完全可以解释为什么交换setMonth和setDate方法的位置就可以解决这个问题了!!!
我还发现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的计划泡汤了!!!
这里的解决方案:setMonth和setDate交换顺序~~~
是不是觉得很无语了,这setMonth和setDate到底按照怎样的顺序使用呀!!!
首先无论setMonth和setDate的顺序如何,我都强烈建议:在设置日期时,必须先设置月份,再设置天数
最终解决方案
- 不使用setMonth和setDate,直接通过setFullYear直接设置年月日
var date = new Date(2023, 6, 31); // 2023-07-31
date.setFullYear(2023, 1, 28) // 2023-02-28
- 设置月份时,将日期设为1
var date = new Date(2023, 6, 31); // 2023-07-31
date.setMonth(1, 1); // 2023-02-01 日期合理,不会被更新
date.setDate(28); // 2023-02-28
- 使用第三方模块
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 天,点击查看活动详情