关于滚动吸顶

所有人其实就是一个整体,别人的不幸就是你的不幸。不要以为丧钟为谁而鸣,它就是为你而鸣。

1.题外话

说句题外话,请问对于下面的代码是输出什么信息呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<style type="text/css">
*, html, body {
margin: 0;
padding: 0;
}
.p1 {
position: relative;
width: 100vw;
height: 300px;
padding: 60px;
border: 1px solid green;
}
.p2 {
width: 100%;
height: 100px;
border: 1px solid red;
padding: 20px;
}
.c {
width: 100%;
height: 50px;
background: yellow;
}
</style>
<body>
<div class="p1">
<div class="p2">
<div class="c"></div>
</div>
</div>

<script type="text/javascript">
window.onload = function () {
let c = document.querySelector('.c');
console.log('c.offsetTop', c.offsetTop);
}
</script>
</body>

求元素c的offsetTop?是20吗还是21?答案是都不是,为什么呢?因为对于offsetTop的定义是用来获取其相对于自己的定位父级顶部的距离,那么问题来了,这个定位父级是怎么定义的呢?答案就是与当前元素最近的position != static的元素。因此,对于上面这种场合来说,输出的结果应该是81px。

下面言归正传,首先介绍使用position: sticky的形式实现滚动置顶。

2.黏性定位之position:sticky

先看黏性定位的定义:结合了 position:relative 和 position:fixed 两种定位功能于一体的特殊定位,适用于一些特殊场景。元素先按照普通文档流定位,然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。而后,元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。

接下来带着问题加深理解下新朋友黏性定位(position: sticky):

问1.黏性定位position:sticky会有这么样的展示效果?

答:首先了解下使用方法,要使用黏性定位的话那么使用黏性定位的元素还需要搭配定位属性top,bottom,left,right四者之一。并且使用黏性定位的元素其任意一个父元素(包括祖先元素)都不能够将overflow设置为除了visible之外的其它值,否则的话黏性定位将会没有效果,所以如果某个时候你发现你写的黏性定位不起作用的话,先检查一下是否是因为设置黏性定位元素的父元素或者是父元素是不是设置了overflow为非visible之外的值。下面通过一个例子讨论展示效果是怎么呈现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<style type="text/css">
.container {
width: 100vw;
height: 200vh;
margin-top: 100px;
}
.header {
box-sizing: border-box;
width: 100vw;
height: 50px;
background: yellow;
position: sticky;
top: 60px;
}
</style>
<div class="container">
<div class="header"></div>
</div>

在上面的代码中,我们设置了header的阈值是60px,这就表明着当header距离视窗的高度小于等于60px的时候,此时header将会固定在离视窗60px处的位置。这也就意味着只需要container滚动40px就能看到这种滚动置顶的效果。

问2.黏性定位position:sticky的元素的定位上下文是谁?

答:就视觉表现来看,我将其理解为两种行为:分别是距离自己最近的父级块元素和视窗本身。那么这两种行为在什么时候触发呢?触发了黏性定位所设置的那个阈值的时候,如果当黏性定位的元素距离视窗的距离大于阈值的话,那么他的定位上下文就是父元素本身;如果当黏性定位的元素距离视窗的距离小于阈值的话,那么他的定位上下文就是视窗自身了。

黏性定位就使用方法来说还是很方便的,但是兼容性问题不是特别好。

3.根据offsetTop来实现滚动吸顶

在前面我们也提到过了,那就是一个元素的offsetTop值是相对于其定位父级的距离值,这个定位父级的position值为非static的父级元素或者body。那么问题来了,如果我们需要滚动置顶的元素的父级或者祖先级别元素有某个使用了定位属性的话,那么就会出现滚动吸顶不准确的情况了,如何解决?思路大致如下所示:不断调用offsetTop,没调用一次向上延伸到定位父级元素上面。直到到了body为止,下面介绍一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<style type="text/css">
html, body {
margin: 0;
padding: 0;
background: #fff;
}
.wrapper {
position: relative;
margin-top: 60px;
}
.content {
width: 100vw;
height: 150px;
background: #f0f0f0;
margin-bottom: 20px;
}
.header {
width: 100vw;
height: 50px;
background: orange;
margin-bottom: 20px;
}
.fixedHeader {
position: fixed;
top: 0;
left: 0;
}
</style>
<body>
<div class="wrapper">
<div class="content"></div>
<div class="header"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
</div>
</body>
<script type="text/javascript">
function computeOffset (ele, direction) {
let offsetY = 0;
let offsetX = 0;
while (ele != window.document.body && ele != null) {
offsetY += ele.offsetTop;
offsetX += ele.offsetLeft;
ele = ele.offsetParent;
}
if (direction == 'left') {
return offsetX;
} else {
return offsetY;
}
}

function checkClassNameExists (ele, className) {
let arr = Array.from(ele.classList);
if (arr.indexOf(className) !== -1) {
return true;
} else {
return false;
}
}

function fixedHeader (ele) {
if (checkClassNameExists(ele, 'fixedHeader')) return;
ele.classList.add('fixedHeader');
}

function normalHeader (ele) {
if (!checkClassNameExists(ele, 'fixedHeader')) return;
ele.classList.remove('fixedHeader');
}

window.onload = function () {
let headerEle = document.querySelector('.header');
let truelyOffset = computeOffset(headerEle);

window.addEventListener('scroll', function () {
let scrollY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
if (scrollY > truelyOffset) {
fixedHeader(headerEle);
} else {
normalHeader(headerEle);
}
});
}
</script>

要避免这里是相对于定位父级的offset,所以这里一直在取其相对的定位父级,直到到了body或者到了null的时候。

拓展:使用document.documentElement.style可以获取到浏览器所支持的css属性列表数据。