这篇文章是三天前写就的,有大佬给我提了一些修改意见,我以为这个意见确实中肯。因此就有了这个升级的修改版本。代码同步更新到 GitHub 了。html
修改内容以下:前端
getBoundingClientRect()
集合含义IntersectionObserver
, 新方案)修改更新的内容在第 4 点和第 5 点,若是你看过本文,能够直接看修改更新的内容。或者你能够再看一遍。vue
我入职第二家公司接到的第一个需求就是修复以前外包作的滚动吸顶效果。我当时很纳闷为什么一个滚动吸顶会有 bug,后来我查看代码才发现直接用的是 offsetTop
这个属性,并且并无作兼容性处理。ios
offsetTopgit
用于得到当前元素到定位父级( element.offsetParent )顶部的距离(偏移值)。github
定位父级
offsetParent
的定义是:与当前元素最近的 position != static 的父级元素。web
或许写这个代码的人没有注意到“定位父级”这个这个附属条件。api
后来在项目中总会遇到滚动吸顶的效果须要实现,如今我将我知道的 4 种滚动吸顶实现方式作详细介绍。浏览器
position:sticky
实现offset().top
实现offsetTop
实现obj.getBoundingClientRect().top
实现以上这四种方式你都了解吗?相关代码已上传到 GitHub ,感兴趣的能够 clone 代码到本地运行。望给个 star 支持一下。缓存
咱们先看一下效果图:
1、使用 position:sticky
实现
一、粘性定位是什么?
粘性定位 sticky
至关于相对定位 relative
和固定定位 fixed
的结合;在页面元素滚动过程当中,某个元素距离其父元素的距离达到 sticky
粘性定位的要求时;元素的相对定位 relative
效果变成固定定位 fixed
的效果。
二、如何使用?
使用条件:
overflow:hidden
或者 overflow:auto
属性top、bottom、left、right
4 个值之一,不然只会处于相对定位sticky
元素的高度sticky
元素仅在其父元素内生效在须要滚动吸顶的元素加上如下样式即可以实现这个效果:
.sticky {
position: -webkit-sticky;
position: sticky;
top: 0;
}
复制代码
三、这个属性好用吗?
咱们先看下在 Can I use 中看看这个属性的兼容性:
能够看出这个属性的兼容性并非很好,由于这个 API 还只是实验性的属性。不过这个 API 在 IOS 系统的兼容性仍是比较好的。
因此咱们在生产环境若是使用这个 API 的时候通常会和下面的几种方式结合使用。
2、使用 JQuery 的 offset().top
实现
咱们知道 JQuery 中封装了操做 DOM 和读取 DOM 计算属性的 API,基于 offset().top
这个 API 和 scrollTop()
的结合,咱们也能够实现滚动吸顶效果。
...
window.addEventListener('scroll', self.handleScrollOne);
...
handleScrollOne: function() {
let self = this;
let scrollTop = $('html').scrollTop();
let offsetTop = $('.title_box').offset().top;
self.titleFixed = scrollTop > offsetTop;
}
...
复制代码
这样实现当然能够,不过因为 JQuery 慢慢的退出历史的舞台,咱们在代码中尽可能不使用 JQuery 的 API。咱们能够基于 offset().top
的源码本身处理原生 offsetTop
。因而乎就有了第三种方式。
scrolloTop() 有兼容性问题,在微信浏览器、IE、某些 firefox 版本中 $('html').scrollTop() 的值会为 0,因而乎也就有了第三种方案的兼容性写法。
3、使用原生的 offsetTop
实现
咱们知道 offsetTop
是相对定位父级的偏移量,假若须要滚动吸顶的元素出现定位父级元素,那么 offsetTop
获取的就不是元素距离页面顶部的距离。
咱们能够本身对 offsetTop
作如下处理:
getOffset: function(obj,direction){
let offsetL = 0;
let offsetT = 0;
while( obj!== window.document.body && obj !== null ){
offsetL += obj.offsetLeft;
offsetT += obj.offsetTop;
obj = obj.offsetParent;
}
if(direction === 'left'){
return offsetL;
}else {
return offsetT;
}
}
复制代码
使用:
...
window.addEventListener('scroll', self.handleScrollTwo);
...
handleScrollTwo: function() {
let self = this;
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
let offsetTop = self.getOffset(self.$refs.pride_tab_fixed);
self.titleFixed = scrollTop > offsetTop;
}
...
复制代码
你是否是看出了以上两种方式的一些问题?
咱们必定须要使用
scrollTop - offsetTop
的值来实现滚动吸顶的效果吗?答案是否认的。
咱们一同看看第四种方案。
4、使用 obj.getBoundingClientRect().top
实现
定义:
这个 API
能够告诉你页面中某个元素相对浏览器视窗上下左右的距离。
使用:
tab 吸顶可使用 obj.getBoundingClientRect().top
代替 scrollTop - offsetTop
,代码以下:
// html
<div class="pride_tab_fixed" ref="pride_tab_fixed">
<div class="pride_tab" :class="titleFixed == true ? 'isFixed' :''">
// some code
</div>
</div>
// vue
export default {
data(){
return{
titleFixed: false
}
},
activated(){
this.titleFixed = false;
window.addEventListener('scroll', this.handleScroll);
},
methods: {
//滚动监听,头部固定
handleScroll: function () {
let offsetTop = this.$refs.pride_tab_fixed.getBoundingClientRect().top;
this.titleFixed = offsetTop < 0;
// some code
}
}
}
复制代码
1. getBoundingClientRect():
用于得到页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。不包含文档卷起来的部分。
该函数返回一个 object
对象,有8个属性: top, right, buttom, left, width, height, x, y
2. offsetTop:
用于得到当前元素到定位父级( element.offsetParent )顶部的距离(偏移值)。
定位父级
offsetParent
的定义是:与当前元素最近的 position != static 的父级元素。
offsetTop
和 offsetParent
方法相结合能够得到该元素到 body
上边距的距离。代码以下:
getOffset: function(obj,direction){
let offsetL = 0;
let offsetT = 0;
while( obj!== window.document.body && obj !== null ){
offsetL += obj.offsetLeft;
offsetT += obj.offsetTop;
obj = obj.offsetParent;
}
if(direction === 'left'){
return offsetL;
}else {
return offsetT;
}
}
复制代码
offsetWidth:
元素在水平方向上占用的空间大小:
offsetWidth = border-left + padding-left + width + padding-right + border-right
offsetHeight:
元素在垂直方向上占用的空间大小:
offsetHeight = border-top + padding-top + height + padding-bottom + border-bottom
注:若是存在垂直滚动条,offsetWidth 也包括垂直滚动条的宽度;若是存在水平滚动条,offsetHeight 也包括水平滚动条的高度;
offsetTop:
元素的上外边框至 offsetParent
元素的上内边框之间的像素距离;
offsetLeft:
元素的左外边框至 offsetParent
元素的左内边框之间的像素距离;
注意事项
display:none
,则它的偏移量属性都为 0;出现抖动的缘由是由于:在吸顶元素 position 变为 fixed 的时候,该元素就脱离了文档流,下一个元素就进行了补位。就是这个补位操做形成了抖动。
解决方案
为这个吸顶元素添加一个等高的父元素,咱们监听这个父元素的 getBoundingClientRect().top
值来实现吸顶效果,即:
<div class="title_box" ref="pride_tab_fixed">
<div class="title" :class="titleFixed == true ? 'isFixed' :''">
使用 `obj.getBoundingClientRect().top` 实现
</div>
</div>
复制代码
这个方案就能够解决抖动的 Bug 了。
这个问题是我比较头痛,以前我没有在乎过这个问题。直到有一天我用美团点外卖的时候,我才开始注意这个问题。
描述:
缘由: 在 ios 系统上不能实时监听 scroll 滚动监听事件,在滚动中止时才触发其相关的事件。
解决方案:
还记得第一种方案中的 position:sticky
吗?这个属性在 IOS6 以上的系统中有良好的兼容性,因此咱们能够区分 IOS 和 Android 设备作两种处理。
IOS 使用
position:sticky
,Android 使用滚动监听getBoundingClientRect().top
的值。
若是 IOS 版本太低呢?这里提供一种思路:window.requestAnimationFrame()
。
性能优化篇(新增)
到此 4 中滚动吸顶的方式介绍完了,但是这样就真的结束了吗?其实仍是有优化的空间的。
咱们从两个方向作性能优化(实际上是一个方向):
咱们知道过分的 reflow 会使页面的性能降低。因此咱们须要尽量的下降 reflow 的次数,给用户更加流畅的感受。
有的朋友或许会说这个我知道,但是这和滚动吸顶有什么关系呢?
不急,你是否还记得滚动吸顶使用了 offsetTop 或者 getBoundingClientRect().top 来获取响应的偏移量呢?
既然有读取元素的属性就天然会致使页面 reflow。
所以咱们优化的方向就是从减小读取元素属性次数下手,查看代码发现一触发屏幕滚动事件就会调用相关方法读取元素的偏移量。
解决这个问题有如下两个方案:
这个方案很常见,不过其带来的反作用也很明显,就是在吸顶效果会有些延迟,若是产品能够接受那就不失为一种好方法。
这样能够控制在必定时间内只读取
这里节流函数就直接是用 lodash.js
封装好的 throttle
方法。
代码以下:
window.addEventListener('scroll', _.throttle(self.handleScrollThree, 50));
复制代码
第二种方案相对来讲容易接受一点,就是支持 IntersectionObserver 就用 IntersectionObserver,不然就用 throttle。
咱们先讲讲 IntersectionObserver
IntersectionObserver 能够用来监听元素是否进入了设备的可视区域以内,而不须要频繁的计算来作这个判断。
经过这个属性咱们就能够在元素不在可视范围内,不去读取元素的相对位置,已达到性能优化;当浏览器不支持这个属性的时候就使用 throttle 来处理。
咱们看看这个属性的兼容性怎么样:
大概支持了 60% 以上,在项目中仍是可使用的(你须要作好兼容性处理)。
使用 IntersectionObserver 和 throttle 优化的代码以下:
IntersectionObserverFun: function() {
let self = this;
let ele = self.$refs.pride_tab_fixed;
if( !IntersectionObserver ){
let observer = new IntersectionObserver(function(){
let offsetTop = ele.getBoundingClientRect().top;
self.titleFixed = offsetTop < 0;
}, {
threshold: [1]
});
observer.observe(document.getElementsByClassName('title_box')[0]);
} else {
window.addEventListener('scroll', _.throttle(function(){
let offsetTop = ele.getBoundingClientRect().top;
self.titleFixed = offsetTop < 0;
}, 50));
}
},
复制代码
注意
IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。
规格写明,IntersectionObserver的实现,应该采用 requestIdleCallback()。它不会当即执行回调,它会调用 window.requestIdleCallback()
来异步的执行咱们指定的回调函数,并且还规定了最大的延迟时间是 100 毫秒。
总结:
这种 IntersectionObserver 和 throttle 结合的方案不失为一种可选择的方案,这种方案的优势就在于能够有效地减小页面 reflow 的风险,不过缺点也是有的,须要牺牲页面的平滑度。具体该如何取舍,就看业务的须要啦。
《前端词典》这个系列会持续更新,每一期我都会讲一个出现频率较高的知识点。但愿你们在阅读的过程中能够斧正文中出现不严谨或是错误的地方,本人将不胜感激;若经过本系列而有所得,本人亦将不胜欣喜。
若是你以为个人文章写的还不错,能够关注个人微信公众号,公众号里会提早剧透呦。
若是你想加群交流,也能够添加有点智能的机器人,自动拉你进群: