1. 介绍
工作中碰到一个需求,在后台管理系统中,从列表页点击查看详情,然后再点击浏览器的返回列表,希望保持列表页的scrollTop
目前的表现是,回到列表页总是回到顶部
我在网上查找的大部分方法都是在列表页监听scrollTop,在页面卸载的时候,保存当时的列表数据和scrollTop到状态管理(或者storage中),返回的时候读取状态管理(或者storage)中的列表数据和scrollTop回显出来
但是我觉得这种操作很麻烦,而且我印象浏览器自带的特性就是可以保存scrollTop的
2. 浏览器自带的特性
于是我先进行了一波印证,不用任何现代前端框架,看看浏览器自带的特性是啥样的
用原生html写了两个最简单的页面
- index.html 是一个itemList的列表,点击任何一个item都会跳转到detail
- detail.html 是一个超长的详情页,拥有2个按钮,一个是返回,一个是跳转
index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.list {
overflow: hidden;
}
.item {
width: 100px;
height: 300px;
border: 1px solid #000;
margin: 10px;
padding: 5px;
}
</style>
<script>
function goDetail() {
window.location.href = './detail.html'
}
window.onload = function () {
// 给list增加 20 个item
var list = document.querySelector('.list')
for (var i = 0; i < 20; i++) {
var item = document.createElement('div')
item.className = 'item'
item.innerText = `item ${i} \n 点击跳转到详情页`
// 点击方法
item.onclick = function () {
goDetail()
}
list.appendChild(item)
}
}
</script>
</head>
<body>
<div class="list"></div>
</body>
</html>
detail.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.box {
width: 90px;
height: 1999px;
background: rosybrown;
}
</style>
</head>
<body>
<div style="position: fixed; top: 20px; right: 10px; width: 100px">
<button onclick="history.back(-1)">返回列表页</button>
<button onclick="window.location.href = './index.html'">跳转列表页</button>
</div>
<div class="box" onclick="history.back(-1)">这是deal页</div>
</body>
</html>
2.1 浏览器自带的特性-结论
2.1.1 结论1
- 浏览器自带的
history.back(-1)
本身就是可以保持scrollTop的 - 如果是用 window.location.href = './index.html' 则会重置scrollTop
如下图:

2.1.2 结论2
我又进行了一个额外的测试:
在列表页(index.html)onload中加上setTimeout 去创建items,滚动条是无法保存的
index.html
js
window.onload = function () {
console.log(`🚀 ~ onload`)
setTimeout(() => {
// 给list增加 20 个item
var list = document.querySelector('.list')
for (var i = 0; i < 20; i++) {
var item = document.createElement('div')
item.className = 'item'
item.innerText = `item ${i} \n 点击跳转到详情页`
// 点击方法
item.onclick = function () {
goDetail()
}
list.appendChild(item)
}
}, 12)
}
如下图:

2.1.3 结论3(重要!)
注意:以上2个测试,页面右侧滚动条都是body的滚动条
,事实上对于一个普通html,如果我们不加任何样式干预,浏览器页面的滚动条其实都是body的
如果我把body overflow: hidden;
让产生滚动的容器是 list,会有什么表现呢
index.html 增加以下样式:
html
<style>
html,
body {
height: 100%;
overflow: hidden;
}
.list {
height: 100%;
overflow: auto; // 让list作为滚动容器
}
.item {
width: 100px;
height: 300px;
border: 1px solid #000;
margin: 10px;
padding: 5px;
}
</style>
- 结论:只有当滚动容器是body的时候,浏览器自带的特性才会记住它的scrollTop
如下图,此时滚动容器是list:

2.2 浏览器自带的特性-总结
如果想要页面可以通过浏览器自带的特性进行保存scrollTop,要满足
- 通过history.back触发,或者通过浏览器自带的返回键触发
- 返回页面时,要马上恢复数据
- 滚动的容器一定是body
3. React 的表现
3.1 React原始的表现
因为项目中,有太多的干扰项,我先建一个干净的react项目,只增加react-router-dom依赖包,想要看看react最原始的表现如何
我直接用vite官网的模板创建的,只安装了react-router-dom,相关版本请看package.json
package.json:

同样的,我创建了一个列表页index和一个详情页detail,和之前样式都是一样的
可以通过CodeSandBox访问:
3.2 React原始的表现-结论
3.2.1 结论1:body作为滚动容器
-
react + react router本身也是可以保存scrollTop的
-
但是有些区别:
2.1 在react中,从index跳转到detail时,detail的scrollTop不会回到顶部
2.2 在detail中直接跳转(不调用navigate(-1),而是调用navigate(path)),回到index页时,不会重置scrollTop
可以认为:body作为滚动容器时,react本身的特性是在跳转中永远都会保持scrollTop
如下图:

3.2.2 结论2:body作为滚动容器,使用useEffect,useState初始值[]
代码进行一些修改:

结论:
如果使用了useState + useEffect ,并且useState初始值是 []
的话,返回index页时,scrollTop总会回到顶部。
我猜测是因为useEffect的执行时机是在dom渲染结束之后,此时dom已经渲染过一次了(scrollTop已经归零),再setList scrollTop肯定不会回到原位了
效果如下图:

3.2.3 结论3:body作为滚动容器
使用useEffect,useState初始值defaultList

结论: 设置useState初始值,即在dom渲染之前设置好list,表现和结论1时是一样的:在跳转中永远都会保持scrollTop
使用useLayoutEffect也是同样的效果
3.2.4 结论4:使用非body元素作为滚动容器
会是什么表现呢
我加了一个 div.page 设置为 height:100vh; overflow: auto; 同时 body设置为overflow: hidden;
结果和原生浏览器是一样的:只要是非body作为滚动容器,都不会保持scrollTop
4. 最终的结论
- 浏览器本身就有保持scrollTop的特性
- 前提是一定要body作为滚动容器
- React同样也有,区别是前进的时候不会重置scrollTop到顶部(任何跳转都会保持scrollTop)
我们在项目中大部分的场景都没保持scrollTop,大概率是因为:1.可能不是body作为滚动容器;2. 返回到列表页时,重新进行请求,并没有缓存之前的列表数据,列表数据经历了从空数组[]
到重新赋值的过程,在这个过程中scrollTop已经重置到顶部了。
得知了这些,我们要做的就是:
- 保证列表页的滚动容器是body
- 缓存列表页的数据到状态管理,返回列表页的时候,直接使用缓存的数据
- 处理React中前进(跳转)时,总会保持scrollTop的问题
接下来请看第二篇的内容: