基于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 innerText和innerHtml应用
Jan 28 Javascript
js获取GridView中行数据的两种方法 分享
Jul 13 Javascript
JavaScript基于ajax编辑信息用法实例
Jul 15 Javascript
简单谈谈json跨域
Mar 13 Javascript
React.js入门实例教程之创建hello world 的5种方式
May 11 Javascript
jQuery Easyui 验证两次密码输入是否相等
May 13 Javascript
jquery的ajax提交form表单的两种方法小结(推荐)
May 25 Javascript
基于Bootstrap+jQuery.validate实现表单验证
May 30 Javascript
js表单元素checked、radio被选中的几种方法(详解)
Aug 22 Javascript
vue实现滑动到底部加载更多效果
Oct 27 Javascript
Vue实现滑动拼图验证码功能
Sep 15 Javascript
angular异步验证器防抖实例详解
Mar 31 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
php面象对象数据库操作类实例
2014/12/02 PHP
PHP中常见的缓存技术实例分析
2015/09/23 PHP
php注册登录系统简化版
2020/12/28 PHP
PHP实现求解最长公共子串问题的方法
2017/11/17 PHP
改版了网上的一个js操作userdata
2007/04/27 Javascript
css图片自适应大小
2007/11/28 Javascript
Javascript 日期对象Date扩展方法
2009/05/30 Javascript
jquery ajax 检测用户注册时用户名是否存在
2009/11/03 Javascript
JQuery中的ready函数冲突的解决方法
2010/05/17 Javascript
鼠标事件延时切换插件
2011/03/12 Javascript
在javascript中对于DOM的加强
2013/04/11 Javascript
JQuery each()嵌套使用小结
2014/04/18 Javascript
window.print打印指定div指定网页指定区域的方法
2014/08/04 Javascript
JS实用技巧小结(屏蔽错误、div滚动条设置、背景图片位置等)
2016/06/16 Javascript
jQuery实现移动端手机商城购物车功能
2016/09/24 Javascript
js返回顶部实例分享
2016/12/21 Javascript
详谈表单重复提交的三种情况及解决方法
2017/08/16 Javascript
vue better-scroll插件使用详解
2018/01/25 Javascript
让axios发送表单请求形式的键值对post数据的实例
2018/08/11 Javascript
jquery+ajax实现上传图片并显示上传进度功能【附php后台接收】
2019/06/06 jQuery
Python中的集合类型知识讲解
2015/08/19 Python
利用Python实现Windows定时关机功能
2017/03/21 Python
Python定时发送消息的脚本:每天跟你女朋友说晚安
2018/10/21 Python
Python实现高斯函数的三维显示方法
2018/12/29 Python
Pandas删除数据的几种情况(小结)
2019/06/21 Python
python把转列表为集合的方法
2019/06/28 Python
Selenium python时间控件输入问题解决方案
2020/07/22 Python
Python urllib3软件包的使用说明
2020/11/18 Python
HTML5新特性之语义化标签
2017/10/31 HTML / CSS
Champs Sports加拿大:北美最大的以商场为基础的专业运动鞋和服装零售商之一
2018/05/01 全球购物
德国内衣、泳装和睡衣网上商店:Bigsize Dessous
2018/07/09 全球购物
自学考试自我鉴定范文
2013/09/26 职场文书
2014年资料员工作总结
2014/11/18 职场文书
幼儿园园长新年寄语
2015/08/17 职场文书
SQL试题 使用窗口函数选出连续3天登录的用户
2022/04/24 Oracle
Java Redisson多策略注解限流
2022/09/23 Java/Android