BetterScroll 是一款重点解决移动端各类滚动场景需求的开源插件GitHub地址,有下列功能支持滚动列表,下拉刷新,上拉刷新,轮播图,slider等功能。
为了知足这些功能,better-scroll经过使用惯性滚动、边界回弹、滚动条淡入淡出来确保滚动的流畅。同时还支持不少API和事件,具体支持的事件能够查看官网讲的很是详细。
因为它基于原生JavaScript 实现,不依赖任何框架,因此既能够原生 JavaScript 引用,也能够与目前前端 MVVM 框架结合使用,好比,其官网上的示例就是与 Vue 的结合。css
再讲如何使用的以前,咱们先来了解一下他的滚动原理:在浏览器中的滚动中,当内容的高度高于外边容器的高度的时候也就出现了滚动条,咱们能够经过使用滚动条来看到超出的部分.前端
better-scroll的原理正是基于这里,内容部分的宽度/高度必须大于外部宽度/高度。因此在使用
的时候外部容器的须要设置固定宽度,还有一个问题须要设置overflow:hidden,这是由于为了隐藏超出部分。而后就是何时对better-scroll进行初始化,这个有点麻烦,可是所幸,做者已经在vue框架下进行封装,咱们只须要像个麻瓜同样往里边填东西就好了。可是有一点须要注意:滚动的元素只能是第一个容器的第一个元素。源码以下:vue
// this.scroller就是滚动的内容,this.wrapper是容器
this.scroller = this.wrapper.children[0]复制代码
若是咱们须要滚动多个内容怎么办呢,就用一个元素将其包裹住,让他成为容器的第一个子元素就好了。如何使用讲完了,咱们来说讲源码,毕竟这是一个源码解析的文章css3
scrollTo()函数是better-scroll很是核心的一个函数,事实上咱们在调用scrollToElement的
时候,内部进行的操做仍是scrollTo函数git
BScroll.prototype.scrollTo = function (x, y, time=0, easing = ease.bounce) {
// useTransition是否使用css3 transition,isInTransition表示是否在滚动过程当中
// this.x表示translate后的位置或者初始化this.x = 0
this.isInTransition = this.options.useTransition
&& time > 0 && (x !== this.x || y !== this.y)
// 若是使用的transition,就调用一系列transition的设置,默认是true
if (!time || this.options.useTransition) {
this._transitionProperty()
this._transitionTimingFunction(easing.style)
this._transitionTime(time)
// 这个函数会更改this.x
this._translate(x, y)
// time存在protoType表示不只在屏幕滑动的时候, momentum 滚动动画运行过程当中实时派发 scroll 事件
if (time && this.options.probeType === 3) {
// 这个函数的做用是派发scroll事件
this._startProbe()
}
// wheel用于picker组件设置,不用管
if (this.options.wheel) {
if (y > 0) {
this.selectedIndex = 0
} else if (y < this.maxScrollY) {
this.selectedIndex = this.items.length - 1
} else {
this.selectedIndex = Math.round(Math.abs(y / this.itemHeight))
}
} else {
// 进行动画this._animate
this._animate(x, y, time, easing.fn)
}
}
};复制代码
咱们来依次看看这个函数,其中简单的操做用代码注明,也就不作太多的描述,其中例如this._transition这种有关transform的都是改变他的位置而已,这里我须要说明一下,咱们在制做轮播图的时候,别去使用transform这种方法来作轮播图,由于当咱们须要获取transform属性值的时候,你会获取到的值是一个很是奇怪的矩阵,获得translateX或者translateY的值是一件很是痛苦的事,能够看看做者是如何获取transform的值的,es6
matrix = matrix[style.transform].split(')')[0].split(', ')
x = +(matrix[12] || matrix[4])
y = +(matrix[13] || matrix[5])复制代码
我是一脸蒙蔽,要是你以为你水平很高当我没说。this.options.probeType这个probeType配置代表的是咱们须要在什么状况下派发scroll事件,在better-scroll的原理中是默认阻止浏览器的默认行为的,那咱们是如何派发事件的呢?github
export function tap(e, eventName) {
let ev = document.createElement('Event')
ev.initEvent(eventName, true, true)
e.target.dispatchEvent(ev)
}复制代码
建立一个element,而后初始化,而后派发事件,咱们就能够像addEventListener('click', fn, false)这样的方式来监听事件addEventListener(eventName, fn, false)。这儿有一个参数叫easing,咱们来看看easing是什么
下面是一个easing的一个选项:vuex
bounce: {
style: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
fn: function (t) {
return 1 - (--t * t * t * t)
}
}复制代码
能够看到easing经过贝瑟尔函数,和fn让咱们的动画显得不是那么僵硬。贝瑟尔函数能够去看看,他让动画再也不那么突兀。后端
在实际开发中,有时候从后端请求到数据后,咱们dom结构发生变化,因此须要调用refresh方法,来看看他是什么玩意数组
BScroll.prototype.refresh = function () {
// return getBoundingRect getRect()
let wrapperRect = getRect(this.wrapper)
this.wrapperWidth = wrapperRect.width
this.wrapperHeight = wrapperRect.height
let scrollerRect = getRect(this.scroller)
this.scrollerWidth = scrollerRect.width
this.scrollerHeight = scrollerRect.height
const wheel = this.options.wheel
// wheel用于picker组件设置
if (wheel) {
this.items = this.scroller.children
this.options.itemHeight = this.itemHeight = this.items.length ? this.scrollerHeight / this.items.length : 0
if (this.selectedIndex === undefined) {
this.selectedIndex = wheel.selectedIndex || 0
}
this.options.startY = -this.selectedIndex * this.itemHeight
this.maxScrollX = 0
this.maxScrollY = -this.itemHeight * (this.items.length - 1)
} else {
// 容许滑动的距离
this.maxScrollX = this.wrapperWidth - this.scrollerWidth
this.maxScrollY = this.wrapperHeight - this.scrollerHeight
}
// 滚动原理容器的宽度小于scroller的宽度
// scrollX设置为true表示能够横向滚动
this.hasHorizontalScroll = this.options.scrollX && this.maxScrollX < 0
this.hasVerticalScroll = this.options.scrollY && this.maxScrollY < 0
// 若是水平不存在的话
if (!this.hasHorizontalScroll) {
this.maxScrollX = 0
this.scrollerWidth = this.wrapperWidth
}
if (!this.hasVerticalScroll) {
this.maxScrollY = 0
this.scrollerHeight = this.wrapperHeight
}
this.endTime = 0
// 移动方向
this.directionX = 0
this.directionY = 0
// return el.offsetLeft
// el.offsetLeft是距离父容器的距离
// el.getBoundingClientRect()返回的是距离页面的距离
this.wrapperOffset = offset(this.wrapper)
// 切换到refresh事件
this.trigger('refresh')
// 重置位置
this.resetPosition()
}复制代码
当咱们的dom结构发生变化的时候,咱们就须要从新计算父容器和容器的大小了,这样就能够从新渲染了,这个函数没什么太难理解的部分,须要注意的是getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。他同element.style获取的有些不一样getBoundingClientRect()获取到的值是相对视口左上角,意思是说在获取right值的时候,事实上是left+element.clientWidth。并且getBoundingClientRect()是只能读取,而element.style不只能读取,还能获取。el.offsetLeft返回的距离父容器的距离,若是咱们须要获得元素距离document的距离的话咱们就须要这样写
export function offset(el) {
let left = 0
let top = 0
while (el) {
left -= el.offsetLeft
top -= el.offsetTop
el = el.offsetParent
}
return {
left,
top
}
}复制代码
一直找到没有父元素的时候,就找到元素距离document的距离了
在better-scroll的源码中,屡次用到trigger函数,咱们来看看他都作了什么
BScroll.prototype.trigger = function (type) {
let events = this._events[type]
if (!events) {
return
}
let len = events.length
let eventsCopy = [...events]
for (let i = 0; i < len; i++) {
let event = eventsCopy[i]
let [fn, context] = event
if (fn) {
fn.apply(context, [].slice.call(arguments,1))
}
}
}复制代码
trigger函数的做用就是切换到某个事件中,获取到事件,而后使用fn进行调用。没什么太大难度,这里想到一点可以体现es6的优越性的地方,好比a = [1,2,3] 在es5中若是咱们须要获取a这个数组长度的时候,咱们须要这样写
let len = a.length复制代码
可是在es6中咱们再也不须要这样写了,这样写就行
let { length } = a复制代码
若是须要获取其余属性值,就麻瓜式往里边填。这里还涉及一个深拷贝的问题,数组和对象的深拷贝这里不作过多阐述。上述最重要的我认为就是这三个函数
这个better-scroll的源码条理清晰,毕竟滴滴D8的段位摆在那儿,很是适合阅读。还有一些就是我对源码分析的文章的见解。在写这个源码分析的文章的时候,我意识到一个问题,那就是不只我本身可以看懂,之前我也写过vuex的源码分析,基本就是把代码所有贴上去,写了大概2万字,我如今以为这种方法欠妥,正确的方式应该就是把重要的部分提取出来,最重要的引导一个思路。把代码整个贴出来,显得繁琐不说,又至关于读者本身把注释看了一遍而已,因此我认为正确的方式是弄出一个思路,读者尝试读源码的时候,可以有一个大概的概念。可以本身理清思路
注释代码已经上传到GitHub至于为何这个标题不写better-scroll的源码分析呢,我怕有些人说有些源码分析的文章就是垃圾,因此至少在字面上进行改变(逃。。。)