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


Posted in Javascript onJanuary 23, 2018

前言

记得以前偶然有一次浏览过一个开源的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的了解更多了,最后,附上github项目地址

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

Javascript 相关文章推荐
js继承的实现代码
Aug 05 Javascript
关于全局变量和局部变量的那些事
Jan 11 Javascript
jquery选择器-根据多个属性选择示例代码
Oct 21 Javascript
jquery easyui 对于开始时间小于结束时间的判断示例
Mar 22 Javascript
Javascript在IE和Firefox浏览器常见兼容性问题总结
Aug 03 Javascript
AngularJS实现表单验证功能
Jan 09 Javascript
js实现数字递增特效【仿支付宝我的财富】
May 05 Javascript
关于jquery中attr()和prop()方法的区别
May 28 jQuery
JS基于对象的链表实现与使用方法示例
Jan 31 Javascript
javascript 构建模块化开发过程解析
Sep 11 Javascript
vue-cli3项目升级到vue-cli4 的方法总结
Mar 19 Javascript
JavaScript中的this妙用实例分析
May 09 Javascript
AngularJS基于http请求实现下载php生成的excel文件功能示例
Jan 23 #Javascript
简述vue中的config配置
Jan 23 #Javascript
JS实现多物体运动的方法详解
Jan 23 #Javascript
JS运动改变单物体透明度的方法分析
Jan 23 #Javascript
JS实现基于拖拽改变物体大小的方法
Jan 23 #Javascript
基于vue cli重构多页面脚手架过程详解
Jan 23 #Javascript
JS实现点击下拉菜单把选择的内容同步到input输入框内的实例
Jan 23 #Javascript
You might like
LotusPhp笔记之:Logger组件的使用方法
2013/05/06 PHP
WebQQ最新登陆协议的用法
2014/12/22 PHP
PHP实现的曲线统计图表示例
2016/11/10 PHP
在PHP 7下安装Swoole与Yar,Yaf的方法教程
2017/06/02 PHP
PHP中phar包的使用教程
2017/06/14 PHP
php实现微信模板消息推送
2018/03/30 PHP
详解php curl带有csrf-token验证模拟提交方法
2018/04/18 PHP
解析js中获得父窗口链接getParent方法以及各种打开窗口的方法
2013/06/19 Javascript
jQuery实用技巧必备(中)
2015/11/03 Javascript
javascript图片延迟加载实现方法及思路
2015/12/31 Javascript
jQuery中判断对象是否存在的方法汇总
2016/02/24 Javascript
Node.js配合node-http-proxy解决本地开发ajax跨域问题
2016/08/31 Javascript
详解本地Node.js服务器作为api服务器的解决办法
2017/02/28 Javascript
JavaScript 事件流、事件处理程序及事件对象总结
2017/04/01 Javascript
微信小程序中使用javascript 回调函数
2017/05/11 Javascript
angularjs http与后台交互的实现示例
2018/12/21 Javascript
vue实现购物车的监听
2020/04/20 Javascript
Python迭代器与生成器基本用法分析
2018/07/26 Python
python+splinter实现12306网站刷票并自动购票流程
2018/09/25 Python
Python 输入一个数字判断成绩分数等级的方法
2018/11/15 Python
对python遍历文件夹中的所有jpg文件的实例详解
2018/12/08 Python
python版本五子棋的实现代码
2018/12/11 Python
Python实现TCP通信的示例代码
2019/09/09 Python
Pyqt5 关于流式布局和滚动条的综合使用示例代码
2020/03/24 Python
怎么快速自学python
2020/06/22 Python
python获取linux系统信息的三种方法
2020/10/14 Python
Python类的继承super相关原理解析
2020/10/22 Python
详解CSS3中使用gradient实现渐变效果的方法
2015/08/18 HTML / CSS
质检员的岗位职责
2013/11/15 职场文书
应聘会计求职信
2014/06/11 职场文书
普通党员自我剖析材料
2014/10/07 职场文书
2014最新股权信托合同协议书
2014/11/18 职场文书
街道办残联2016年助残日活动总结
2016/04/01 职场文书
Java中使用Filter过滤器的方法
2021/06/28 Java/Android
一些让Python代码简洁的实用技巧总结
2021/08/23 Python
mysql 获取相邻数据项
2022/05/11 MySQL