js实现随页面滚动切换的tab栏

最近项目有个需求,比较简单,就是顶部有tab栏,点击tab栏页面滚动到对应位置,手动的滚动页面tab栏也要随之切换。项目是基于vue2+ant design vue开发的,本来是想借用组件库来方便实现的,但是ui确实相差甚远,而且想着也是个简单功能,就放弃了使用组件库,转而用js来实现这个逻辑。

实现过程中,发现这虽然只是个简单的知识点,但确实也有一些要注意的地方。

各种各样的高度、距离

我们都知道浏览器里有各种各样的高度、距离等等。实现这个功能我们也免不了需要去计算这些高度,但是除了常用的几个,把所有高度都记住确实还比较困难,在此先回顾一下这些内容。

参考文章链接:前端页面内的高度、位置简述

屏幕的宽高

screen.height:屏幕高度。

screen.width:屏幕宽度。

screen.availHeight:屏幕可用高度。即屏幕高度减去上下任务栏后的高度,可表示为软件最大化时的高度。

screen.availWidth:屏幕可用宽度。即屏幕宽度减去左右任务栏后的宽度,可表示为软件最大化时的宽度。

任务栏的宽高就可以根据上面的内容去计算出来了。

浏览器宽高

window.outerHeight:浏览器高度

window.outerWidth:浏览器宽度

window.innerHeight:浏览器内页面可用高度。此高度包含了水平滚动条的高度(若存在)。可表示为浏览器当前高度去除浏览器边框、工具条后的高度。

window.innerWidth :浏览器内页面可用宽度;此宽度包含了垂直滚动条的宽度(若存在)。可表示为浏览器当前宽度去除浏览器边框后的宽度。

当然,屏幕与浏览器的宽高可能并不常用,加下来就是一些比较常用的内容了。

body页面的宽高

body.offsetHeight:body总高度

body.offsetWidth:body总宽度

body.clientHeight:body展示的高度

body.clientWidth:body展示的宽度

所有元素的位置

有些计算是涉及到盒子模型的,在此不过多赘述

clientHeight和clientWidth:元素的内尺寸,不包含边框

offsetHeight和offsetWidth:元素的外尺寸,包含边框, 不包含外边距

offsetTop和offsetLeft:元素的左上角距离已定位的父元素左上角的距离,如果找不到这个父元素那就是body

scrollLeft和scrollTop:元素被卷起的高度和宽度

点击tab栏 -> 页面滚动

首先说明一下,我的页面中tab栏是粘性定位的,高度为80,也就是说滚动后元素距离页面顶部的高度为80就是一个合适的位置。

页面中内容并没有被已定位的父元素包裹,所以元素的offsetTop就是其距离页面顶部的距离。当然如果你的内容是被已定位元素包裹的,你可以通过 元素的offsetTop + 其定位父元素的offsetTop来计算出元素距离页面顶部的距离。

1
2
// 拿到要滚动到页面顶部的内容元素
let elem = this.$refs.tab

页面滚动的操作,我们可以借助window.scrollTo这个api来实现,可以接受一个options对象,options对象接收两个参数topbehaviortop是页面卷起的高度,behavior是滚动的行为。

如果我们没有tab栏,那么卷起 元素距页面顶部的高度,就可以使元素位于页面顶部,但是有tab栏在顶部,所以我们需要去除这个高度。

1
2
3
4
window.scrollTo({
top: elem.offsetTop-80,
behavior: 'smooth' // smooth是平滑滚动
})

页面滚动 -> tab栏自动切换

此时我们可以通过点击tab栏实现页面滚动到合适的位置,反之我们手动滑动页面,tab栏也需要随之进行切换。

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
// 监听滚动事件,使用节流的原因就不赘述了
window.addEventListener('scroll',this.throttle(this.changeTabByScroll, 200))
// 节流函数
throttle(fn,delay){
let _this = this
let valid = true
// 这里之所以不用匿名函数,是为了页面卸载掉时候取消监听
function realScrollFn(){
if(!valid){
return;
}
valid = false
setTimeout(()=>{
fn()

valid = true
},delay)
}
// scroll事件绑定的函数,暴露到外部用于取消监听
_this.realScrollFn = realScrollFn
return realScrollFn
},

// 滚动的处理函数
changeTabByScroll(){
let _this = this
// 页面当前滚动到的位置
let scrollTop = document.documentElement.scrollTop
// 每个tab的ref
let refNames = ['content1', 'content2', 'content3', 'content4', 'content5', 'content6']
// 如果页面当前卷起的高度的小于最顶部的tab距离页面顶部的高度,那就选中第一个tab即可
if(scrollTop < _this.$refs['content1'][0].offsetTop){
_this.$refs['tabs'].changeTab('content1')
return;
}
// 不然对话对所有的元素进行遍历,对比元素距离页面顶部的高度与页面卷起的高度
refNames.forEach((item)=>{
let elemOffsetTop = _this.$refs[item][0].offsetTop
// 此处的数值是元素高度计算出来的,大家需要自己计算
if(scrollTop < elemOffsetTop + 400 && scrollTop > elemOffsetTop-151){
_this.$refs['tabs'].changeTab(item)
return;
}
})

}

// 页面卸载的时候取消事件的监听
window.removeEventListener('scroll',this.realScrollFn)

兼容问题

看起来现在好像没什么问题了对吗?

但是这引发了一个新的问题,我们滚动页面是可以自动切换tab没有问题的,不过我们点击tab的时候,也会触发页面的滚动,这个页面的滚动也会触发滚动事件,这就会导致我们点击tab的时候tab不能准确的切换到指定的tab,而是跳来跳去。

这让我想到了以前遇到的的一个问题,有两个联动的组件,一个是数值的输入,一个是拖动条,两个都可以用,我们改变了一个,另一个要随之改变,也是导致了一个变动,另一个随之改变,这个随之改变又导致第一个跟随着变动。。。。。。好家伙,开始套娃了。

这次我也采取了跟上次的问题差不多的解决措施,加一个中间变量。因为我们手动对页面进行滚动是没有问题的,只有点击tab的时候才会出现这个问题,所以我们加一个中间变量,用于标识页面的滚动是否是点击tab触发的,如果是,我们就不执行滚动事件。

我用的方法比较low,就是加了个定时器

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
// 点击tab事件
changeSelectedTab(key){
let _this = this
// 标记为通过点击tab触发的滚动
_this.byClick = true
// ...执行正常的逻辑
// 添加一个定时器,待指定时间后,再将该值置为false
setTimeout(()=>{
_this.byClick = false
},500)
},

// 节流的函数
throttle(fn,delay){
let _this = this
let valid = true
// 闭包中定义一个有名函数,匿名函数会导致没法取消监听
function realScrollFn(){
// 将这个中间变量加进来,如果是因为点击tab触发的滚动,就不执行函数
if(!valid || _this.byClick){
return;
}
valid = false
setTimeout(()=>{
fn()

valid = true
},delay)
}
// scroll事件绑定的函数,暴露到外部用于取消监听
_this.realScrollFn = realScrollFn
return realScrollFn
},

最开始是不想通过定时器来做的,但是查阅了关于window.scrollTo这个api,并没有提供完成之后才执行的回调函数,就拿定时器这种取巧的方法来实现了。大家也可以通过promise等方法来准确的控制哦。

这么简单的内容还啰嗦了这么就,感谢能看到这儿。

作者

胡兆磊

发布于

2021-11-22

更新于

2022-10-23

许可协议