better-scroll源码阅读(三):上拉下拉与点击事件
上一篇文章里我们介绍了better-scroll滚动相关的核心代码,我们现在清楚的知道当我们的手指在屏幕上滑动的时候,better-scroll在后面都在做哪些事情。
今天我们继续看一下better-scroll是如何处理特殊的滑动操作的,如上拉下拉以及点击操作。没错,在better-scroll里点击也是一种特殊的滑动操作,我们下面会解释。
跟前一篇文章相比,今天需要阅读的代码量并不多,我们先看一下上拉下拉操作。
下拉操作
在第一篇文章里我们已经介绍过better-scroll初始化过程中有一步是初始化实现滚动以外的功能。如下面我们要重点分析的上拉下拉操作。
BScroll.prototype._initExtFeatures = function () {
...
if (this.options.pullUpLoad) {
this._initPullUp()
}
if (this.options.pullDownRefresh) {
this._initPullDown()
}
...
}
接下来的工作就是看一下,_initPullUp()和_initPullDown()两个方法具体做了哪些事情。我们这里先看一下_initPullDown()方法的执行过程。下拉操作相关的代码都定义在src/scroll/pulldown.js文件中,代码不多,我们全部拿过来。
import { ease } from '../util/ease'
import { DIRECTION_DOWN } from '../util/const'
export function pullDownMixin(BScroll) {
BScroll.prototype._initPullDown = function () {
// must watch scroll in real time
this.options.probeType = 3
}
BScroll.prototype._checkPullDown = function () {
const {threshold = 90, stop = 40} = this.options.pullDownRefresh
// check if a real pull down action
if (this.directionY !== DIRECTION_DOWN || this.y < threshold) {
return false
}
if (!this.pulling) {
this.pulling = true
this.trigger('pullingDown')
}
this.scrollTo(this.x, stop, this.options.bounceTime, ease.bounce)
return this.pulling
}
BScroll.prototype.finishPullDown = function () {
this.pulling = false
this.resetPosition(this.options.bounceTime, ease.bounce)
}
}
上面看到,_initPullDown()方法中只有一句话,设置probeType配置项。这里probeType=3表示元素在滚动过渡状态依然会实时派发"scroll"事件,相关的实现方式我们在前一篇文章对scrollTo()方法分析时已经介绍过了。
该文件中还定义了_checkPullDown()和finishPullDown()两个方法。
先看_checkPullDown(),该方法会判断下拉是否超过阈值,是的话则派发一次"pullingdown"事件,并且调用scollTo()方法使元素回滚到(this.x, stop)的位置,stop可以通过配置项自定义。这时滚动元素上方会空余stop高度去显示一些信息,如加载动画或文案提示等。
另外,我们在上一篇文章分析_end()方法时,有看到当手指在屏幕上滑动结束后,会调用_checkPullDown()方法。
BScroll.prototype._end = function (e) {
... // 是否开启滚动,是否已销毁的判断,阻止浏览器默认行为操作
this.trigger('touchEnd', {
x: this.x,
y: this.y
})
...
// if configure pull down refresh, check it first
if (this.options.pullDownRefresh && this._checkPullDown()) {
return
}
...
}
最后,finishPullDown()方法中只有一个操作,即调用resetPosition()重置滚动元素的位置。(关于resetPosition()我们在第一篇文章最后有介绍)。
这里需要注意的是,当下拉操作结束后,即下拉后数据更新完,一定要调用finishPullDown()方法,使滚动元素恢复到原始位置,否则滚动元素会停留在前面提到的(this.x, stop)的位置,导致滚动元素上方多出stop高度的区域。
上拉操作
看完下拉操作,接下来是上拉。有关上拉操作的代码放在src/scroll/pullup.js文件中。
import { DIRECTION_UP } from '../util/const'
export function pullUpMixin(BScroll) {
BScroll.prototype._initPullUp = function () {
// must watch scroll in real time
this.options.probeType = 3
this.pullupWatching = false
this._watchPullUp()
}
BScroll.prototype._watchPullUp = function () {
if (this.pullupWatching) {
return
}
this.pullupWatching = true
const {threshold = 0} = this.options.pullUpLoad
this.on('scroll', checkToEnd)
function checkToEnd(pos) {
if (this.movingDirectionY === DIRECTION_UP && pos.y <= (this.maxScrollY + threshold)) { //判断是否是一次上拉操作
this.trigger('pullingUp')
this.pullupWatching = false
this.off('scroll', checkToEnd)
}
}
}
BScroll.prototype.finishPullUp = function () {
if (this.isInTransition) {
this.once('scrollEnd', () => {
this._watchPullUp()
})
} else {
this._watchPullUp()
}
}
}
可以看到在_initPullUp()方法中,会强制probeType=3,同时调用_watchPullUp()方法。
在_watchPullUp()方法中,会监听"scroll"事件,并执行checkEnd()方法。即在滚动中会反复判断是否存在上拉操作,如果是,则派发"pullingup"事件,同时解绑"scroll"事件的回调函数checkEnd(),避免多次重复派发"pullingup"事件。
此时由于已经解绑checkEnd()回调函数`,即之后的滚动过程中,不会再监测是否有上拉操作。这就是很多同学使用better-scroll过程中,经常遇到下面问题的原因,better-scroll只触发一次"pullingup"事件后就不再触发第二次了。
因此,在上拉操作结束后,一定要记得调用finishPullUp()方法,该方法会再次调用_watchPullUp()方法,为"scroll"事件添加checkEnd()回调函数,保证之后的"pullingup"事件能够顺利触发。
-----更新----- 上面代码可能会导致上拉时触发多次“pullingUp"的bug,在V1.7.0版本中已经修改。具体修改可看该次commit。
问题思考
我们已经看完了上拉下拉操作的实现,现在有一个问题,为什么上拉操作没有向下拉操作一样,放在_end()方法中,即滑动结束后去监测,而是放在"scroll"的事件回调中?
这是由于,"pullingup"事件是可以通过惯性滚动触发的。即当向上的惯性滚动过程中如果满足上拉,会触发"pullingup"事件,但是向下的惯性滚动过程中如果满足下拉条件,不应该触发"pullingdown"事件。归根结底,是两种不同需求。
惯性滚动过程中,可以触发"pullingup"事件,因此需要强制probeType=3,即过渡状态,同样会实时派发"scroll"事件,不断监测是否存在上拉。
但是既然惯性滚动不应该触发"pullingdown"事件,那么为什么下拉初始化_initPullDown()中,也要强制probeType=3??这个我还没有想明白。
点击操作
默认情况下,除了input、textarea、button、select这些元素,better-scroll会阻止浏览器的"click"事件。 另外,better-scroll提供了一个preventDefaultException配置项,允许用户手动配置哪些元素可以接收到浏览器派发的原生"click"事件。
相关代码如下:
BScroll.prototype.handleEvent = function (e) {
switch (e.type) {
...
case 'click':
if (this.enabled && !e._constructed) {
if (!preventDefaultException(e.target, this.options.preventDefaultException)) {
e.preventDefault()
e.stopPropagation()
}
}
break
}
}
这里的e._constructed我们一会再解释。先看下,这里调用了preventDefaultException()方法,表示不满足相应条件的话,会阻止默认的点击事件,同时阻止事件的进一步传递。因为,"click"事件是被绑定在wapper元素上的,所以,如果wapper内部的元素受到点击,"click"事件会在捕获阶段被拦截,并停止传播。
再来看下关键步骤preventDefaultException(e.target, this.options.preventDefaultException),该函数中,第二个参数是用户提供的配置项preventDefaultException,该项是一个对象。
这是一个非常有用的配置,它的 key 是 DOM 元素的属性值,value 可以是一个正则表达式。比如我们想配一个 class 名称为 test 的元素,那么配置规则为 {className:/(^|\s)test(\s|$)/}。
符合正则表达式的元素,不会被阻止"click"事件。
preventDefaultException()方法的实现如下:
export function preventDefaultException(el, exceptions) {
for (let i in exceptions) {
if (exceptions[i].test(el[i])) {
return true
}
}
return false
}
默认情况下,preventDefaultException的值为{ tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/},即默认情况下,input、textarea、button、select这些元素的"click"事件不会被浏览器阻止。所以,如果想要让某些元素的"click"事件不被拦截,可以通过配置preventDefaultException选项实现。
另外,better-scroll还提供了另一个配置项click。
作用:better-scroll 默认会阻止浏览器的原生 click 事件。当设置为 true,better-scroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 _constructed,值为 true。
然后我们来看下,better-scroll是如何实现派发"click"事件的。在手指滑动结束后,bettr-scroll会执行一次_checkClick()操作。
BScroll.prototype._end = function (e) {
...
this.trigger('touchEnd', {
x: this.x,
y: this.y
})
...
// check if it is a click operation
if (this._checkClick(e)) {
this.trigger('scrollCancel')
return
}
}
_checkClick()方法中会判断,如果此次滑动小于15px(即this.moved === false),会被认为是一次点击操作,然后会执行click(e)函数。(为什么是15px,其实是由momentumLimitDistance配置项决定,在上一篇文章介绍_move()函数中,只有滑动距离大于momentumLimitDistance时,才会认为是一次有效的滑动操作,才会执行this.moved = ture)
BScroll.prototype._checkClick = function (e) {
// when in the process of pulling down, it should not prevent click
let preventClick = this.stopFromTransition && !this.pulling
this.stopFromTransition = false
// we scrolled less than 15 pixels
if (!this.moved) {
if (this.options.wheel) {
... //不关心
} else {
if (!preventClick) {
if (this.options.tap) {
// 与click()函数类似,这里不再介绍,有兴趣可以去看代码 https://github.com/ustbhuangyi/better-scroll/blob/master/src/util/dom.js#L122
tap(e, this.options.tap)
}
if (this.options.click) {
click(e) // 关键步骤
}
return true
}
return false
}
}
return false
}
在click()函数中,better-scroll会为除了select, input, textarea外的其他元素创建一个自定义的"click"事件,该函数中的前半部分主要是做"click"事件的属性设置,整个函数关键步骤即new MouseEvent()。同时,为自定义的"click"事件添加一个_constructed标识,作为和原生"click"事件的区分。最后通过dispatchEvent()触发一次自定义的"click"事件。
export function click(e) {
let target = e.target
if (!(/(SELECT|INPUT|TEXTAREA)/i).test(target.tagName)) {
let eventSource
if (e.type === 'mouseup' || e.type === 'mousecancel') {
eventSource = e
} else if (e.type === 'touchend' || e.type === 'touchcancel') {
eventSource = e.changedTouches[0]
}
let posSrc = {}
if (eventSource) {
posSrc.screenX = eventSource.screenX || 0
posSrc.screenY = eventSource.screenY || 0
posSrc.clientX = eventSource.clientX || 0
posSrc.clientY = eventSource.clientY || 0
}
let ev
const event = 'click'
const bubbles = true
// cancelable set to false in case of the conflict with fastclick
const cancelable = false
if (typeof MouseEvent !== 'undefined') {
ev = new MouseEvent(event, extend({
bubbles,
cancelable
}, posSrc))
} else {
ev = document.createEvent('Event')
ev.initEvent(event, bubbles, cancelable)
extend(ev, posSrc)
}
ev._constructed = true // 为自定义"click"事件添加 _constructed标识
target.dispatchEvent(ev) //触发自定义click事件
}
}
从上面的分析可以得到,如果配置项click为true,那么除了select, input, textarea外的其他元素都会收到better-scroll派发的带有_constructed标识的自定义"click"事件。
由于peventDefaultException默认值为{tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/},即input, textarea, button, select元素的都会正常的收到原生的"click"事件。所以,这时候就有一个问题,button元素在配置项click为true时会收到两个"click"事件,一个是原生的浏览器派发的"click"事件,一个是better-scroll派发的带有_constructed标识的"click"事件。
因此,在实际开发过程中如果需要使用"click"事件,不建议设置click配置项为true。最好通过配置preventDefaultException配置项实现。
另外,当better-scroll派发自定义"click"事件时,为什么没有把button元素排除在外??或许这里有什么深意?还是黄老师漏掉的一个bug?
更新 在v1.7.0版本中,黄老师已经修复了这个bug。以后的版本里,click配置项为true时,button元素不再会收到两次"click"事件。
结语
好了,今天的任务已经完成,我们已经清楚better-scroll是如何处理上拉下拉和点击操作的。下一篇文章我们会分析实现picker组件的相关代码。