来吧继续阅读组件源码,Scrollbar 滚动条组件安排上
不知道elementUI官方文档上为何没有这个组件,一起来看看这个被雪藏的组件吧
先说感受:对不起,是我肤浅了,滚动条组件又秀到我了!
前期说明
1、该组件不是简单的修改原生滚动条样式,而是自己实现了一个虚拟滚动条
2、既然是虚拟的,原生的滚动条该如何处理,如何隐藏?
3、既然是虚拟的,滑动滚动条时对应的视图该如何变化,两者的对照关系是怎么样的?
4、既然是虚拟的,当页面尺寸发生变化或元素尺寸发生变化时又该如何处理呢?
带着问题出发
一、为什么要实现虚拟滚动条来替换原生的?
1、先说明下,chrome和safari两个浏览器原生的滚动条样式相差非常大,我是搞了三四年前端开发才知道的(请原谅我,我是个穷逼,一直没用过mac)。
下图是两者的对比,并且safari浏览器鼠标放到滑块上后,滑块的宽度还会变大,有一个明显的交互效果变化

2、因为不同浏览器的滚动条外观是不一样的。虽然也可以直接修改CSS3中的
::-webkit-scrollbar
相关属性来达到修改原生滚动条样式,但这个属性部分浏览器上没有能够完美兼容,而且也难做到动画等交互效果的统一。需要做风格统一时,所以elementUI就自己实现了虚拟滚动条补充说明下:如何修改原生滚动条样式
/** * 滚动条整体部分 * width 表示垂直方向滑轨的宽度 * height表示水平方向滑轨的高度 */ ::-webkit-scrollbar { width: 6px; height: 6px; transition: .3s all; } /*滚动条的滑轨*/ ::-webkit-scrollbar-track { background-color: transparent; /*滑轨hover效果*/ &:hover { background-color: rgba(20, 20, 20, 0.04); } } /*滚动条里面的滑块,能向上向下移动*/ ::-webkit-scrollbar-thumb { background-color: rgba(20, 20, 20, .5); border-radius: 4px; transition: .3s all; /*滑块hover效果*/ &:hover { background-color: rgba(59, 59, 59, .5); } /*滑块按下效果*/ &:active { background-color: rgba(23, 23, 28, .5); } }
二、原生的滚动条该如何处理,如何隐藏?
1、选中滚动条的元素(如下图)一直很奇怪,这里的margin-bottom:-17px;margin-right:-17px;
是什么鬼?怎么会出现负的外边距

2、当手动把maigin-right改为0后,margin-right:0;
,发现原生的滚动条又出现了,原来是通过负边距来隐藏原生滚动条的
下图说明:图中灰色的为原生滚动条,蓝色的为虚拟滚动条(为了对比明显,把虚拟滚动条的背景改成了蓝色)

三、滑动滚动条时对应的视图该如何变化,两者的对照关系是怎么样的?
敲重点:这块的逻辑是最核心也是最复杂的,先说结论,等看过源码后就彻底理解了。
1、看图说话
滚动视图clientHeight与scrollHeight的比例 = 虚拟滚动条thumb与滑轨bar的比例


基本用法
el-scrollbar 滚动条组件用于优化页内滚动条的UI效果,使用时必须指定高度!
<el-scrollbar style="height: 100px;">
<p v-for="item in 10">{{item}}</p>
</el-scrollbar>
隐藏原生横向滚动条
/deep/ .el-scrollbar__wrap { overflow-x: hidden; }
el-scrollbar 容器结构
scrollbar
组件中嵌套wrap
和view
两层元素。wrap
为滚动层,view
为视图容器层。同时生成两种虚拟滚动条horizontal
和vertical

