基于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 相关文章推荐
Javascript常用运算符(Operators)-javascript基础教程
Dec 14 Javascript
jquery HotKeys轻松搞定键盘事件代码
Aug 30 Javascript
js对数字的格式化使用说明
Jan 12 Javascript
关于setInterval、setTimeout在jQuery中的使用注意事项
Sep 28 Javascript
ajax中get和post的说明及使用与区别
Dec 23 Javascript
jquery中对于批量deferred的处理方法
Jan 22 Javascript
javascript使用正则控制input输入框允许输入的值方法大全
Jun 19 Javascript
jQuery获取访问者IP地址的方法(基于新浪API与QQ查询接口)
May 25 Javascript
jQuery实现的淡入淡出图片轮播效果示例
Aug 29 jQuery
详解在HTTPS 项目中使用百度地图 API
Apr 26 Javascript
react 生命周期实例分析
May 18 Javascript
基于JS实现计算24点算法代码实例解析
Jul 23 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
Email+URL的判断和自动转换函数
2006/10/09 PHP
php中的一个中文字符串截取函数
2007/02/14 PHP
php REMOTE_ADDR之获取访客IP的代码
2008/04/22 PHP
PHP中SimpleXML函数用法分析
2014/11/26 PHP
PHP开启opcache提升代码性能
2015/04/26 PHP
php判断当前操作系统类型
2015/10/28 PHP
php简单获取复选框值的方法
2016/05/11 PHP
PHP之图片上传类实例代码(加了缩略图)
2016/06/30 PHP
PHP使用递归算法无限遍历数组示例
2017/01/13 PHP
php实现QQ小程序发送模板消息功能
2019/09/18 PHP
javascript 密码强弱度检测万能插件
2009/02/25 Javascript
javascript 触发HTML元素绑定的函数
2010/09/11 Javascript
判断多个input type=file是否有已经选择好文件的代码
2012/05/23 Javascript
jQuery 滑动方法slideDown向下滑动元素
2014/01/16 Javascript
JavaScript跨浏览器获取页面中相同class节点的方法
2015/03/03 Javascript
jquery zTree异步加载简单实例讲解
2016/02/25 Javascript
js 定位到某个锚点的方法
2016/11/19 Javascript
详解angularjs popup-table 弹出框表格指令
2017/09/20 Javascript
vue实现动态显示与隐藏底部导航的方法分析
2019/02/11 Javascript
js module大战
2019/04/19 Javascript
vue+element实现表格新增、编辑、删除功能
2019/05/28 Javascript
利用Python中的输入和输出功能进行读取和写入的教程
2015/04/14 Python
Python中绑定与未绑定的类方法用法分析
2016/04/29 Python
深入理解Python中的内置常量
2017/05/20 Python
Python实现将通信达.day文件读取为DataFrame
2018/12/22 Python
Django xadmin开启搜索功能的实现
2019/11/15 Python
python使用梯度下降和牛顿法寻找Rosenbrock函数最小值实例
2020/04/02 Python
css 省略号 css3让多余的字符串消失并附加省略号的实现代码
2013/02/07 HTML / CSS
CSS3毛玻璃效果(blur)有白边问题的解决方法
2016/11/15 HTML / CSS
HTML5中在title标题标签里设置小图标的方法
2020/06/23 HTML / CSS
培训主管岗位职责
2014/02/01 职场文书
运动会通讯稿150字
2014/02/15 职场文书
精神文明单位申报材料
2014/05/02 职场文书
初中生毕业评语
2014/12/29 职场文书
如何使用Python对NetCDF数据做空间相关分析
2021/04/21 Python
Python实现位图分割的效果
2021/11/20 Python