在 web 的开发过程中,有时我们需要实现当点击一个按钮或超链接时,立刻滚动跳转到本页面中指定的位置。对此有很多的实现方式,但大体可以分为以下两类:

  • 通过 html 锚点实现
  • 通过 js 实现
    • 通过 scrollTo() 实现
    • 通过 Element.scrollIntoView()实现

今天的这篇文章,将讨论上述两类的实现原理与各自的优缺点。鉴于各种实现的 html 结构与 css 样式是共有的,所以我先将实现示例的共有的 html 结构与 css 样式列出来。

html 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html>
<ul class="list">
<a name="topAnchor"></a>
<li class="list__item" id="top">我是顶部</li>
<li class="list__item"></li>
<li class="list__item"></li>
<li class="list__item"></li>
<li class="list__item"></li>
<li class="list__item"></li>
</ul>
<div class="operation">
<!--
代码片段1
-->
<button class="operation__back-top" id="backTopBtn2">
回到顶部
</button>
<button class="operation__back-top" id="backTopBtn3">
回到顶部
</button>
</div>
</html>

css 样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<style>
.list li {
display: flex;
align-items: center;
justify-content: center;
height: 12vh;
margin-bottom: 10px;
background-color: aqua;
}
.operation {
line-height: 120px;
text-align: center;
}
</style>

通过锚点实现

对于这种实现方式,我们只需要在 html 结构的”代码片段 1”注释处插入以下代码:

1
<a href="#topAnchor">回到顶部</a>

或者采用 js 生成 html 的方式:

先在”代码片段 1”注释处插入以下代码:

1
2
3
<button class="operation__back-top" id="backTopBtn1">
回到顶部
</button>

再添加下面的 javascript 代码:

1
2
3
4
5
6
7
8
9
10
11
const backTopBtn1 = document.getElementById('backTopBtn1');
backTopBtn1.addEventListener('click', function(e) {
const aNode = document.createElement('a');
aNode.href = '#topAnchor';
e.target.appendChild(aNode);
aNode.onclick = function(e) {
e.stopPropagation();
};
aNode.click();
e.target.removeChild(aNode);
});

上述方法,在点击 a 标签或者按钮后,我们会看到页面立马跑到顶部。对于交互性要求不高的项目,这种做法没什么问题,同时不同浏览器之间的兼容性问题也不大。但如果我们的项目交互性要求高了之后就不行了,这种一下子就跑到页面顶部的交互方式会让用户觉得有些突兀。通过查询资料,可以在 style 中设置 html, body { scroll-behavior:smooth; }让过度变得比较平滑一些。但更好的解决方式还是通过 js 来实现。下面我将着重介绍通过 js 的实现方式。

通过 scrollTo() 实现:

此 api 需要传递 DOM 元素相对于 window 的 left 和 top 的距离,当然它还有一个 behavior 参数,将其设置为 smooth 后,过度就会比较平滑一些。步骤如下:

  1. 计算目标元素距离顶部的距离
  2. 通过事件触发

实现代码如下:

1
2
3
4
5
6
const backTopBtn2 = document.getElementById('backTopBtn2');
const TOP = document.getElementById('top');
const y = TOP.offsetTop;
backTopBtn2.addEventListener('click', function(e) {
window.scrollTo({ top: y, left: 0, behavior: 'smooth' }); // 此例子仅展示简单 demo,只考虑 top 坐标
});

相较于第一种实现方式,scrollTo 支持动画,使得页面跳转会更丝滑,但它对 iframe 的支持度不够。

通过 Element.scrollIntoView()实现:

该 api 相较于上一个,节点信息更加的明确,操作方法也更加的简洁,更利于后续的维护。实现代码如下:

1
2
3
4
5
const backTopBtn3 = document.getElementById('backTopBtn3');
const TOP = document.getElementById('top');
backTopBtn3.addEventListener('click', function(e) {
TOP.scrollIntoView({ behavior: 'smooth' });
});

从效果上来看,该 api 和 scrollTo 的作用是一致的,但是从代码结构上来说,scrollIntoView 会更加的简洁且在 iframe 中表现也很优秀,基本上被用到的频率更高。

然后,参考 eleUI 中的 backTop 组件源码,我又做了下面这个版本:

1
2
3
4
5
6
7
8
9
10
11
12
const distance = 70; // 滚动条每次滚动的距离
function up() {
let start = document.documentElement.scrollTop;
function gotop() {
scrollTo(0, start - distance);
start = start - distance;
if (start > 0) {
window.requestAnimationFrame(gotop);
}
}
window.requestAnimationFrame(gotop);
}

eleUI 中的 backTop 组件源码的实现方式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const cubic = value => Math.pow(value, 3);
const easeInOutCubic = value => value < 0.5
? cubic(value * 2) / 2
: 1 - cubic((1 - value) * 2) / 2;

scrollToTop() {
const el = this.el; // el如果是一个盒子,滚动条会在该盒子内滚动,若没被传参,则为整个页面
const beginTime = Date.now();
const beginValue = el.scrollTop;
const rAF = window.requestAnimationFrame || (func => setTimeout(func, 16));
const frameFunc = () => {
const progress = (Date.now() - beginTime) / 500;
if (progress < 1) {
el.scrollTop = beginValue * (1 - easeInOutCubic(progress));
rAF(frameFunc);
} else {
el.scrollTop = 0;
}
};
rAF(frameFunc);
}

上面的源码中,用了一个数学曲线来搞的滚动,这样可以让无论页面内容多高,回到顶部都是差不多的时间。而我的却是写死的 70,这样界面越长,回到顶部就会花越多的时间。

参考资料: