博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
设计一个基于vue.js 2.x的虚拟滚动条
阅读量:6246 次
发布时间:2019-06-22

本文共 9428 字,大约阅读时间需要 31 分钟。

前言

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

设计

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

设计滚动条dom

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

其次:我们知道,我们要做到与原生滚动条一样强大!就必须设计水平滚动条和垂直滚动条,滚动条和scrollPanel属于兄弟节点之间的关系,因为滚动条的存在不能使原本的样式排版错误,并且支持topleft来控制其位置,所以滚动条的position必须是absolute,好了,我们叫水平滚动条为:hBar,垂直滚动条为:vBar

最后:我们设计了scrollPanelvBarhBar, 我们需要一个父div来把他们包装起来,然后加个样式:position: relative

实践

设计组件结构

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

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

具体实现

hBar/vBar

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

  1. 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

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

vuescroll

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

  1. 取得子组件的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;    }    // ...复制代码
  1. 显示滚动条

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

// ...        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;            }        }    // ...复制代码
  1. 获取滚动条的高度

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

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

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

  1. 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;}复制代码
  1. 监听滚轮滚动的事件。
// ...    on: {        wheel: vm.wheel    }    // ...     wheel(e) {        var vm = this;        vm.showVBar();        vm.scrollVBar(e.deltaY > 0 ? 1 : -1, 1);        e.stopPropagation();    }    // ...复制代码
  1. 监听滚动条拖拽事件
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); // 把事件放到数组里面,等销毁之前移除掉注册的时间。    }复制代码
  1. 适配移动端,监听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"        });    }复制代码
  1. 滚动内容

滚动内容的原理无非就是改变scrollPanelscrollTop/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);    },复制代码
  1. 销毁注册的事件。

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

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

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

运行截图

PC端运行截图如下图所示:

注册监听事件以后如下图所示:

在手机上运行截图:

可以看出,跟原生滚动条表现效果一致。

结语&感悟

以上就基本把我设计的滚动条设计完了,首先很感激掘金给了我这么一个分享平台,然后感谢slimScroll的作者给了我这么一个思路。做完这个插件, 我对dom元素的scrollWidth、scrollHeigh、scrollTop、scrollLeft的了解更多了,最后,附上地址,如果本文对你有所帮助,请点个star,非常感谢~

转载地址:http://hblia.baihongyu.com/

你可能感兴趣的文章
swift - idfa(唯一标示/下载量/广告追踪)
查看>>
GC垃圾回收
查看>>
HDU 4804 Campus Design
查看>>
nyist 42 一笔画 (欧拉回路 + 并查集)
查看>>
用javascript实现用户登录验证
查看>>
博客05--查找
查看>>
进程与线程
查看>>
git 创建本地分支、提交到远程分支
查看>>
什么是http?
查看>>
pthreads v3下的同步处理synchronized
查看>>
10.第一个小项目
查看>>
SDS(Simple Dynamic String)一个简易动态字符串库
查看>>
swfit-pod使用
查看>>
(九)easyUI之选项卡
查看>>
日志分析工具ELK(三)
查看>>
PAT (Advanced Level) 1049. Counting Ones (30)
查看>>
HDU 5763 Another Meaning
查看>>
session详解
查看>>
scroll滚动条
查看>>
mysql链接超时错误
查看>>