最近项目中碰到一个奇怪问题,项目是react+antd+umi实现,当页面中点击对话框,页面会自动滚动到最上面。最终定位跟antd dialog的实现有关,在某种条件下会触发此问题,下面分享一下问题定位过程,希望对遇到类似问题的同学有所帮助。
问题现象
列表页滚动到下方,点击操作弹出对话框,列表自动滚动到最上面。此问题在上个版本还不存在。


问题定位
一定是触发了滚动事件才会滚动到列表最上方,怎么定位哪里触发的滚动事件呢,从代码入手毫无头绪。好在可以通过浏览器devtool打断点,跟踪一下哪里触发。首先尝试了scroll事件,确实能够进到断点,但是堆栈只有一贞,没法找到源头。接着尝试了focus事件。

进到focus事件时已经滚动到上方了,此时有堆栈信息,从堆栈定位到rc-dialog,其中有一句this.switchScrollingEffect(),高度怀疑这里触发了问题,在这里打上断点,证实了确实如此。

进一步调试到this.switchScrollingEffect里面,问题触发点在setStyle这里,设置了如下样式后,就会触发滚动到最上面。

switchScrollingEffect如下:

setStyle如下:

结合最近修改代码反复尝试,发现导致上面问题在于如下样式:
1 2 3 4
| html { overflow-x: hidden; overflow-y: auto; }
|
也就是当html有如上样式(body没有,如果body也有也不会触发问题),再设置body样式{overflow: hidden, overflow-x: hidden, overflow-y: hidden},就会出现自动滚动到最上面。
找到问题原因后我写了一小段代码最小化重现了这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <html> <body id="content"> <button onclick="restore()"> restore </button><br> sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss<br> s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br>s<br> <button onclick="setStyle()"> setstyle </button> <button onclick="showsize()"> showsize </button> </body> </html>
|
1 2 3 4 5 6 7 8 9 10
| html { border: 1px solid red; height: 100%; overflow-y: auto; }
body { border: 1px solid blue; height: 100%; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function setStyle() { var s = document.getElementById("content"); s.style['overflow'] = 'hidden'; s.style['overflow-x'] = 'hidden'; s.style['overflow-y'] = 'hidden';
} function showsize() { var s = document.getElementById("content"); alert("document.body.offsetWidth="+document.body.offsetWidth+"\nwindow.innerWidth="+window.innerWidth+"\ndocument.body.scrollHeight="+document.body.scrollHeight+"\nwindow.innerHeight="+window.innerHeight); } function restore() { var s = document.getElementById("content"); s.style['overflow'] = ''; s.style['overflow-x'] = ''; s.style['overflow-y'] = ''; }
|
问题分析
因为body没有设置overflow-y,默认是visible,所以内容显示到框外。
html设置了overflow-y: auto,所以出现纵向滚动条。
当点击setstyle后,body高度变小,html就滚动到最上面。
如果html没有设置overflow-y: auto,则默认是visible,不会滚动,仍然保持在底部。
如果body也设置了overflow-y:auto,则body会出现纵向滚动条,而html的滚动条只会覆盖body的clientheight区域。点击setstyle,只是将body的滚动条去掉,不会触发html滚动。
所以rc-dialog.switchScrollingEffect()的实现是存在一些缺陷的。
其他问题
上面调试过程中,isBodyOverflowing的实现引起了我的兴趣。
1 2 3 4 5 6 7
| function isBodyOverflowing() { return ( document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) && window.innerWidth > document.body.offsetWidth ); }
|
window.innerWidth > document.body.offsetWidth这个条件,表示body的width+padding没有超过浏览器窗口内宽。document.body.scrollHeight >
(window.innerHeight || document.documentElement.clientHeight)表示有纵向滚动。所以这个条件的意图有点让人不太理解,前一个条件很容易不满足,例如设置body的width为100vw。

(图片来自https://www.pianshen.com/article/6486132360/)