better-scroll-blog icon indicating copy to clipboard operation
better-scroll-blog copied to clipboard

_start、_move、_end(三大核心方法流程)

Open proc07 opened this issue 7 years ago • 0 comments

前言

首先介绍这三大方法之前,我们先去学习下它是如何绑定函数的(写法很精妙)。

在 init.js 中 _init 初始化方法中绑定了 _addDOMEvents 函数

BScroll.prototype._init = function (el, options) {
   // some code here...  

    this._addDOMEvents()

  // some code here...  
}

BScroll.prototype._addDOMEvents = function () {
    let eventOperation = addEvent
    this._handleDOMEvents(eventOperation)
}
BScroll.prototype._removeDOMEvents = function () {
    let eventOperation = removeEvent
    this._handleDOMEvents(eventOperation)
}

调用 _addDOMEvents 方法将全部的事件绑定到 Scroll 中。这里把 addEvent 变量传入到

_handleDOMEvents 方法中,而 _removeDOMEvents 方法也是如此。

  • 这样有个好处: 处理所需要的事件交给 _handleDOMEvents 方法就行。在外面传入 绑定 或者 解绑 指令就行。

在这里 addEvent 中却有一个新的参数,引起了我的注意,就是 passive

// dom.js 
export function addEvent(el, type, fn, capture) {
  el.addEventListener(type, fn, {passive: false, capture: !!capture})
}

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

我平常使用的时候,从未见过,表示很好奇,一搜索,才发现。passive监听器

  • passive: addEventListener 中浏览器不知道你有没有调用了 e.preventDefault() 方法来阻止默认事件,所以会在执行的时候进行监听,但在某些性能低的手机中可能出现卡顿的现象。为了解决这个问题 passive 参数就诞生了,设置为 true 时,浏览器不会去监听,即使你调用了也失效。
BScroll.prototype._handleDOMEvents = function (eventOperation) {
    let target = this.options.bindToWrapper ? this.wrapper : window
    eventOperation(window, 'orientationchange', this)
    eventOperation(window, 'resize', this)
    if (this.options.click) {
      eventOperation(this.wrapper, 'click', this, true)
    }
    if (!this.options.disableMouse) {
      eventOperation(this.wrapper, 'mousedown', this)
      eventOperation(target, 'mousemove', this)
      eventOperation(target, 'mousecancel', this)
      eventOperation(target, 'mouseup', this)
    }
    if (hasTouch && !this.options.disableTouch) {
      eventOperation(this.wrapper, 'touchstart', this)
      eventOperation(target, 'touchmove', this)
      eventOperation(target, 'touchcancel', this)
      eventOperation(target, 'touchend', this)
    }
    eventOperation(this.scroller, style.transitionEnd, this)
}

将所有的事件绑定到这里,eventOperation 中原本第3个参数是函数,为什么换成了 this 呢?

  • 在 addEventListener 如果绑定的是一个对象类型的话,则触发事件的时候会默认调用 this 这个对象下的一个叫 handleEvent 方法。
BScroll.prototype.handleEvent = function (e) {
    switch (e.type) {
      // code...
      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
      // code...
      case 'wheel':
      case 'DOMMouseScroll':
      case 'mousewheel':
        this._onMouseWheel(e)
        break
    }
}
  • 这样写即可以兼容PC端、移动端、各个浏览器 事件,又减少了重复绑定的问题。perfect

_start 函数流程

BScroll.prototype._start = function (e) {
    let _eventType = eventType[e.type]
    /************ 1、判断 PC 端只允许左键点击 ***************/
    if (_eventType !== TOUCH_EVENT) {
      if (e.button !== 0) {
        return
      }
    }
    /************ 2、判断是否可操作、是否未销毁、但是第3个判断就有点让人不解了  ***************/
    // 在下面 _eventType = this.initiated,那如果 this.initiated 存在的话,
    // 什么情况下 this.initiated !== _eventType?
    if (!this.enabled || this.destroyed || (this.initiated && this.initiated !== _eventType)) {
      return
    }
    // _eventType 两种值:TOUCH_EVENT = 1,MOUSE_EVENT = 2
    this.initiated = _eventType

    /************ 3、是否阻止默认事件  ***************/
    // eventPassthrough 参数的设置会导致 preventDefault 参数无效,这里要注意!
    // preventDefaultException:input、textarea、button、select 原生标签默认事件则不阻止!
    if (this.options.preventDefault && !preventDefaultException(e.target, this.options.preventDefaultException)) {
      e.preventDefault()
    }

    // 作用:在第一次进入 `_move` 方法时触发 scrollStart 函数。
    this.moved = false 
    // 记录开始位置到结束位置之间的距离
    this.distX = 0
    this.distY = 0
    // _end 函数判断移动的方向 -1(左或上方向) 0(默认) 1(右或下方向)
    this.directionX = 0 
    this.directionY = 0
    this.movingDirectionX = 0
    this.movingDirectionY = 0
    // 滚动的方向位置(h=水平、v=垂直)
    this.directionLocked = 0

    /************ 4、设置 transition 运动时间,不传参数默认为0  ***************/  
    this._transitionTime()
    this.startTime = getNow()

    if (this.options.wheel) {
      this.target = e.target
    }

    /************ 5、若上一次滚动还在继续时,此时触发了_start,则停止到当前滚动位置 ***************/
    this.stop()

    let point = e.touches ? e.touches[0] : e
   
    // 在 _end 方法中用于计算快速滑动 flick
    this.startX = this.x
    this.startY = this.y
    // 在 _end 方法中给this.direction(X|Y) 辨别方向
    this.absStartX = this.x
    this.absStartY = this.y
   // 实时记录当前的位置
    this.pointX = point.pageX
    this.pointY = point.pageY

    /** 6、BScroll 还提供了一些事件,方便和外部做交互。如外部使用 on('beforeScrollStart') 方法来监听操作。 **/
    this.trigger('beforeScrollStart')
}

