JS如何统一滚动条样式

CSS滚动条样式

之前重构微博私信的时候,地址,要保持各个浏览器滚动条样式的统一。
在webkit内核浏览器我们可以这样定义整个页面的滚动条样式:

1
2
3
4
5
6
7
8
9
10
11
12
::-webkit-scrollbar {  //隐藏滚轮display: none;
width: 6px;
height: 6px;
}

::-webkit-scrollbar-track-piece {
background: #eee;
}

::-webkit-scrollbar-thumb:vertical {
background: #666;
}

其他浏览器我们可以使用CSS自己写一个div模拟滚动条:

如何知道滚动条宽度

util.js

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
let cached
export function getScrollBarSize(fresh) { //获取滚动条宽度
if(fresh || cached === undefined) {
const inner = document.createElement('div')
inner.style.width = '100%'
inner.style.height = '200px'

const outer = document.createElement('div')
const outerStyle = outer.style

outerStyle.position = 'absolute'
outerStyle.top = 0
outerStyle.left = 0
outerStyle.pointerEvents = 'none'
outerStyle.visibility = 'hidden'
outerStyle.width = '200px'
outerStyle.height = '150px'
outerStyle.overflow = 'hidden'

outer.appendChild(inner)

document.body.appendChild(outer)

const widthContained = inner.offsetWidth
outer.style.overflow = 'scroll'
let widthScroll = inner.offsetWidth

if(widthContained === widthScroll) {
widthScroll = outer.clientWidth
}

document.body.removeChild(outer)

cached = widthContained - widthScroll
}
return cached || 15 //MAC可以设置滚动条显示形式
}

export const debounce = function(func, wait, immediate) { //节流
let timeout, args, context, timestamp, result;

let later = function() {
let last = Date.now() - timestamp; // 获取现在与上一次防抖函数的运行间隔时间

if(last < wait && last >= 0) {
// 间隔太小,频率过多,继续延迟
timeout = setTimeout(later, wait - last); // wait - last为接下来不触发防抖的期望时间
} else {
timeout = null; // 间隔够长,运行函数
if(!immediate) {
result = func.apply(context, args);
if(!timeout) context = args = null;
}
}
};

return function(..._args) {
context = this;
args = _args;
timestamp = Date.now(); //刷新最新一次调用该防抖函数的时间戳
let callNow = immediate && !timeout; // 是否需要立即调用一次,
if(!timeout) timeout = setTimeout(later, wait); // 同一时间内只存在一个超时
if(callNow) {
// 立即调用一次
result = func.apply(context, args);
context = args = null;
}
// 短时间内触发多次不会调用原函数
return result;
};
};



export function randomColor() { //生成随机色
return '#' + ('00000' + (Math.random() * 0x1000000 << 0).toString(16)).slice(-6);
}

scrollbars.vue

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
<template>
<div class="scroller-wrapper" ref="wrapper">

<div class="content-wrapper" ref="slot" @scroll="onScrollHandle" @mouseenter="showBar" @mouseleave="yLeaveHandle">
<slot></slot>
</div>

<div class="y-bar-outer bar" ref="ybar">
<div class="inner"
@mousedown="yMouseDownHandle"
:class="{move:move.yMouseDown}"
:style="`height:${yBarHeight}px;opacity:${yBarOpa};transform:translateY(${yInnerTop}px)`"></div>
</div>
</div>
</template>

<script>
import { getScrollBarSize, debounce, randomColor } from "./util.js";

