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的源碼分析呢,我怕有些人說有些源碼分析的文章就是垃圾,因此至少在字面上進行改變(逃。。。)