我写该文章,主要是想结合代码探究 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
}

better-scroll 源码分析的更多相关文章

  1. [Android实例] Scroll原理-附ScrollView源码分析

    想象一下你拿着放大镜贴很近的看一副巨大的清明上河图, 那放大镜里可以看到的内容是很有限的, 而随着放大镜的上下左右移动,就可以看到不同的内容了 android中手机屏幕就相当于这个放大镜, 而看到的内 ...

  2. [Android实例] Scroll原理-附ScrollView源码分析 (转载)

    想象一下你拿着放大镜贴很近的看一副巨大的清明上河图, 那放大镜里可以看到的内容是很有限的, 而随着放大镜的上下左右移动,就可以看到不同的内容了 android中手机屏幕就相当于这个放大镜, 而看到的内 ...

  3. jQuery 2.0.3 源码分析 事件体系结构

    那么jQuery事件处理机制能帮我们处理那些问题? 毋容置疑首先要解决浏览器事件兼容问题 可以在一个事件类型上添加多个事件处理函数,可以一次添加多个事件类型的事件处理函数 提供了常用事件的便捷方法 支 ...

  4. BOOtstrap源码分析之 tooltip、popover

    一.tooltip(提示框) 源码文件: Tooltip.jsTooltip.scss 实现原理: 1.获取当前要显示tooltip的元素的定位信息(top.left.bottom.right.wid ...

  5. bootstrap源码分析之scrollspy(滚动侦听)

    源码文件: Scrollspy.js 实现功能 1.当滚动区域内设置的hashkey距离顶点到有效位置时,就关联设置其导航上的指定项2.导航必须是 .nav > li > a 结构,并且a ...

  6. jQuery.lazyload使用及源码分析

    前言: 貌似以前自己也写过图片懒加载插件,但是新公司使用的是jQuery.lazyload插件,为了更好的运用,自己还是把源码看了遍,分别记录了如何使用, 插件原理,各个配置属性的完整解释,demo实 ...

  7. gomoblie flappy 源码分析:游戏逻辑

    本文主要讨论游戏规则逻辑,具体绘制技术请参看相关文章: gomoblie flappy 源码分析:图片素材和大小的处理 http://www.cnblogs.com/ghj1976/p/5222289 ...

  8. Robotium源码分析之运行原理

    从上一章<Robotium源码分析之Instrumentation进阶>中我们了解到了Robotium所基于的Instrumentation的一些进阶基础,比如它注入事件的原理等,但Rob ...

  9. 手机自动化测试:appium源码分析之bootstrap十二

    手机自动化测试:appium源码分析之bootstrap十二   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣 ...

  10. 一个普通的 Zepto 源码分析(三) - event 模块

    一个普通的 Zepto 源码分析(三) - event 模块 普通的路人,普通地瞧.分析时使用的是目前最新 1.2.0 版本. Zepto 可以由许多模块组成,默认包含的模块有 zepto 核心模块, ...

随机推荐

  1. Java 中判断类和实例之间的关系

    判断类与实例的关系有以下三种方式 1.instanceof关键字,用来判断对象是否是类的实例 (对象 => 类 )   2.isAssignableFrom,用来判断类型间是否存在派生关系 (类 ...

  2. CCF系列之最大的矩形(201312-3)

    试题名称: 最大的矩形 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 在横轴上放了n个相邻的矩形,每个矩形的宽度是1,而第i(1 ≤ i ≤ n)个矩形的高度是hi.这n个矩 ...

  3. 怎么解决ERROR in Node Sass does not yet support your current environmen问题?

    好久没有重新安装node.js,昨天和小伙伴们一起安装,由于自己是在网上自行下载的node,安装地比较顺利,但另外两个小伙伴用的共享文件夹里自带的node,却是屡次碰到问题,快被逼疯,在运行Vue时总 ...

  4. Docker镜像导致centos-root根分区容量爆满

    当虚拟机服务器运行Docker久了后,发现Docker的文件越来越大,某天发现此台机上的数据库访问不了了,再重启数据库等日志,提示空间不足,查看磁盘空间: root分区满载啊,前段时间还有不少空间的, ...

  5. CentOS 7 修改SSH端口号 和 ssh无法使用密码登录 和 查看pid 端口占用

    如下示例把SSH默认端口22改为122. 1 修改/etc/ssh/sshd_config文件 #Port #找到改行,如果还想继续使用该端口,可以把#去掉,如果使用新端口,可以默认不管 Port # ...

  6. 理解maven的核心概念

    原文出处:http://www.cnblogs.com/holbrook/archive/2012/12/24/2830519.html 好久没进行java方面的开发了,最近又完成了一个java相关的 ...

  7. linkin大话面向对象--组合

    继承是实现类重用的重要手段,但是它有一个最大的坏处:破坏封装.相比之下,组合也是实现类重用的重要方式,但是采用组合方式实现类重用则能提供更好的封装性.比如人有手一样,在人的类中增加一个手的属性. 何时 ...

  8. hadoop - spark on yarn 集群搭建

    一.环境准备 1. 机器: 3 台虚拟机 机器 角色  l-qta3.sp.beta.cn0 NameNode,ResourceManager,spark的master l-querydiff1.sp ...

  9. sed标签

    转载 b label ,无条件跳转到标签label,如果label没有指定,跳转到命令的结尾 t label ,如果最后一次输入的最后一个 s/// 子命令执行成功,跳转到标签label,如果labe ...

  10. 【转】GPS网平差

    进行GPS网平差的目的主要有三个: (1)消除由观测量和已知条件中存在的误差所引起的GPS网在几何上的不一致.包括闭合环闭合差不为0:复测基线较差不为0:通过由基线向量所形成的导线,将坐标由一个已知点 ...