设计一个基于vue.js 2.x的虚拟滚动条

2018-01-24 10:38:11来源:https://juejin.im/post/5a65d4a551882535a554574a作者:稀土掘金人点击

分享


记得以前偶然有一次浏览过一个开源的cms项目,发现这个项目的左边的菜单已经超出了windows的宽度,我就好奇为什么没出滚动条呢?然后我仔细一看,发现它左侧有一个小的div,然后我尝试着拖动它,发现竟能和原生的滚动条一样!可以通过查看它的源码,发现了这款滚动条的叫做slimScroll,然后我去它的github仓库
看了下,研究了一下源码,给我的感觉是我也能做出来一样的滚动条!通过vue实现!


设计

好, 现在开始我们的设计滚动条的步骤:


设计滚动条dom


首先要思考的是:
如果要使你需要滚动的内容滚动的话,首先一点是它的父dom必须为固定长宽,即超出部分要隐藏掉,即加了个样式:overflow: hidden
, 所以,我们给所要滚动的内容加个包装,使它的长宽和父dom相等,然后有一个样式叫:overflow: hidden
,这个包装的元素就叫scrollPanel


其次:我们知道,我们要做到与原生滚动条一样强大!就必须设计水平滚动条和垂直滚动条,滚动条和scrollPanel属于兄弟节点之间的关系,因为滚动条的存在不能使原本的样式排版错误,并且支持top、left来控制其位置,所以滚动条的position必须是absolute,好了,我们叫水平滚动条为:hBar,垂直滚动条为:vBar
最后:我们设计了scrollPanel、vBar、hBar, 我们需要一个父div来把他们包装起来,然后加个样式:position: relative
实践
设计组件结构


首先,我们的插件一共是4个组件,其中3个是子组件,1个是父组件,分别是:
vueScroll
(父组件)、scrollPanel
(包裹需要滚动内容的子组件)、vBar
(垂直滚动条)、hBar
(水平滚动条)



其次,让我们设计一下各组件所分管的功能。这里的组件分为控制层组件和展示组件(熟悉react的同学应该有所了解),展示层组件只完成展示的功能:vBar
、hBar
、scrollPanel
,控制层组件有点类似于cpu,可以控制子组件的各个状态,比如宽、高、颜色、透明度、位置等等。控制层组件就是:vueScroll


具体实现
hBar/vBar


hBar/vBar
这两个分别为水平滚动条和垂直滚动条,所实现的功能大体是一样的,所以旧放在一起说了,这里以vBar
为例。


props
接收父组件传过来的属性,具体为:
{
height: vm.state.height + 'px',//滚动条的高度
width: vm.ops.width, // 滚动条的宽度
position: 'absolute',
background: vm.ops.background, // 滚动条背景色
top: vm.state.top + 'px', // 滚动条的高度
transition: 'opacity .5s', // 消失/显示 所用的时间
cursor: 'pointer', //
opacity: vm.state.opacity, // 透明度
userSelect: 'none'
}

2 事件,主要是当鼠标移动的时候,显示滚动条。


...
render(_c){
return _c(
// ...
{
mouseenter: function(e) {
vm.$emit('showVBar'); // 触发父组件事件,显示滚动条
}
}
// ...
)
}


其中state
表示状态,是在运行时可发生改变的,而ops
则是配置参数,是用户传过来的。


scrollPanel


包裹滚动内容的组件,样式需设置为:overflow: hidden


样式
var style = vm.scrollContentStyle;
style.overflow = 'hidden';
// ...
{
style: style
}
// ...
事件
// ...
render(_c) {
// ...
on: {
mouseenter: function() {
vm.$emit('showBar');
},
mouseleave: function() {
vm.$emit('hideBar');
}
}
// ...
}
// ...
vuescroll

控制组件。控制子组件显示的状态,添加各种监听事件等。


取得子组件的dom元素,用来取得dom的实时信息。
// ...
initEl() {
this.scrollPanel.el = this.$refs['vueScrollPanel'] && this.$refs['vueScrollPanel'].$el;
this.vScrollBar.el = this.$refs['vScrollBar'] && this.$refs['vScrollBar'].$el;
this.hScrollBar.el = this.$refs['hScrollBar'] && this.$refs['hScrollBar'].$el;
}
// ...
显示滚动条

显示滚动条,包括显示水平滚动条和显示垂直滚动条,这里以显示垂直滚动条为例:


// ...
var temp;
var deltaY = {
deltaY: this.vScrollBar.ops.deltaY // 获取用户配置的deltaY
};
if(!this.isMouseLeavePanel || this.vScrollBar.ops.keepShow){
if ((this.vScrollBar.state.height = temp = this.getVBarHeight(deltaY))) { // 判断条件
// 重新设置滚动条的状态
this.vScrollBar.state.top = this.resizeVBarTop(temp);
this.vScrollBar.state.height = temp.height;
this.vScrollBar.state.opacity = this.vScrollBar.ops.opacity;
}
}
// ...
获取滚动条的高度

因为dom元素的高度不是固定的,所以你要实时地获取dom真实的高度,滚动条的高度计算公式如下:


var height = Math.max(
scrollPanelHeight /
(scrollPanelScrollHeight / scrollPanelHeight),
this.vScrollBar.minBarHeight
);


即:滚动条的高度:scrollPanel的高度 == scrollPanel的高度:dom元素高度


