开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情
需求
这几天做一个大屏可视化项目时,遇到一个需求,当某区域出现滚动条时,让它循环滚动展示数据。
当然,作为一个优秀的开发,我们还可以优化一下:鼠标放上去的时候,停止自动滚动;鼠标移开区域,重新开始滚动。
实现
基本实现思路
实现功能之前,我们需要了解三个参数
- 可视高度clientHeight
- 滚动条距离顶部的高度scrollTop
- 滚动区域高度scrollHeight
根据上图的信息,我们很容易实现滚动条的自动滚动逻辑:
scrollHeight 大于clientHeight时,元素需要滚动,scrollTop++;当scrollTop + clientHeight等于scrollHeight时,即元素滚动到底部,我们再让scrollTop等于0即可。
转换成代码:
// 获取滚动区域的DOM
let parentDom = document.getElementById("XXX")
// 判断是否有滚动条
if(parentDom.scrollHeight <= parentDom.clientHeight) return
// 如果滚动到底部,scrollTop = 0;
if (
parentDom.scrollTop + parentDom.clientHeight ===
parentDom.scrollHeight
) {
parentDom.scrollTop = 0;
} else {
// 滚动条自增
parentDom.scrollTop++;
}
当然,我们需要设置一个定时器,让这个逻辑重复执行。
const scroll = () => {
// 上述代码
};
let timer= setInterval(scroll, 100);
至此,我们的基本思路就完成了。当我们项目中有多个这样需要滚动的区域时,我们应该将这个方法封装,进行复用。
代码封装与复用
我们以主流的vue为例,进行代码封装,最合适的方式应该是将其作为vue中的指令使用。
阅读以下教程,需要您具备vue指令基础知识,可以参考vue官网教程。
指令的基本结构
首先,我们创建scroll.js文件,写入指令的最基本结构
// 实现滚动条自动滚动功能
const directive = {
inserted(el, binding) {
...
},
};
export default directive
main.js中引入该文件
import Vue from "vue";
import App from "./App.vue";
import directive from "./change";
Vue.config.productionTip = false;
// 注册名为 scroll 的指令
Vue.directive("scroll", directive);
new Vue({
render: (h) => h(App),
}).$mount("#app");
此时,我们创造了一个名为v-scroll的指令,我们直接使用
// App.vue
<template>
<div id="app" v-scroll>
// XXXXX这里是您的自定义html
</div>
</template>
完善指令
结合我们最开始的实现思路,可以完善指令
// js实现动态变化的数字
const directive = {
inserted(el) {
//el就是绑定的dom对象
if (!el) return;
const scroll = () => {
let parentDom = el;
// 判断是否有滚动条
if(parentDom.scrollHeight <= parentDom.clientHeight) return
//判断元素是否滚动到底部(可视高度+距离顶部=整个高度)
if (
parentDom.scrollTop + parentDom.clientHeight ===
parentDom.scrollHeight
) {
parentDom.scrollTop = 0;
} else {
parentDom.scrollTop++; // 元素自增距离顶部
}
};
let timer = setInterval(scroll, 100);
};
export default directive;
现在我们指令的功能就完成了。当然,它还有个缺陷,鼠标移入这个元素的时候不能自动停止,移开这个区域不能恢复自动滚动。我们只需要在dom上绑定onmouseenter和onmouseleave事件即可。
inserted(el) {
// .....
let timer = setInterval(scroll, rate);
el.onmouseenter = () => {
clearInterval(timer);
timer = null;
};
el.onmouseleave = function () {
timer = setInterval(scroll, 100);
};
},
},
指令传值
更优的,这个滚动速度我们还可以自定义,通过元素来传值。vue指令钩子函数的的第二个参数binding.value可以接受参数。
// App.vue
<template>
<div id="app" v-scroll="100">
// XXXXX这里是您的自定义html
</div>
</template>
此时,binding.value的值就是100,我们进一步优化代码
inserted(el, binding) {
const rate = binding?.value ?? 100;
if (!el) return;
const scroll = () => {
// ...省略
};
let timer: any = setInterval(scroll, rate);
el.onmouseenter = () => {
clearInterval(timer);
timer = null;
};
el.onmouseleave = function () {
timer = setInterval(scroll, rate);
};
},
大功告成!!
看完有收获的话,来个一键三连吧!
兼容性处理方案
上面的方法很大程度解决了业务的需求,但也有不足的地方:
1.如果数据会定时刷新获取,数据变化导致滚动区域高度发生变化时,由于滚动条使用的是初始化时的高度设置,此时滚动条自动滚动功能会出现异常。(其他方式更改浏览器高度时也会导致此问题)
2.在浏览器缩放小于百分之百时,滚动功能也会出现异常。
dom变化时的兼容性处理
当用户增加或者隐藏浏览器的书签栏、全屏浏览器、手动拖动浏览器窗口时,都会导致滚动区域dom高度变化。
此外,滚动区域数据更新也会导致滚动区域dom高度发生变化。
能否使用指令的 componentUpdated 钩子?
我们知道自定义指令除了bind钩子,还有其他钩子函数。
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
通过官方解释,我们可以知道,如果浏览器dom高度发生变化时(数据更新重新渲染导致),componentUpdated钩子一定会执行。所以,我们将指令的钩子改为componentUpdated可以解决问题吗?
答案是不可以的,因为每次dom更新都会重新为当前dom元素添加定时器,最后,会导致滚动速度越来越快。
除非,每次添加定时器前,我们先判断存在定时器与否,如果存在,将其清除即可。
为了方便,我们直接将指令改为函数写在vue文件里,便于控制。
<template>
<div class="main" ref="rank"></div>
</template>
export default {
data() {
return {
timer:null,
};
},
methods:{
addTimer(){
this.$nextTick( ()=> {
const el = this.$refs.rank
if (!el) return;
if(this.timer){
clearInterval(this.timer);
this.timer = null;
}
// 指令里面的代码copy过来
// .....
)
},
}
}
注意,这里我们使用了 this.$nextTick,必须保证dom渲染后才执行滚动条相关逻辑
那么,如果在浏览器高度变化时,调用addTimer这个方法呢?
很简单,使用window的resize方法。
mounted(){
window.addEventListener('resize',this.addTimer() )
},
那么,dom高度变化的问题我们用这个方法就可以完美解决了。
缩放时兼容性处理
为什么缩放时,会导致滚动条滚动异常呢?这和浏览器的window.devicePixelRatio接口有关
Window 接口的devicePixelRatio返回当前显示设备的物理像素分辨率与CSS像素分辨率之比。 此值也可以解释为像素大小的比率:一个CSS像素的大小与一个物理像素的大小。 简单来说,它告诉浏览器应使用多少屏幕实际像素来绘制单个CSS像素。
当我们不缩放屏幕时,打印这个值
console.log(window.devicePixelRatio);
// 1
当我们屏幕缩放到110%时,打印这个值
console.log(window.devicePixelRatio);
// 1.100000023841858
当我们屏幕缩放到90%时,打印这个值
console.log(window.devicePixelRatio);
// 0.8999999761581421
可见,在屏幕不缩放时,屏幕的物理像素与css的像素之比是一比一,进行缩放时,这个值会变,这就是导致滚动条失效的原因。
那么,修改代码也就非常容易了
methods:{
addTimer(){
this.$nextTick( ()=> {
const el = this.$refs.rank
if (!el) return;
if(this.timer){
clearInterval(this.timer);
this.timer = null;
}
const rate = window.time.scroll ?? 100;
let parentDom = el;
let scrollAll = parentDom.scrollHeight - parentDom.clientHeight
const ratio = parseFloat((1 / window.devicePixelRatio).toFixed(2) ) + 0.01
const scroll = () => {
//判断元素是否滚动到底部(可视高度+距离顶部=整个高度)
if ( parentDom.scrollTop > scrollAll - 2 ) {
parentDom.scrollTop = 0;
} else {
parentDom.scrollTop += ratio; // 元素自增距离顶部
}
};
this.timer = setInterval(scroll, rate);
el.onmouseenter = () => {
clearInterval(this.timer);
this.timer = null;
};
el.onmouseleave = () => {
this.timer = setInterval(scroll, rate);
};
}
)
},
},
至此,大工告成!