生成el-scrollbar html
1、打开项目的入口文件packages/scrollbar/src/main.js
export default {
name: 'ElScrollbar',
components: { Bar },
props: {
native: Boolean, // 是否使用原生滚动条,即不生成自定义虚拟滚动条
wrapStyle: {}, // wrap的内联样式,支持数组和字符串两种格式, 如[{"background": "red"}, {"color": "red"}] 转化为 {background: "red", color: "red"}
wrapClass: {}, // 自定义wrap的类名
viewClass: {}, // 自定义view的类名
viewStyle: {}, // 自定义view的行内样式
noresize: Boolean, // 如果container容器尺寸不会发生变化,最好设置它可以优化性能
tag: { // 组件最外层的包裹标签,默认为 div
type: String,
default: 'div'
}
},
data() {
return {
sizeWidth: '0', // 水平滚动条的宽度
sizeHeight: '0', // 垂直滚动条的高度
moveX: 0, // 水平滚动条的移动比例
moveY: 0 // 垂直滚动条的移动比例
};
},
computed: {
wrap() {
return this.$refs.wrap;
}
},
render(h) {
// 获取原生滚动条的宽度
let gutter = scrollbarWidth();
// 获取wrap的内联样式
let style = this.wrapStyle;
// 如果滚动条的宽度存在,设置偏移量,用来隐藏原生的滚动条
if (gutter) {
const gutterWith = `-${gutter}px`;
const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`;
/**
* 如是wrapStyle为数组 Array<Object> [{"background": "red"}, {"color": "red"}]
* 则会被转为对象 {background: "red", color: "red"}
*/
if (Array.isArray(this.wrapStyle)) {
style = toObject(this.wrapStyle);
style.marginRight = style.marginBottom = gutterWith;
} else if (typeof this.wrapStyle === 'string') {
style += gutterStyle;
} else {
style = gutterStyle;
}
}
// 生成el-scrollbar__view容器,通过this.$slots.default将el-scrollbar组件中包裹的内容插入其中
const view = h(this.tag, {
class: ['el-scrollbar__view', this.viewClass],
style: this.viewStyle,
ref: 'resize'
}, this.$slots.default);
// 生成el-scrollbar__wrap结构,并绑定滚动事件,并设置overflow:scroll样式,是产生滚动的容器
const wrap = (
<div
ref="wrap"
style={ style }
onScroll={ this.handleScroll }
class={ [this.wrapClass, 'el-scrollbar__wrap', gutter ? '' : 'el-scrollbar__wrap--hidden-default'] }>
// 将view结构嵌套其中
{ [view] }
</div>
);
let nodes;
// 如果不使用原生滚动条,则添加虚拟滚动条
if (!this.native) {
nodes = ([
wrap,
// 添加水平的虚拟滚动条
<Bar
move={ this.moveX }
size={ this.sizeWidth }></Bar>,
// 添加垂直的虚拟滚动条
<Bar
vertical
move={ this.moveY }
size={ this.sizeHeight }></Bar>
]);
} else {
// 否则使用原生的滚动条,并且没有绑定滚动事件
nodes = ([
<div
ref="wrap"
class={ [this.wrapClass, 'el-scrollbar__wrap'] }
style={ style }>
{ [view] }
</div>
]);
}
// 生成最终的组件
return h('div', { class: 'el-scrollbar' }, nodes);
},
2、打开项目的入口文件src/utils/scrollbar-width.js
了解scrollbarWidth函数如何获取原生滚动条的宽度
// 利用闭包来存储原生滚动条的宽度 let scrollBarWidth; export default function() { // 如果是服务端直接返回0 if (Vue.prototype.$isServer) return 0; // 如果scrollBarWidth值存在,返回已存储的值 if (scrollBarWidth !== undefined) return scrollBarWidth; /** * 1、生成一个div为outer,将该元素插入到body中 * 2、生成一个div为inner(宽度为100%),将该元素插入outer中 * 3、原生滚动条宽度:outer的offsetWidth - inner的offsetWidth */ const outer = document.createElement('div'); outer.className = 'el-scrollbar__wrap'; outer.style.visibility = 'hidden'; outer.style.width = '100px'; outer.style.position = 'absolute'; outer.style.top = '-9999px'; document.body.appendChild(outer); const widthNoScroll = outer.offsetWidth; outer.style.overflow = 'scroll'; const inner = document.createElement('div'); inner.style.width = '100%'; outer.appendChild(inner); const widthWithScroll = inner.offsetWidth; outer.parentNode.removeChild(outer); scrollBarWidth = widthNoScroll - widthWithScroll; return scrollBarWidth; };
小结:
1)通过render函数,根据配置项生成滚动条的html结构
2)通过scrollbarWidth函数来计算原生滚动条的宽度,并给el-scrollbar__wrap元素设置对应的偏移量,从而实现隐藏原生的滚动条
计算滑块的高度与位移
还是packages/scrollbar/src/main.js
scrollTop与clientHeight的比例 = moveY与虚拟滚动条thumb的比例 = 滚动条thumb的translateY
mounted() { if (this.native) return; // 初始化时计算一次滑块的高度 this.$nextTick(this.update); // 当容器的尺寸发生变化时,重新计算滑块的高度 !this.noresize && addResizeListener(this.$refs.resize, this.update); }, methods: { /** * 当元素滚动时,计算出水平和垂直方向滚动条的位移translateX和translateY * 1、视图clientHeight与scrollHeight的比例 = 虚拟滚动条thumb与滑轨bar的比例 * 2、所以当视图滚动时,scrollTop与clientHeight的比例 = moveY与虚拟滚动条thumb的比例 = 滚动条thumb的translateY * 3、假如scrollTop和clientHeight都为100px,此时滚动条thumb的translateY = 100% * 注意:translateY和translateX距离都是基于自身宽高设置的 * */ handleScroll() { const wrap = this.wrap; this.moveY = ((wrap.scrollTop * 100) / wrap.clientHeight); this.moveX = ((wrap.scrollLeft * 100) / wrap.clientWidth); }, /** * update方法用来计算滑块el-scrollbar__thumb的高度 * 1、得到el-scrollbar__wrap容器的clientHeight/scrollHeight的比例 * 2、根据上文的思路:视图clientHeight与scrollHeight的比例 = 虚拟滚动条thumb与滑轨bar的比例 * 3、利用css百分比设置样式,当bar为父元素时,滑块thumb的高度为: heightPercentage + '%' * */ update() { let heightPercentage, widthPercentage; const wrap = this.wrap; if (!wrap) return; // 这里乘以100,方便利用百分比设置滑块高度,水平和垂直方向计算方式一致 heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight); widthPercentage = (wrap.clientWidth * 100 / wrap.scrollWidth); this.sizeHeight = (heightPercentage < 100) ? (heightPercentage + '%') : ''; this.sizeWidth = (widthPercentage < 100) ? (widthPercentage + '%') : ''; } }, beforeDestroy() { if (this.native) return; // 销毁组件时,移除监听事件,有始有终 !this.noresize && removeResizeListener(this.$refs.resize, this.update); }
小结:
1、初始化时通过update方法,根据视图clientHeight与scrollHeight的比例 = 虚拟滚动条thumb与滑轨bar的比例,计算出垂直滚动条的高度或水平滚动条的宽度
2、给el-scrollbar__wrap元素绑定滚动事件,来计算虚拟滚动条thumb的位移,滚动条thumb的translateY = scrollTop与clientHeight的比例 = moveY与虚拟滚动条thumb的比例
3、当容器的尺寸发生变化时,重新计算滑块的高度
el-scrollbar__bar 虚拟滚动条
生成 hmtl
打开packages/scrollbar/src/bar.js
1、通过render函数,生成el-scrollbar__bar
滑轨和el-scrollbar__thumb
滑块
2、给滑轨和滑块分别绑定mousedown
事件,监听鼠标左键按下事件。
这里分两种情况,一种鼠标点击滑轨,另一种是鼠标拖动滑块
export default {
name: 'Bar',
props: {
vertical: Boolean, // 是否垂直滚动条
size: String, // size 对应的是水平滚动条的width或垂直滚动条的height
move: Number // move 用于设置 translateX 或 translateY 属性
},
computed: {
// 从BAR_MAP中返回一个的新对象,垂直滚动条属性集合 或 水平滚动条属性集合
bar() {
return BAR_MAP[this.vertical ? 'vertical' : 'horizontal'];
},
// 获取父元素的el-scrollbar__wrap,用于获取对应的scrollTop 值
wrap() {
return this.$parent.wrap;
}
},
render(h) {
/**
* 以垂直滚动条为例,vertical为true
* 1、bar会返回当前滚动条类型的滚动条属性集合
* 2、滑轨的类名为:el-scrollbar__bar is-vertical,通过绝对定位,滑轨的高度 = { top: 2px; bottom: 2px; right: 2px;}, 靠右撑满了el-scrollbar__wrap
* 3、通过renderThumbStyle方法,来设计滑块的高度与translateY
* 4、给滑轨和滑块分别绑定mousedown事件,监听鼠标左键按下事件,这里分两种情况,一种鼠标点击滑轨,另一种是鼠标拖动滑块
* */
const { size, move, bar } = this;
return (
<div
class={ ['el-scrollbar__bar', 'is-' + bar.key] }
onMousedown={ this.clickTrackHandler } >
<div
ref="thumb"
class="el-scrollbar__thumb"
onMousedown={ this.clickThumbHandler }
style={ renderThumbStyle({ size, move, bar }) }>
</div>
</div>
);
}
renderThumbStyle方法用来设置水平和垂直方向滚动条的样式
/** * 以垂直滚动条为例 * renderThumbStyle({ 40%, 50%, { * offset: 'offsetHeight', * scroll: 'scrollTop', * scrollSize: 'scrollHeight', * size: 'height', * key: 'vertical', * axis: 'Y', * client: 'clientY', * direction: 'top' * }} 转化为 {height: 50%; transform: translateY(40%);} * */ export function renderThumbStyle({ move, size, bar }) { const style = {}; const translate = `translate${bar.axis}(${ move }%)`; style[bar.size] = size; style.transform = translate; style.msTransform = translate; style.webkitTransform = translate; return style; };
情况1:鼠标点击滑轨
如下图所示:
点击轨道区域时,滑块中心会快速移动到该位置,并且更新视图的scrollTop
流程小结:
1、计算出鼠标点击的位置距离滑轨顶部的距离offset
2、让滑块的中心滑动到鼠标点击的位置offset - thumb.offsetHeight
的一半
3、计算出滑块滑动距离与滑轨的占比thumbPositionPercentage
4、根据:视图clientHeight与scrollHeight的比例 = 虚拟滚动条thumb与滑轨bar的比例,反向得到,滑块滑块的距离的占比 = scrollTop与scrollHeight的比例,反向计算出视图的scrollTop

clickTrackHandler
方法
// 点击滑轨时的处理逻辑 clickTrackHandler(e) { /** * 主要流程: * 1、e.target.getBoundingClientRect()[this.bar.direction] → el-scrollbar__bar.top来获取滑轨距页面顶部的距离 * 2、e[this.bar.client]) → e.clientY来获取鼠标距页面顶部的距离 * 3、offset为鼠标距el-scrollbar__bar容器顶部的距离 * 4、thumbHalf为滑块offsetHeight的一半,获取一半高度的原因,是鼠标点击后,期望滑块的中心移动到此 * 5、thumbPositionPercentage为计算后得到的滑块偏移位置,根据偏移量与滑轨的占比,也就是滚动块所处的位置 * 6、根据上文中提到的:视图clientHeight与scrollHeight的比例 = 虚拟滚动条thumb与滑轨bar的比例,反向得到,滑块滑块的距离的占比 = scrollTop与scrollHeight的比例 * 7、反向计算出scrollTop的值,然后修改this.wrap.scrollTop,使el-scrollbar__wrap触发滚动 * * */ const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]); const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2); const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]); this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100); },
情况2:鼠标拖动滑块
流程小结:
1、滑块拖动前,先记录鼠标距滑块底部的距离(this.Y)
2、滑块拖动时,计算鼠标距滑轨顶部的距离
3、offset - thumbClickPosition
计算出滑动距离占滑轨的比例(计算方式和情况1一样)
4、根据:视图clientHeight与scrollHeight的比例 = 虚拟滚动条thumb与滑轨bar的比例,反向得到,滑块滑块的距离的占比 = scrollTop与scrollHeight的比例,反向计算出视图的scrollTop
clickThumbHandler(e) { /** * 防止右键单击滑动块 * e.ctrlKey: 检测事件发生时Ctrl键是否被按住了 * e.button: 指示当事件被触发时哪个鼠标按键被点击 0,鼠标左键;1,鼠标中键;2,鼠标右键 */ if (e.ctrlKey || e.button === 2) { return; } // 开始拖拽 this.startDrag(e); /** * 计算点击滑块时鼠标距滑块底部的距离 * 1、e.currentTarget[this.bar.offset] ⇒ el-scrollbar__thumb.offsetHeight(滑块的高度) * 2、e[this.bar.client] ⇒ e.clientY (鼠标距顶部的距离) * 3、e.currentTarget.getBoundingClientRect()[this.bar.direction]) ⇒ el-scrollbar__thumb.getBoundingClientRect().top (滑块距页面顶部的距离) * 4、this[this.bar.axis] ⇒ this.Y = 鼠标距滑块底部的距离 * */ this[this.bar.axis] = (e.currentTarget[this.bar.offset] - (e[this.bar.client] - e.currentTarget.getBoundingClientRect()[this.bar.direction])); }, startDrag(e) { // 停止后续的相同事件函数执行 e.stopImmediatePropagation(); // 按下状态设为true this.cursorDown = true; // 监听鼠标移动事件 on(document, 'mousemove', this.mouseMoveDocumentHandler); // 监听鼠标按键松开事件 on(document, 'mouseup', this.mouseUpDocumentHandler); // 拖拽滚动块时,此时禁止鼠标长按划过文本选中。 document.onselectstart = () => false; }, // 按下滚动条,并且鼠标移动时 mouseMoveDocumentHandler(e) { // 如果按下状态为false,返回 if (this.cursorDown === false) return; // 获取this.Y 滑动时鼠标距滑块底部的距离 const prevPage = this[this.bar.axis]; if (!prevPage) return; /** * 计算按下滑块滑动时,鼠标距滑轨顶部的距离 * 1、this.$el.getBoundingClientRect()[this.bar.direction] ⇒ el-scrollbar__bar.getBoundingClientRect().top来获取滑轨距页面顶部的距离 (滑块距顶部的距离) * 2、e[this.bar.client] ⇒ e.clientY (鼠标距顶部的距离) * 3、offset = 滑块滑动时鼠标距滑轨顶部的距离 * */ const offset = ((this.$el.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]) * -1); // thumbClickPosition为鼠标距滑块顶部的距离 const thumbClickPosition = (this.$refs.thumb[this.bar.offset] - prevPage); /** * 计算出滚动距离占滑轨的比例 * 1、offset - thumbClickPosition 为滑块滚动的距离 * 2、滚动距离占滑轨的比例 = (offset - thumbClickPosition) * 100 / el-scrollbar__bar.offsetHeight * */ const thumbPositionPercentage = ((offset - thumbClickPosition) * 100 / this.$el[this.bar.offset]); /** * 计算出视图需要滚动的距离 * 1、根据上文的结论,反向得到,滑块滑块的距离的占比 = scrollTop与scrollHeight的比例 * 2、最终计算出 this.wrap.scrollTop的距离 * */ this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100); },
总结
1、巧妙的设计思路
1)虚拟滚动条的设计思想:视图clientHeight与scrollHeight的比例 = 虚拟滚动条thumb与滑轨bar的比例
2)建立视图与虚拟滚动条的联动关系;联动关系大致分为两种:
①视图自身的滚动 ②滑块位置的移动;两者任何一个的变化要同步修改另一个的状态
2、单个知识点总结:
1)计算原生滚动条的宽度,如何隐藏原生滚动条
2)new ResizeObserver
监听元素尺寸的变化
3)getBoundingClientRect
获取元素的大小及其相对于视口的位置
4)document.onselectstart = () => false;
禁止鼠标选中文本
5)translateX、translateY
是相对于自身尺寸进行设置的
6)e.stopImmediatePropagation()
停止后续的相同事件函数执行
7)再次复习了offsetHeight、scrollTop、scrollHeight
这些属性
参考链接
# Element-ui el-scrollbar 源码解析
评论(0)