resizeVBarTop
,为了防止误差,并且可以求出滚动条距离父元素的高度。
resizeVBarTop({height, scrollPanelHeight, scrollPanelScrollHeight, deltaY}) {
// cacl the last height first
var lastHeight = scrollPanelScrollHeight - scrollPanelHeight - this.scrollPanel.el.scrollTop;
if(lastHeight < this.accuracy) {
lastHeight = 0;
}
var time = Math.abs(Math.ceil(lastHeight / deltaY));
var top = scrollPanelHeight - (height + (time * this.vScrollBar.innerDeltaY));
return top;
}
监听滚轮滚动的事件。
// ...
on: {
wheel: vm.wheel
}
// ...
wheel(e) {
var vm = this;
vm.showVBar();
vm.scrollVBar(e.deltaY > 0 ? 1 : -1, 1);
e.stopPropagation();
}
// ...
监听滚动条拖拽事件
listenVBarDrag: function() {
var vm = this;
var y;
var _y;
function move(e) {
_y = e.pageY;
var _delta = _y - y;
vm.scrollVBar(_delta > 0 ? 1 : -1, Math.abs(_delta / vm.vScrollBar.innerDeltaY));
y = _y;
}
function t(e) {
var deltaY = {
deltaY: vm.vScrollBar.ops.deltaY
};
if(!vm.getVBarHeight(deltaY)) {
return;
}
vm.mousedown = true;
y = e.pageY; // 记录初始的Y的位置
vm.showVBar();
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', function(e) {
vm.mousedown = false;
vm.hideVBar();
document.removeEventListener('mousemove', move);
});
}
this.listeners.push({
dom: vm.vScrollBar.el,
event: t,
type: "mousedown"
});
vm.vScrollBar.el.addEventListener('mousedown', t); // 把事件放到数组里面,等销毁之前移除掉注册的时间。
}
适配移动端,监听touch
事件。原理跟拖拽事件差不多,无非就是多了个判断,来判断当前方向是x还是y。
listenPanelTouch: function() {
var vm = this;
var pannel = this.scrollPanel.el;
var x, y;
var _x, _y;
function move(e) {
if(e.touches.length) {
var touch = e.touches[0];
_x = touch.pageX;
_y = touch.pageY;
var _delta = void 0;
var _deltaX = _x - x;
var _deltaY = _y - y;
if(Math.abs(_deltaX) > Math.abs(_deltaY)) {
_delta = _deltaX;
vm.scrollHBar(_delta > 0 ? -1 : 1, Math.abs(_delta / vm.hScrollBar.innerDeltaX));
} else if(Math.abs(_deltaX) < Math.abs(_deltaY)){
_delta = _deltaY;
vm.scrollVBar(_delta > 0 ? -1 : 1, Math.abs(_delta / vm.vScrollBar.innerDeltaY));
}
x = _x;
y = _y;
}
}
function t(e) {
var deltaY = {
deltaY: vm.vScrollBar.ops.deltaY
};
var deltaX = {
deltaX: vm.hScrollBar.ops.deltaX
};
if(!vm.getHBarWidth(deltaX) && !vm.getVBarHeight(deltaY)) {
return;
}
if(e.touches.length) {
e.stopPropagation();
var touch = e.touches[0];
vm.mousedown = true;
x = touch.pageX;
y = touch.pageY;
vm.showBar();
pannel.addEventListener('touchmove', move);
pannel.addEventListener('touchend', function(e) {
vm.mousedown = false;
vm.hideBar();
pannel.removeEventListener('touchmove', move);
});
}
}
pannel.addEventListener('touchstart', t);
this.listeners.push({
dom: pannel,
event: t,
type: "touchstart"
});
}
滚动内容


滚动内容的原理无非就是改变scrollPanel
的scrollTop/scrollLeft
来达到控制内容上下左右移动的目的。


scrollVBar: function(pos, time) {
// >0 scroll to down<0 scroll to up

var top = this.vScrollBar.state.top;
var scrollPanelHeight = getComputed(this.scrollPanel.el, 'height').replace('px', "");
var scrollPanelScrollHeight = this.scrollPanel.el.scrollHeight;
var scrollPanelScrollTop = this.scrollPanel.el.scrollTop;
var height = this.vScrollBar.state.height;
var innerdeltaY = this.vScrollBar.innerDeltaY;
var deltaY = this.vScrollBar.ops.deltaY;
if (!((pos < 0 && top <= 0) || (scrollPanelHeight <= top + height && pos > 0) || (Math.abs(scrollPanelScrollHeight - scrollPanelHeight) < this.accuracy))) {
var Top = top + pos * innerdeltaY * time;
var ScrollTop = scrollPanelScrollTop + pos * deltaY * time;
if (pos < 0) {
// scroll ip
this.vScrollBar.state.top = Math.max(0, Top);
this.scrollPanel.el.scrollTop = Math.max(0, ScrollTop);
} else if (pos > 0) {
// scroll down
this.vScrollBar.state.top = Math.min(scrollPanelHeight - height, Top);
this.scrollPanel.el.scrollTop = Math.min(scrollPanelScrollHeight - scrollPanelHeight, ScrollTop);
}
}
// 这些是传递给父组件的监听滚动的函数的。
var content = {};
var bar = {};
var process = "";
content.residual = (scrollPanelScrollHeight - scrollPanelScrollTop - scrollPanelHeight);
content.scrolled = scrollPanelScrollTop;
bar.scrolled = this.vScrollBar.state.top;
bar.residual = (scrollPanelHeight - this.vScrollBar.state.top - this.vScrollBar.state.height);
bar.height = this.vScrollBar.state.height;
process = bar.scrolled/(scrollPanelHeight - bar.height);
bar.name = "vBar";
content.name = "content";
this.$emit('vscroll', bar, content, process);
},
销毁注册的事件。

刚才我们已经把注册事件放到listeners数组里面了,我们可以在beforedestroy钩子里将他们进行销毁。


// remove the registryed event.
this.listeners.forEach(function(item) {
item.dom.removeEventListener(item.event, item.type);
});

以上部分就是这个组件的核心源码了。


相关文章

    无相关信息

微信扫一扫

第七城市微信公众平台