export default {
props: {
autoHide: { //滚动条自动隐藏
type: Boolean,
default: false
},
forData: {
type: [Array,Number],
default: function() {
return [];
}
},
ScrollTop:{
type: Number,
default: 0
},
listenScrollBottom: {
type: [Function, Boolean],
default: ()=>{}
},
listenScrollTop:{
type: [Function, Boolean],
default: ()=>{}
},
listenScroll:{
type: [Function, Boolean],
default: ()=>{}
}
},

data() {
return {
yBarHeight: 0, //滑块高度
yBarOpa: 1, //滑块透明度
yInnerTop: 0, //滑块top距离

info: {
// 刷新的时候才更新 减少读取 增加性能
wrapperHeight: 0, //外容器高度
contentHeight: 0, //内部总高度
yBarHeight: 0 //滑轨高度
},
move: {
yMouseDown: false,
yMouseDownOffset: 0
}
};
},
mounted() {
// 注册节流函数
this.debounceHide = debounce(() => {
// 放置在此处的判断是个妥协
// 尽管会可读性 但是这个判断过于重要 几乎所有地方都需要达成条件才触发自动隐藏
// 并且在外部判断 由于延时的特性会导致一些同步问题
if(this.autoHide && !this.move.yMouseDown) {
this.hideBar();
}
}, 1000);

// 提示用户可以滚动
if(this.autoHide) {
this.showBar();
this.debounceHide();
}

this.refresh();
},
methods: {
refresh() {
this.setContentOffset(); //-17px
this.reGetInfo(); //获取滚动条信息

if(this.info.contentHeight <= this.info.wrapperHeight + 1) {
this.yBarHeight = 0;
} else {
this.yBarHeight = (this.info.wrapperHeight / this.info.contentHeight) * this.info.yBarHeight; //高度百分比=>滑块高度
}

this.onScrollHandle(); //更新滑块top
},
setContentOffset() {
// 获取该设备的默认滚动条宽度 依赖这个宽度把滚动条顶出去
let scrollBarWidth = getScrollBarSize();
this.$refs.slot.style.marginRight = `${-scrollBarWidth}px`;
this.$refs.slot.style.marginBottom = `${-scrollBarWidth}px`;
},
reGetInfo() {
// 获取基本的宽高信息
this.info.wrapperHeight = this.$refs.wrapper.clientHeight;
this.info.contentHeight = this.$refs.slot.children[0].clientHeight;
this.info.yBarHeight = this.$refs.ybar.clientHeight;
},
onScrollHandle() {
// 自动隐藏
this.showBar();
this.debounceHide();

const slotScrollTop = this.$refs.slot.scrollTop;

const yScrollPercent = slotScrollTop / this.info.contentHeight; //计算scrollTop所占容器高度百分百
this.yInnerTop = this.info.yBarHeight * yScrollPercent; //滑块top

this.listenScroll(slotScrollTop);
// 监听滚动到底部
if(this.listenScrollBottom) {
const bottomHeight = this.info.contentHeight - this.info.wrapperHeight - slotScrollTop;

console.log('bottomHeight',bottomHeight)

if(bottomHeight <= 0.5) {
this.listenScrollBottom();
}
}
if(this.listenScrollTop) {

if(slotScrollTop == 0) {
this.listenScrollTop();
}
}
},
showBar() {
this.yBarOpa = 1;
},
hideBar() {
this.yBarOpa = 0;
},
yMouseDownHandle(e) {
const eventScreenY = e.screenY;
this.move.yMouseDown = true;
this.move.yMouseDownOffset = eventScreenY;

this.showBar();

const moveHandle = e => {
e.preventDefault();
// 计算出Y轴偏移和缩放比例 让content进行滚动
const moveScreenY = e.screenY;
let offsetY = moveScreenY - this.move.yMouseDownOffset;
this.move.yMouseDownOffset = moveScreenY;
let scale = this.info.contentHeight / this.info.yBarHeight;
let contentOffset = offsetY * scale;
this.$refs.slot.scrollTop += contentOffset;
};

document.addEventListener("mousemove", moveHandle);

document.addEventListener("mouseup", e => {
// 清除监听 重设默认值
// 自动隐藏
document.removeEventListener("mousemove", moveHandle);
this.move.yMouseDown = false;
this.move.yMouseDownOffset = 0;
this.debounceHide();
});
},
yLeaveHandle() {
if(!this.move.yMouseDown) {
this.debounceHide();
}
}
},
watch: {
forData(newVal) {
this.$nextTick(()=>{
this.refresh();
})
},
ScrollTop(newVal){
console.log(newVal)
this.$refs.slot.scrollTop=newVal;
}
}
}
</script>

<style lang="stylus">
.scroller-wrapper {
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
overflow: hidden;
.content-wrapper {
position absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin-right: -15px;
margin-bottom: -15px;
overflow: scroll;
}
.y-bar-outer {
position: absolute;
width: 6px;
transition: opacity 200ms ease 0s;
opacity: 1;
right: 0px;
bottom: 0px;
top: 0px;
border-radius: 0px;
.inner {
position: relative;
display: block;
width: 100%;
cursor: pointer;
border-radius: inherit;
background-color: red;
transition: opacity 0.4s ease,height .6s ease;
}
}
.bar {
.inner {
&:hover {
background-color: rgba(255, 0, 0, 1);
}
&.move {
background-color: rgba(255, 0, 0, 1);
}
}
}
}
</style>

App.vue

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<template>
<div id="app">
<div class="main">
<scrollbars
autoHide
:forData="forData"
:ScrollTop="ScrollTop"
:listenScrollBottom="listenScrollBottom"
:listenScrollTop="listenScrollTop"
:listenScroll="listenScroll">
<div class="scroll-con">
<ul class="inner">
<li v-for="(item,index) in num" :style="{height:`${item.height}px`,lineHeight: `${item.height}px`,background:`${LiRandomColor()}`}">{{index+1}}</li>
</ul>
</div>
</scrollbars>
</div>
<button @click="add">增加</button>
</div>
</template>

<script>
import Scrollbars from "./components/scrollbars";
import { randomColor } from "./components/util.js";
export default {
name:"app",
data(){
return {
ScrollTop:0,
num:[]
}
},
mounted() {
var a=[];
for(var i=0;i<30;i++){
a.push({
height:parseInt(Math.random()*40+30,10)+1
})
}
this.num=a;
this.$nextTick(()=>{
this.ScrollTop=document.querySelector(".scroll-con").clientHeight
})
},
computed:{
forData(){
return this.num.length
}
},
components: {
Scrollbars
},
methods: {
listenScrollBottom() {
console.log("to bottom");
},
listenScrollTop() {
console.log("to top");
},
listenScroll(valH){
//console.log(valH);
},
add(){
for(var i=0;i<10;i++){
this.num.push({
height:parseInt(Math.random()*40+30,10)+1
})
}
this.$nextTick(()=>{
this.ScrollTop=document.querySelector(".scroll-con").clientHeight
})
},
LiRandomColor(){
return randomColor();
}
}
};
</script>

<style>
* {
padding: 0px;
margin: 0px;
}

#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
color: #2c3e50;
margin-top: 60px;
}

h1,
h2 {
font-weight: normal;
}

ul {
list-style-type: none;
padding: 0;
}

li {
color: red;
display: block;
height: 30px;
line-height: 30px;
background:#aaa;

}
li:nth-child(2n){
background: #eee;
}
a {
color: #42b983;
}

.inner {
background-color: #ccc;
color: #fff;
}

.main {
height: 400px;
width: 790px;
margin: 0 auto;
border: 1px solid red;
position: relative;

}
</style>

增加list项闪烁问题


之前尝试过Vue.nextTick 解决,发现效果不太好。 解决办法就是插入数据的时候先移除一个数据,保持列表永远只有固定数目的数据!

自定义滚动条的实现思路与关键算法

前端必备自定义滚动库——iScroll