美文网首页
better-scroll用法及源码学习(一)

better-scroll用法及源码学习(一)

作者: nucky_lee | 来源:发表于2019-09-27 17:47 被阅读0次

    官方文档地址:

    http://ustbhuangyi.github.io/better-scroll/doc/zh-hans/

    应用场景:列表滚动

    版本:1.15.2
    用法:

    我们先来看一下它的 html 结构:

    <section class="menu_container">
        /* wrapper层级 */
        <div class="wrapper" ref="menuList">  
            /* scroller层级.此层级内容的高度必须大于wrapper,才能滚动 */
                <ul class="content">
                    <li>...</li>
                    <li>...</li>
                    ...
                </ul>
        </div>
     </section>
    
    less代码如下:
    .menu_container {
        position: absolute;
        left: 0;
        top: 0;
        bottom: 0;
        overflow: hidden;
        display: flex;
          
        .wrapper {
            display: flex;
            flex-direction: column;
            flex: 1;
            overflow: hidden;
        }
    }
    

    better-scroll 是作用在外层 wrapper 容器上的,滚动的部分是 content 元素。这里要注意的是,better-scroll 只处理容器(wrapper)的第一个子元素(content)的滚动,其它的元素都会被忽略。

    script代码如下:
    <script>
    import BScroll from "better-scroll";
    export default {
      data() {
        return {
          showLoading: true, //加载动画
          bScroll: null,
        };
      },
    
      mounted() {
        this.initData();
      },
    
      methods: {
        async initData() {
          ...
          this.hideLoading();
    
          this.$nextTick(() => {
            // DOM 现在更新了
            this.initBScroll();
          });
        },
    
        hideLoading() {
          this.showLoading = false;
        },
     
        initBScroll() {
          if (!this.bScroll) {
            this.bScroll = new BScroll(this.$refs.menuList, {
              mouseWheel: true,
              probeType: 3, //有时候我们需要知道滚动的位置。当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件;当 probeType 为 2 的时候,会在屏幕滑动的过程中实时的派发 scroll 事件;当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。如果没有设置该值,其默认值为 0,即不派发 scroll 事件。
              click: true //better-scroll 默认会阻止浏览器的原生 click 事件。当设置为 true,better-scroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 _constructed,值为 true。
            });
          }
        }
      },
      watch: {
        showLoading: function(value) {
          if (!value) {
            this.$nextTick(() => {
              if (this.bScroll) {
                this.bScroll.refresh();
              }
            });
          }
        }
      }
    };
    </script>
    
    better-scroll滚动无效的原因

    https://blog.csdn.net/qiqi_77_/article/details/79361413
    可在这里一一排除这几个情况。

    下面开始分析大神的源码~

    源码版本:0.1.15;此版本相对比较容易理解,后面会逐渐阅读最新源码。

    构造函数

    export class BScroll extends EventEmitter {
        constructor(el, options) {
            super();
            this.wrapper = typeof el === 'string' ? document.querySelector(el) : el;
            this.scroller = this.wrapper.children[0];
            this.scrollerStyle = this.scroller.style;
    
            this.options = {
                startX: 0,
                startY: 0,
                scrollY: true,/* 默认开启纵向滚动 */
                bounce: true,/* 当滚动超过边缘的时候会有一小段回弹动画。设置为 true 则开启动画 */
                bounceTime: 800,/* 设置回弹动画的动画时长 */
                resizePolling: 60,/* 当窗口的尺寸改变的时候,需要对 better-scroll 做重新计算,为了优化性能,我们对重新计算做了延时。60ms 是一个比较合理的值 */
                preventDefault: true,/* 当事件派发后是否阻止浏览器默认行为 */
                preventDefaultException: {
                    tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/
                },/* better-scroll 的实现会阻止原生的滚动,这样也同时阻止了一些原生组件的默认行为。这个时候我们不能对这些元素做 preventDefault,所以我们可以配置 preventDefaultException。默认值 {tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/}表示标签名为 input、textarea、button、select 这些元素的默认行为都不会被阻止。 */
                useTransition: true,/* 是否使用 CSS3 transition 动画。如果设置为 false,则使用 requestAnimationFrame 做动画 */
                useTransform: true,/* 是否使用 CSS3 transform 做位移。如果设置为 false, 则设置元素的 top/left (这种情况需要 scroller 是绝对定位的) */
                wheel: false,/* 这个配置是为了做 Picker 组件用的,默认为 false,如果开启则需要配置一个 Object。 */
                momentum: true,/* 当快速在屏幕上滑动一段距离的时候,会根据滑动的距离和时间计算出动量,并生成滚动动画。设置为 true 则开启动画 */
                momentumLimitTime: 300,/* 只有在屏幕上快速滑动的时间小于 momentumLimitTime,才能开启 momentum 动画 */
                momentumLimitDistance: 15,/* 只有在屏幕上快速滑动的距离大于 momentumLimitDistance,才能开启 momentum 动画 */
                // let {deceleration, swipeBounceTime, bounceTime} = options;
                swipeTime: 2500,/* 设置 momentum 动画的动画时长 */
                // deceleration: 0.001,/* 表示 momentum 动画的减速度 */
                deceleration: 0.0015,
                swipeBounceTime: 500,/* 设置当运行 momentum 动画时,超过边缘后的回弹整个动画时间 */
            }
    
            /* 初始化并addEventListener */
            this._init();
    
        /* 判断页面是否可以滚动及初始化页面位置 */
            this.refresh();
        }
        ...
    }
    

    可以看出BScroll继承自EventEmitter,即一个发布订阅模式的类。作者借此类实现滚动状态的监听。关于EventEmitter 可以查看https://www.jianshu.com/p/2ed4684cca77

    /* DOM 事件触发 */
    handleEvent(e) {
          switch (e.type) {
              case 'touchstart':
              case 'mousedown':
                  this._start(e);
                  break;
              case 'touchmove':
              case 'mousemove':
                  this._move(e);
                  break;
              case 'touchend':
              case 'mouseup':
              case 'touchcancle':
              case 'mousecancle':
                  this._end(e);
                  break;
              case 'transitionend':
              case 'webkitTransitionEnd':
              case 'oTransitionEnd':
              case 'MSTransitionEnd':
                  this._transitionEnd(e);
                  break;
          }
      }
    
    用户开始触摸时触发
    _start(e) {
            ...
            if (this.options.preventDefault && !isBadAndroid && !preventDefaultException(e.target, this.options.preventDefaultException)) {
                e.preventDefault();//阻止页面滚动
            }
            /* 此时move为false */
            this.moved = false;
            /* 滚动总距离 */
            this.distX = 0;
            this.distY = 0;
            
            this._transitionTime();
            /* 开始触摸时间 */
            this.startTime = +new Date();
    
            /* 如果页面滚动过程中又有新的触屏或者滚动操作 */
            if (this.options.useTransition && this.isInTransition) {
                /* 停止旧的滚动操作 */
                this.isInTransition = false;
                /* 获取滚动的位置坐标 */
                let pos = this.getComputedPosition();
                /* 页面位置坐标置为pos */
                this._translate(pos.x, pos.y);
    
                this.trigger('scrollEnd');
            }
            /* 初始化位置信息 */
            let point = e.touches ? e.touches[0] : e;
    
            this.startX = this.x;
            this.startY = this.y;
            this.absStartX = this.x;
            this.absStartY = this.y;
            /* pageX和pageY:获取鼠标指针距离文档(HTML)的左上角距离,不会随着滚动条滚动而改变 */
            this.pointX = point.pageX;
            this.pointY = point.pageY;
    
            this.trigger('beforeScrollStart');
        }
    
    用户移动触摸点时触发
    _move(e) {
         ...
    
            if (this.options.preventDefault) {
                /* 阻止屏幕的touchmove,mousemove事件 */
                e.preventDefault();
            }
    
            /* 记录一段滚动之间的间隔距离 deltaX deltaY */
            let point = e.touches ? e.touches[0] : e;
            let deltaX = point.pageX - this.pointX;
            let deltaY = point.pageY - this.pointY;
    
            this.pointX = point.pageX;
            this.pointY = point.pageY;
    
            /* 计算滚动总距离 */
            this.distX += deltaX;
            this.distY += deltaY;
    
            let absDistX = Math.abs(this.distX);
            let absDistY = Math.abs(this.distY);
    
            let timestamp = +new Date();
    
            // We need to move at least 15 pixels for the scrolling to initiate
            /* 如果滑动时间过长 及 距离过短,打断此次滑动事件 */
            if (timestamp - this.endTime > this.options.momentumLimitTime && (absDistY < this.options.momentumLimitDistance && absDistX < this.options.momentumLimitDistance)) {
                return;
            }
    
            deltaX = this.hasHorizontalScroll ? deltaX : 0;
            deltaY = this.hasVerticalScroll ? deltaY : 0;
    
            /* 计算最新的滚动位置 */
            let newX = this.x + deltaX;
            let newY = this.y + deltaY;
    
            //如果滑动超出了界限,就减速或停止
            if (newX > 0 || newX < this.maxScrollX) {
                if (this.options.bounce) {
                    newX = this.x + deltaX / 3;
                } else {
                    newX = newX > 0 ? 0 : this.maxScrollX;
                }
            }
            if (newY > 0 || newY < this.maxScrollY) {
                if (this.options.bounce) {
                    newY = this.y + deltaY / 3;
                } else {
                    newY = newY > 0 ? 0 : this.maxScrollY;
                }
            }
    
    
            if (!this.moved) {
                this.moved = true;
                this.trigger('scrollStart');
            }
            /* 将页面滚动到最新位置 */
            this._translate(newX, newY);
    
            if (timestamp - this.startTime > this.options.momentumLimitTime) {
                /* 如果手指拖动时间过长,更新开始时间及坐标 */
                this.startTime = timestamp;
                this.startX = this.x;
                this.startY = this.y;
            }
    
            if (this.options.probeType > 1) {
                this.trigger('scroll', {
                    x: this.x,
                    y: this.y
                });
            }
    
            let scrollLeft = document.documentElement.scrollLeft || window.pageXOffset || document.body.scrollLeft;
            let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
    
            let pX = this.pointX - scrollLeft;
            let pY = this.pointY - scrollTop;
    
            /* 当手指一直往上或者往下拖动到距离屏幕边缘momentumLimitDistance(即15像素)处,停止move */
            if (pX > document.documentElement.clientWidth - this.options.momentumLimitDistance || pX < this.options.momentumLimitDistance || pY < this.options.momentumLimitDistance || pY > document.documentElement.clientHeight/*屏幕高度*/ - this.options.momentumLimitDistance
            ) {
                this._end(e);
            }
        }
    
    触摸点取消时触发
    _end(e) {
            ...
            if (this.options.preventDefault && !preventDefaultException(e.target, this.options.preventDefaultException)) {
                /* 阻止默认的end事件 */
                e.preventDefault();
            }
    
            /* 如果在边界之外重置 */
            if (this.resetPosition(this.options.bounceTime, ease.bounce)) {
                return;
            }
    
            this.isInTransition = false;
    
            /* 确保最后一个位置是四舍五入的 */
            let newX = Math.round(this.x);
            let newY = Math.round(this.y);
    
            if (!this.moved) {
                /* 滚动距离非常少或者点击动作会触发此操作 */
                this.trigger('scrollCancle');
                return;
            }
    
            this.scrollTo(newX, newY);
    
            this.endTime = +new Date();
    
            let duration = this.endTime - this.startTime;
            let absDistX = Math.abs(newX - this.startX);
            let absDistY = Math.abs(newY - this.startY);
    
            let time = 0;
            // start momentum animation if needed 短时间内移动距离大于300,启动动量动画
            if (this.options.momentum && duration < this.options.momentumLimitTime && (absDistY > this.options.momentumLimitDistance || absDistX > this.options.momentumLimitDistance)) {
                let momentumX = this.hasHorizontalScroll ? momentum(this.x, this.startX, duration, this.maxScrollX, this.options.bounce ? this.wrapperWidth : 0, this.options)
                    : { destination: newX, duration: 0 };
                let momentumY = this.hasVerticalScroll ? momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0, this.options)
                    : { destination: newY, duration: 0 };
                newX = momentumX.destination;
                newY = momentumY.destination;
                time = Math.max(momentumX.duration, momentumY.duration);
                this.isInTransition = 1;
            }
            let easing = ease.swipe;
    
          ...
            /* 将页面滚动到最新位置 */
            this.scrollTo(newX, newY, time, easing);
        }
    
    transform动画执行结束后触发
    _transitionEnd(e) {
            if (e.target !== this.scroller || !this.isInTransition) {
                return;
            }
    
            this._transitionTime();
            /* 动画执行结束后,如果动量动画或者手动滚动越界,重置位置 */
            if (!this.resetPosition(this.options.bounceTime, ease.bounce)) {
                this.isInTransition = false;
                this.trigger('scrollEnd');
            }
        }
    

    此时,一个完整的滚动操作就已经完结了。
    通过这个滚动操作可以看出核心原理,是阻止页面的系统滚动,添加事件监听,在各个dom事件中处理用户滚动事件,通过手动transform将页面移动到合适的位置。

    相关文章

      网友评论

          本文标题:better-scroll用法及源码学习(一)

          本文链接:https://www.haomeiwen.com/subject/oueauctx.html