欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于vue.js 2.x的虚拟滚动条的示例代码

程序员文章站 2022-07-05 20:29:58
前言 记得以前偶然有一次浏览过一个开源的cms项目,发现这个项目的左边的菜单已经超出了windows的宽度,我就好奇为什么没出滚动条呢?然后我仔细一看,发现它左侧有一个小...

前言

记得以前偶然有一次浏览过一个开源的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 。

1、样式

var style = vm.scrollcontentstyle;
 style.overflow = 'hidden';
 // ...
 {
   style: style
 }
 // ...

2、事件

// ...
  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;
  }
  // ...

2、显示滚动条

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

// ...
    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;
      }
    }
  // ...

3、获取滚动条的高度

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

var height = math.max(
      scrollpanelheight / 
      (scrollpanelscrollheight / scrollpanelheight), 
      this.vscrollbar.minbarheight
      );

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

4、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;
}

5、监听滚轮滚动的事件。

// ...
  on: {
    wheel: vm.wheel
  }
  // ...
   wheel(e) {
    var vm = this;
    vm.showvbar();
    vm.scrollvbar(e.deltay > 0 ? 1 : -1, 1);
    e.stoppropagation();
  }
  // ...

6、监听滚动条拖拽事件

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); // 把事件放到数组里面,等销毁之前移除掉注册的时间。
  }

7、适配移动端,监听 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"
    });
  }

8、滚动内容

滚动内容的原理无非就是改变 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);
  },

9、销毁注册的事件。

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

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

运行截图

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

基于vue.js 2.x的虚拟滚动条的示例代码

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

基于vue.js 2.x的虚拟滚动条的示例代码

在手机上运行截图:

基于vue.js 2.x的虚拟滚动条的示例代码

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

结语&感悟

以上就基本把我设计的滚动条设计完了,首先很感激掘金给了我这么一个分享平台,然后感谢slimscroll的作者给了我这么一个思路。做完这个插件, 我对dom元素的scrollwidth、scrollheigh、scrolltop、scrollleft的了解更多了,最后,附上

以上部分就是这个组件的核心源码了。希望对大家的学习有所帮助,也希望大家多多支持。