• better-scroll 源码分析


            我写该文章,主要是想结合代码探究 better-scroll 是如何处理下列操作的。该过程如下图,用文字描述为:手指触摸屏幕,向上快速滑动,最后在手指离开屏幕后,内容获得动量继续滚动,若内容滚动超越顶部边界会回弹。

            我们从整体开始一步一步来探究。better-scroll 包装了一个 BScroll 类以提供功能,我们可以在 better-scroll/src/index.js 文件中看到,它的构造器中传入两个参数 el 和 options。在构造函数中,比较重要的是执行_init(el, options)方法,如下所示。

    function BScroll(el, options) {
        // ...
        this._init(el, options)
    }

            _init(el, options)方法在better-scroll/src/scroll/init.js文件中定义,相关代码如下。

    BScroll.prototype._init = function (el, options) {
        // ...
    
        this._addDOMEvents() // 添加事件处理函数
    
        this._initExtFeatures() // 初始化特性操作,如下拉刷新
    
        this._watchTransition()
    
        // ...
    }

         因为如何实现特性操作并不是我的主要目的,所以 _initExtFeatures() 我们忽略掉。先来看一下 _watchTransition() 方法,该方法的代码如下。

    BScroll.prototype._watchTransition = function ()  {
        // ...
        let me = this
        let isInTransition = false
        Object.defineProperty(this, 'isInTransition', {
            get () {
                return isInTransition
            },
            set (newVal) {
                isInTransition = newVal
                let el = me.scroller.children.length ? me.scroller.children : [me.scroller]
                let pointerEvents = (isTransition && !me.pulling) ? 'none' : 'auto'
                for (let i = 0; i < le.length; i++) {
                    el[i].style.pointerEvents = pointerEvents
                }
            }
        })
    }

            此方法的功能主要是为BScroll类的实例,使用Object.defineProperty()增加一个isInTransition属性。当将该属性赋值为true时,将会使滚动元素下的子元素的pointerEvents样式属性赋值为none,以此子元素无法点击。该处理将用在元素滚动等状态时,用户的触摸的期望应该触发的是滚动停止等操作,而不是子元素点击事件。

            _addDOMEvents()方法主要用来绑定事件处理程序,其中在源码中还定义了_removeDOMEvents()方法,它们都会调用_handleDOMEvents(eventOperation)方法。不同的是,_addDOMEvents中eventOperation = addEvent,_removeDOMEvents中eventOperation = removeEvent。

            addEvent和removeEvent只是包装了DOM 2级事件处理方法。

    function addEvent(el, type, fn, capture) {
      el.addEventListener(type, fn, {passive: false, capture: !!capture})
    }
    function removeEvent(el, type, fn, capture) {
      el.removeEventListener(type, fn, {passive: false, capture: !!capture})
    }

            看一下_handleDOMEvents(eventOperation)方法的源码,可以看到eventOperation方法像下面这样调用。

    BScroll.prototype._handleDOMEvents = function (eventOperation) {
        // ...
        eventOperation(window, 'resize', this)
    
        // ...
    }

            可以看到,参数fn被传入的是BScroll类实例的this指针,而不是一个方法。其实是在BScroll类中定义了一个handleEvent方法根据事件类型来处理所有事件。这是HTML5的一个特性,具体介绍可以参照该博文 http://www.ayqy.net/blog/handleevent%E4%B8%8Eaddeventlistener/

            handleEvent方法的源码如下。

    BScroll.prototype.handleEvent = function (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 'touchcancel':
        case 'mousecancel':
          this._end(e)
          break
        case 'orientationchange':
        case 'resize':
          this._resize()
          break
        case 'transitionend':
        case 'webkitTransitionEnd':
        case 'oTransitionEnd':
        case 'MSTransitionEnd':
          this._transitionEnd(e)
          break
        case 'click':
          if (this.enabled && !e._constructed) {
            if (!preventDefaultException(e.target, this.options.preventDefaultException)) {
              e.preventDefault()
              e.stopPropagation()
            }
          }
          break
        case 'wheel':
        case 'DOMMouseScroll':
        case 'mousewheel':
          this._onMouseWheel(e)
          break
      }
    }

             最终处理事件的方法落在_start、_move、_end、_transitionEnd。即,手指触摸时 _start 函数进行处理,手指移动时 _move 函数进行处理,手指离开时 _end 函数进行处理,移动到最远距离后 _transitionEnd 函数处理以进行回弹。

            _start 函数中记录了 e.touches[0].pageX 与 e.touches[0].pageY。

    let point = e.touches ? e.touches[0] : e
    this.startX = this.x
    this.startY = this.y
    this.absStartX = this.x
    this.absStartY = this.y
    this.pointX = point.pageX
    this.pointY = point.pageY

             先来看一下 _move 中的主要代码。

    let point = e.touches ? e.touches[0] : e
    let deltaX = point.pageX - this.pointX
    let deltaY = point.pageY - this.pointY
    this.pointX = point.pageXthis.pointY = point.pageY
    this.distX += deltaXthis.distY += deltaY
    
    let absDistX = Math.abs(this.distX)
    let absDistY = Math.abs(this.distY)
    
    let timestamp = getNow()
    // 我们需要移动最小的距离(单位px)为momentumLimitDistance
    if (timestamp - this.endTime > this.options.momentumLimitTime && (absDistY < this.options.momentumLimitDistance && absDistX < this.options.momentumLimitDistance)) {
    return } 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 } } this._translate(newX, newY) if (timestamp - this.startTime > this.options.momentumLimitTime) { this.startTime = timestamp this.startX = this.x this.startY = this.y }

            为了防止用户触摸时的抖动,要求移动的最小距离要大于 momentumLimitDistance。

            接着处理移动边缘,若移动到上下边缘,那么内容移动的距离将为手指移动距离的1/3,使用户产生拥有阻力的感觉。

            接着使用 _translate(newX, newY) 函数改变内容块的 transition css属性来产生移动效果。

            接下来的代码的作用是为了获取手指离开屏幕时的瞬时速度,我们都知道速度等于距离/时间,当采样的时间越小,计算出的速度更接近瞬时速度。better-scroll 的采样时间要求小于 momentumLimitTime。

            最后在 _end 函数中是如何计算出动量的。

    // start momentum animation if needed
    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 = true
    }

            使用 momentum 函数来计算动量,我们接下来看一下 momentu 函数,在 better-scroll/src/util/momentum.js 文件中。

    export function momentum(current, start, time, lowerMargin, wrapperSize, options) {
      let distance = current - start
      let speed = Math.abs(distance) / time
    
      let {deceleration, itemHeight, swipeBounceTime, wheel, swipeTime} = options
      let duration = swipeTime
      let rate = wheel ? 4 : 15
    
      let destination = current + speed / deceleration * (distance < 0 ? -1 : 1)
    
      if (wheel && itemHeight) {
        destination = Math.round(destination / itemHeight) * itemHeight
      }
    
      if (destination < lowerMargin) {
        destination = wrapperSize ? lowerMargin - (wrapperSize / rate * speed) : lowerMargin
        duration = swipeBounceTime
      } else if (destination > 0) {
        destination = wrapperSize ? wrapperSize / rate * speed : 0
        duration = swipeBounceTime
      }
    
      return {
        destination: Math.round(destination),
        duration
      }
    }

            在该函数中计算步骤如此,首先常规计算出 destination = current + speed / deceleration * (distance < 0 ? -1 : 1)。接着判断按照该结果内容是否超越滚动边界,destination < lowerMargin 时超越滚动下边界,destination > 0 超出滚动上边界。然后再分别使用新的公式计算,注意的是该两个公式使用整个滚动内容的大小,即滚动的范围为公式中的元素,以此保证无法超越滚动边界过多距离。

            最后当动量移动结束时,在 _transitionEnd 方法中重新置位即可,关键代码如下。

    BScroll.prototype._transitionEnd = function (e) {
      if (e.target !== this.scroller || !this.isInTransition) {
        return
      }
    
      this._transitionTime()
      if (!this.pulling && !this.resetPosition(this.options.bounceTime, ease.bounce)) {
        this.isInTransition = false
        if (this.options.probeType !== 3) {
          this.trigger('scrollEnd', {
            x: this.x,
            y: this.y
          })
        }
      }
    }
    BScroll.prototype.resetPosition = function (time = 0, easeing = ease.bounce) {
        let x = this.x
        let roundX = Math.round(x)
        if (!this.hasHorizontalScroll || roundX > 0) {
          x = 0
        } else if (roundX < this.maxScrollX) {
          x = this.maxScrollX
        }
    
        let y = this.y
        let roundY = Math.round(y)
        if (!this.hasVerticalScroll || roundY > 0) {
          y = 0
        } else if (roundY < this.maxScrollY) {
          y = this.maxScrollY
        }
    
        if (x === this.x && y === this.y) {
          return false
        }
    
        this.scrollTo(x, y, time, easeing)
    
        return true
      }
  • 相关阅读:
    MySQL数据库高可用集群搭建-PXC集群部署
    高性能高并发网站架构,教你搭建Redis5缓存集群
    redis连接错误3种解决方案System Error MISCONF Redis is configured to save RDB snapshots
    进程异常行为-反弹Shell攻击,KILL多个进程
    Laravel中我们登录服务器通过 Tinker 手动创建后台管理用户
    Laravel5.x的php artisan migrate数据库迁移创建操作报错SQLSTATE[42000]解决
    Laravel:php artisan key:generate三种报错解决方案,修改默认PHP版本(宝塔面板)
    大型网站如何防止崩溃,解决高并发带来的问题
    PHP微信公众平台OAuth2.0网页授权,获取用户信息代码类封装demo(二)
    iOS开发 ReactiveCocoa入门教程 第二部分
  • 原文地址:https://www.cnblogs.com/SyMind/p/8470071.html
Copyright © 2020-2023  润新知