_move 函数流程

BScroll.prototype._move = function (e) {
    // 老样子,和 _start 方法中一样
    if (!this.enabled || this.destroyed || eventType[e.type] !== this.initiated) {
      return
    }
    if (this.options.preventDefault) {
      e.preventDefault()
    }

    let point = e.touches ? e.touches[0] : e
    // 每触发一次 touchmove 的距离
    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 = getNow()

    /********  1、需要移动至少 (momentumLimitDistance) 个像素来启动滚动  **********/
    if (timestamp - this.endTime > this.options.momentumLimitTime && (absDistY < this.options.momentumLimitDistance && absDistX < this.options.momentumLimitDistance)) {
      return
    }

    /******* 2、如果你在一个方向上滚动锁定另一个方向 ******/
    if (!this.directionLocked && !this.options.freeScroll) {
      /** 
       *  absDistX:横向移动的距离
       *  absDistY:纵向移动的距离
       *  触摸开始位置移动到当前位置:横向距离 > 纵向距离,说明当前是在往水平方向(h)移动,反之垂直方向(v)移动。
       */
      if (absDistX > absDistY + this.options.directionLockThreshold) {
        this.directionLocked = 'h'		// lock horizontally
      } else if (absDistY >= absDistX + this.options.directionLockThreshold) {
        this.directionLocked = 'v'		// lock vertically
      } else {
        this.directionLocked = 'n'		// no lock
      }
    }
   
    /**** 3、若当前锁定(h | v)方向, eventPassthrough 为 锁定方向 相反方向的话则阻止默认事件  ****/
    if (this.directionLocked === 'h') {
      if (this.options.eventPassthrough === 'vertical') {
        e.preventDefault()
      } else if (this.options.eventPassthrough === 'horizontal') {
        this.initiated = false
        return
      }
      deltaY = 0
    } else if (this.directionLocked === 'v') {
      if (this.options.eventPassthrough === 'horizontal') {
        e.preventDefault()
      } else if (this.options.eventPassthrough === 'vertical') {
        this.initiated = false
        return
      }
      deltaX = 0
    }

    // 如果没有开启 freeScroll,只允许一个方向滚动,另一个方向则要清零。
    deltaX = this.hasHorizontalScroll ? deltaX : 0
    deltaY = this.hasVerticalScroll ? deltaY : 0
    this.movingDirectionX = deltaX > 0 ? DIRECTION_RIGHT : deltaX < 0 ? DIRECTION_LEFT : 0
    this.movingDirectionY = deltaY > 0 ? DIRECTION_DOWN : deltaY < 0 ? DIRECTION_UP : 0
    // 最后滚动到的位置
    let newX = this.x + deltaX
    let newY = this.y + deltaY

    // Slow down or stop if outside of the boundaries
    /**** 4、如果超过边界的话,设置了 bounce 参数就让它回弹过来 *****/
    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
      }
    }
    
    // 在 _start 方法中设置的 moved 变量,现在使用到了。
    if (!this.moved) {
      this.moved = true
      this.trigger('scrollStart')
    }
   // 进行滚动...
    this._translate(newX, newY)
    
   /***  5、大于 momentumLimitTime 是不会触发flick\momentum,赋值是为了重新计算,能够在_end函数中触发flick\momentum ***/
    if (timestamp - this.startTime > this.options.momentumLimitTime) {
      this.startTime = timestamp
      this.startX = this.x
      this.startY = this.y
      // 能进入这里的话,都是滚动的比较慢的(大于momentumLimitTime = 300ms)
      if (this.options.probeType === 1) {
        this.trigger('scroll', {
          x: this.x,
          y: this.y
        })
      }
    }
    // 实时的派发 scroll 事件
    if (this.options.probeType > 1) {
      this.trigger('scroll', {
        x: this.x,
        y: this.y
      })
    }

    /**
     * document.documentElement.scrollLeft  获取页面文档向右滚动过的像素数 (FireFox和IE中)
     * document.body.scrollTop 获取页面文档向下滚动过的像素数  (Chrome、Opera、Safari中)
     * window.pageXOffset (所有主流浏览器都支持,IE 8 及 更早 IE 版本不支持该属性)
     */
    let scrollLeft = document.documentElement.scrollLeft || window.pageXOffset || document.body.scrollLeft
    let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop
    // 触摸点(pointX)相对于页面的位置(包括页面原始滚动距离scrollLeft)
    let pX = this.pointX - scrollLeft
    let pY = this.pointY - scrollTop
   
    /** 6、距离页面(上下左右边上)15px以内直接触发_end事件 **/
    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 函数流程

