美文网首页
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