BetterScroll:可能是目前最好用的移動端滾動外掛(原始碼分析)

B_Cornelius發表於2017-11-24

BetterScroll 是一款重點解決移動端各種滾動場景需求的開源外掛GitHub地址,有下列功能支援滾動列表,下拉重新整理,上拉重新整理,輪播圖,slider等功能。
為了滿足這些功能,better-scroll通過使用慣性滾動、邊界回彈、滾動條淡入淡出來確保滾動的流暢。同時還支援很多API和事件,具體支援的事件可以檢視官網講的非常詳細。
由於它基於原生JavaScript 實現,不依賴任何框架,所以既可以原生 JavaScript 引用,也可以與目前前端 MVVM 框架結合使用,比如,其官網上的示例就是與 Vue 的結合。

如何使用:

再講如何使用的之前,我們先來了解一下他的滾動原理:在瀏覽器中的滾動中,當內容的高度高於外邊容器的高度的時候也就出現了滾動條,我們可以通過使用滾動條來看到超出的部分.

better-scroll的原理正是基於這裡,內容部分的寬度/高度必須大於外部寬度/高度。所以在使用
的時候外部容器的需要設定固定寬度,還有一個問題需要設定overflow:hidden,這是因為為了隱藏超出部分。然後就是什麼時候對better-scroll進行初始化,這個有點麻煩,但是所幸,作者已經在vue框架下進行封裝,我們只需要像個麻瓜一樣往裡邊填東西就行了。但是有一點需要注意:滾動的元素只能是第一個容器的第一個元素。原始碼如下:

  // this.scroller就是滾動的內容,this.wrapper是容器
    this.scroller = this.wrapper.children[0]複製程式碼

如果我們需要滾動多個內容怎麼辦呢,就用一個元素將其包裹住,讓他成為容器的第一個子元素就行了。如何使用講完了,我們來講講原始碼,畢竟這是一個原始碼解析的文章

核心程式碼:

1、scrollTo

scrollTo()函式是better-scroll非常核心的一個函式,事實上我們在呼叫scrollToElement的
時候,內部進行的操作還是scrollTo函式

   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的值的,

matrix = matrix[style.transform].split(')')[0].split(', ')
            x = +(matrix[12] || matrix[4])
            y = +(matrix[13] || matrix[5])複製程式碼

我是一臉矇蔽,要是你覺得你水平很高當我沒說。this.options.probeType這個probeType配置表明的是我們需要在什麼情況下派發scroll事件,在better-scroll的原理中是預設阻止瀏覽器的預設行為的,那我們是如何派發事件的呢?

  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的一個選項:

 bounce: {
        style: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
        fn: function (t) {
            return 1 - (--t * t * t * t)
        }
    }複製程式碼

可以看到easing通過貝瑟爾函式,和fn讓我們的動畫顯得不是那麼僵硬。貝瑟爾函式可以去看看,他讓動畫不再那麼突兀。

2、refresh函式

在實際開發中,有時候從後端請求到資料後,我們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的距離了

3、trigger函式

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

相關文章