BScroll.prototype._end = function (e) {
    if (!this.enabled || this.destroyed || eventType[e.type] !== this.initiated) {
      return
    }
    this.initiated = false

    if (this.options.preventDefault && !preventDefaultException(e.target, this.options.preventDefaultException)) {
      e.preventDefault()
    }
   // 派发出 touchEnd 事件
    this.trigger('touchEnd', {
      x: this.x,
      y: this.y
    })
    // 属于一个状态:表示关闭(transition)过渡动画
    this.isInTransition = false

    //  取整
    let newX = Math.round(this.x)
    let newY = Math.round(this.y)
    // (下面辨别移动方向) =  结束位置 - 开始位置
    let deltaX = newX - this.absStartX
    let deltaY = newY - this.absStartY
    this.directionX = deltaX > 0 ? DIRECTION_RIGHT : deltaX < 0 ? DIRECTION_LEFT : 0
    this.directionY = deltaY > 0 ? DIRECTION_DOWN : deltaY < 0 ? DIRECTION_UP : 0

    /**  1、配置下拉刷新  **/
    if (this.options.pullDownRefresh && this._checkPullDown()) {
      return
    }

    /** 2、检查它是否为单击操作 (里面对 Picker组件和Tap 点击事件进行处理) **/
    if (this._checkClick(e)) {
      this.trigger('scrollCancel')
      return
    }

    /** 3、如果超出滚动范围之外,则滚动回 0 或 maxScroll 位置 **/
    if (this.resetPosition(this.options.bounceTime, ease.bounce)) {
      return
    }
   /** 4、滚动到指定的位置 **/
    this.scrollTo(newX, newY)

    this.endTime = getNow()
   // startTime、startX、startY 在 _move 函数中移动超过( momentumLimitTime)会重新赋值
    let duration = this.endTime - this.startTime
    let absDistX = Math.abs(newX - this.startX)
    let absDistY = Math.abs(newY - this.startY)

    /** 5、flick 只有使用了snap组件才能执行 **/
    if (this._events.flick && duration < this.options.flickLimitTime && absDistX < this.options.flickLimitDistance && absDistY < this.options.flickLimitDistance) {
      this.trigger('flick')
      return
    }

    let time = 0
    /** 6、开启动量滚动 (当快速在屏幕上滑动一段距离的时候,会根据滑动的距离和时间计算出动量,并生成滚动动画)**/
    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
    } else {
      if (this.options.wheel) {
        newY = Math.round(newY / this.itemHeight) * this.itemHeight
        time = this.options.wheel.adjustTime || 400
      }
    }

    let easing = ease.swipe
    /** 7、在 snap 组件中,计算到最近的一页轮播图位置上 **/
    if (this.options.snap) {
      let snap = this._nearestSnap(newX, newY)
      this.currentPage = snap
      time = this.options.snapSpeed || Math.max(
          Math.max(
            Math.min(Math.abs(newX - snap.x), 1000),
            Math.min(Math.abs(newY - snap.y), 1000)
          ), 300)
      newX = snap.x
      newY = snap.y

      this.directionX = 0
      this.directionY = 0
      easing = this.options.snap.easing || ease.bounce
    }
   /** 8、进入执行滚动到最后的位置(启动了动量滚动) **/
    if (newX !== this.x || newY !== this.y) {
      // change easing function when scroller goes out of the boundaries
      if (newX > 0 || newX < this.maxScrollX || newY > 0 || newY < this.maxScrollY) {
        easing = ease.swipeBounce
      }
      this.scrollTo(newX, newY, time, easing)
      return
    }

    if (this.options.wheel) {
      this.selectedIndex = Math.round(Math.abs(this.y / this.itemHeight))
    }
    /** 9、在 snap 组件中触发了 scrollEnd 函数,用于无缝滚动 **/
    this.trigger('scrollEnd', {
      x: this.x,
      y: this.y
    })
}

疑点

  • 第3个条件语句中的 this.initiated !== _eventType,不知道是为了解决什么bug,感觉执行不到(应该可以忽略不写)
if (!this.enabled || this.destroyed || (this.initiated && this.initiated !== _eventType)) {
  return
}

总结

  • 这里只是简单介绍了 start move end 方法里面函数的用途。
  • 本篇文章虽然没有很详细的进行对每个部分进行解读,这也是我特意的。在我学习过程中发现,直接拿一个源码代码来解读,文章又长,有些地方可能有的解释的不清楚,所以我下面文章将以功能模块进行拆分解读。

proc07 avatar Jan 31 '18 10:01 proc07