我写该文章, 主要是想结合代码探究 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()
- // ...
- }
我们先来看_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与addeventlistener/
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
- }
来源: https://www.cnblogs.com/SyMind/p/8470071.html