proc07 / better-scroll-blog Goto Github PK
View Code? Open in Web Editor NEWbetter-scroll V1.8.0 源码分析
better-scroll V1.8.0 源码分析
作用:当快速在屏幕中滑动一段距离后(手指离开),根据这段滑动的时间和距离代入到公式,求出最后滚动到的位置,并进行滚动动画。
_end
方法中调用了 momentum 来计算最后的位置。// start momentum animation if needed
/* 必须开启 momentum 配置,滑动的时间小于 momentumLimitTime,且滑动的距离要超过 momentumLimitDistance (px)位置 */
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 {
// code...
}
/**
* current 结束位置
* start 开始位置
* time 时长
* lowerMargin 滚动区域 maxScroll
* wrapperSize 当滚动超过边缘的时候会有一小段回弹动画 (wrapperSize = bounce)
*
* distance 距离
* speed 速度
* deceleration 减速度(系数) 0.001
* destination 目的地
*/
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
// wheel: 为 picker 组件时,加大回弹系数
let rate = wheel ? 4 : 15
// 公式:惯性拖拽 = 最后的位置 + 速度 / 摩擦系数 * 方向
let destination = current + speed / deceleration * (distance < 0 ? -1 : 1)
// picker 组件时,将计算的位置取整
if (wheel && itemHeight) {
destination = Math.round(destination / itemHeight) * itemHeight
}
// 目的地(超过)最大的滚动范围 maxScroll
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
}
}
scrollTo
进行滚动。现在,我们已经知道了这个 momentum 函数的实现,公式:惯性拖拽 = 最后的位置 + 速度 / 摩擦系数 * 方向。
作用:会检测 scroller 内部 DOM 变化,自动调用 refresh 方法重新计算来保证滚动的正确性。它会额外增加一些性能开销,如果你能明确地知道 scroller 内部 DOM 的变化时机并手动调用 refresh 重新计算,你可以把该选项设置为 false。
// init.js
BScroll.prototype._initDOMObserver = function () {
// 判断浏览器支不支持 MutationObserver 方法
if (typeof MutationObserver !== 'undefined') {
let timer
let observer = new MutationObserver((mutations) => {
// 不要在过渡期间进行刷新,或者在边界之外进行刷新
if (this._shouldNotRefresh()) {
return
}
let immediateRefresh = false
let deferredRefresh = false
for (let i = 0; i < mutations.length; i++) {
const mutation = mutations[i]
// attributes 观察目标节点的属性节点(新增或删除了某个属性,以及某个属性的属性值发生了变化)
if (mutation.type !== 'attributes') {
immediateRefresh = true
break
} else {
// dom 应该改变了
if (mutation.target !== this.scroller) {
deferredRefresh = true
break
}
}
}
if (immediateRefresh) {
this.refresh()
} else if (deferredRefresh) {
// attributes changes too often
clearTimeout(timer)
timer = setTimeout(() => {
if (!this._shouldNotRefresh()) {
this.refresh()
}
}, 60)
}
})
const config = {
attributes: true,
childList: true,
subtree: true
}
observer.observe(this.scroller, config)
this.on('destroy', () => {
observer.disconnect()
})
} else {
// 兼容函数
this._checkDOMUpdate()
}
}
BScroll.prototype._shouldNotRefresh = function () {
let outsideBoundaries = this.x > 0 || this.x < this.maxScrollX || this.y > 0 || this.y < this.maxScrollY
return this.isInTransition || this.stopFromTransition || outsideBoundaries
}
首先介绍这三大方法之前,我们先去学习下它是如何绑定函数的(写法很精妙)。
在 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监听器
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 呢?
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
}
}
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')
}
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)
}
}
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
})
}
this.initiated !== _eventType
,不知道是为了解决什么bug,感觉执行不到(应该可以忽略不写)if (!this.enabled || this.destroyed || (this.initiated && this.initiated !== _eventType)) {
return
}
start
move
end
方法里面函数的用途。A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.