better-scroll-blog
better-scroll-blog copied to clipboard
_start、_move、_end(三大核心方法流程)
前言
首先介绍这三大方法之前,我们先去学习下它是如何绑定函数的(写法很精妙)。
在 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
方法里面函数的用途。 - 本篇文章虽然没有很详细的进行对每个部分进行解读,这也是我特意的。在我学习过程中发现,直接拿一个源码代码来解读,文章又长,有些地方可能有的解释的不清楚,所以我下面文章将以功能模块进行拆分解读。