原生JS控制多個滾動條同步跟隨滾動

在一些支持用 markdown寫文章的網站,例如 掘金 或者 CSDN等,後臺寫做頁面,通常都是支持 markdown即時預覽的,也就是將整個頁面分紅兩部分,左半部分是你輸入的 markdown文字,右半部分則即時輸出對應的預覽頁面,例以下面就是 CSDN後臺寫做頁面的 markdown即時預覽效果:css

scrollBy

本文不是闡述如何從 0實現這種效果的(後續 極可能 會單出文章,),拋開其餘,單看頁面主體中左右兩個容器元素,即 markdown輸入框元素和預覽顯示框元素html

本文要探討的是,當這兩個容器元素的內容都超出了容器高度,即都出現了滾動框的時候,如何在其中一個容器元素滾動時,讓另一個元素也隨之滾動。git


DOM結構

既然是與滾動條有關,那麼首先想到 js中控制滾動條高度的一個屬性: scrollTop,只要能控制這個屬性的值,天然也就能控制滾動條的滾動了。github

對於如下 DOM結構:chrome

<div id="container">
  <div class="left"></div>
  <div class="right"></div>
</div>
複製代碼

其中,.left元素是左半部分輸入框容器元素,.right元素是右半部分顯示框容器元素,.container是它們共同的父元素。瀏覽器

因爲須要溢出滾動,因此還須要設置一下對應的樣式(只是關鍵樣式,非所有):bash

#container {
  display: flex;
  border: 1px solid #bbb;
}
.left, .right {
  flex: 1;
  height: 100%;
  word-wrap: break-word;
  overflow-y: scroll;
}
複製代碼

再向 .left.right元素中塞入足夠的內容,讓兩者出現滾動條,就是下面這種效果:markdown

0.png

樣式是出來個大概了,下面就能夠在這些 DOM上進行一系列的操做了。性能


初次嘗試

大體思路,監聽兩個容器元素的滾動事件,在其中一個元素滾動的時候,獲取這個元素的 scrollTop屬性的值,同時將此值設置爲另一個滾動元素的 scrollTop值便可。flex

例如:

var l=document.querySelector('.left')
var r=document.querySelector('.right')
l.addEventListener('scroll',function(){
  r.scrollTop = l.scrollTop
})
複製代碼

效果以下:

scrollBy_1

彷佛很不錯,可是如今是不只想讓右邊跟隨左邊滾動,還想左邊跟隨右邊滾動,因而再加如下代碼:

l.addEventListener('scroll',function(){
  r.scrollTop = l.scrollTop
})
複製代碼

看上去很不錯,然而,哪有那麼簡單的事情。

這個時候你再用鼠標滾輪進行滾動的時候,卻發現滾動得有點吃力,兩個容器元素的滾動彷佛被什麼阻礙住了,很難滾動。

仔細分析,緣由很簡單,當你在左邊滾動的時候,觸發了左邊的滾動事件,因而右邊跟隨滾動,可是與此同時右邊的跟隨滾動也是滾動,因而也觸發了右邊的滾動,因而左邊也要跟隨右邊滾動...而後就進入了一個相似於相互觸發的狀況,因此就會發現滾動得很吃力。


解決scroll事件同時觸發的問題

想要解決上述問題,暫時有如下兩種方案。

scroll事件換成 mousewheel事件

因爲 scroll事件不只會被鼠標主動滾動觸發,同時改變容器元素的 scrollTop也會觸發,元素的主動滾動其實就是鼠標滾輪觸發的,因此能夠將scroll事件換成一個對鼠標滾動敏感而不是元素滾動敏感的事件:'mousewheel',因而上述監聽代碼變成了:

l.addEventListener('mousewheel',function(){
    r.scrollTop = l.scrollTop
})
r.addEventListener('mousewheel',function(){
    l.scrollTop = r.scrollTop
})
複製代碼

效果以下:

scrollBy_2

彷佛是有點用,可是實際上還有兩個問題。

  • 當滾動其中一個容器元素的時候,另一個容器元素雖然也跟着滾動,但滾動得並不流暢,高度有明顯的瞬間彈跳

在網上找了一圈,沒有找到關於 wheel事件滾動頻率相關內容,我推測這可能就是此事件的一個 feature

鼠標每次滾動基本上都並非以 1px爲單位的,其最小單元遠比 scroll事件小的多,我用個人鼠標在 chrome瀏覽器上滾動,每次滾過的距離都剛好是 100px,不一樣的鼠標或者瀏覽器這個數值應該都是不同的,若是你的鼠標質量比較好,齒輪比較精細,那麼應該就會小於 100px, 跳動也就不會那麼大,個人鼠標是公司給配的電腦自帶的,做用只限於能用,因此齒輪刻度比較大,而 wheel事件其實真正監聽的是鼠標滾輪滾過一個齒輪卡點的事件,這也就能解釋爲什麼會出現彈跳的現象了。

這裏寫圖片描述

通常來講,鼠標滾輪每滾過一個齒輪卡點,就能監聽到一個wheel事件,從開始到結束,被鼠標主動滾動的元素已經滾動了 100px,因此另一個跟隨滾動的容器元素也就瞬間跳動了 100px

而之因此上述 scroll事件不會讓跟隨滾動元素出現瞬間彈跳,則是由於跟隨滾動元素每次 scrollTop發生變化時,其值不會有 100px那麼大的跨度,可能也沒有小到1px,但因爲其觸發頻率高,滾動跨度小,最起碼在視覺上就是平滑滾動的了。

若是你想讓右側滾動框也平滑滾動,也是能夠作到的,當每次監聽到 wheel事件的時候,也別管它相比於上次是差了100px仍是 50px的,始終都讓右側的跟隨滾動框按照 10px(或者再稍大點或稍小點的跨度,只要給人視覺上的感覺是平滑滾動而且延遲不是太大就好了)來滾動,連續滾動 10次,那就是100px了,一樣能到達準確的位置,例如以下代碼:

function scrollToY(rightELe, toY, step=10) {
    let diff = rightELe.scrollTop - toY
    let realStep = diff > 0 ? -step : step
    if(Math.abs(diff) > step) {
        rightELe.scrollTop = rightELe.scrollTop + realStep
        requestAnimationFrame(()=>{
            scrollToY(rightELe, toY, step)
        })
    } else {
        rightELe.scrollTop = toY
    }
}
複製代碼
  • wheel只是監聽鼠標滾輪事件,但若是是用鼠標拖動滾動條,就不會觸發此事件,另外的容器元素也就不會跟隨滾動了

這個其實很好解決,用鼠標拖動滾動條確定是能觸發 scroll事件的,而在這種狀況下,你確定可以很輕易地判斷出這個被拖動的滾動條是屬於哪一個容器元素的,只須要處理這個容器的滾動事件,另一個跟隨滾動容器的滾動事件不作處理便可。

  • wheel事件的兼容問題

wheel事件是 DOM Level3的標準事件,可是除了此事件以外,還有不少非標準事件,不一樣的瀏覽器內核使用不一樣的標準,因此可能還須要按狀況來進行兼容,具體可見 MDN MouseWheelEvent

w3c1


實時判斷

若是你難以忍受 wheel的彈跳,也很差肯定右側跟隨滾動框每次滾動的跨度到底多大才能完美,更不想考慮各類兼容,那麼其實還有另外的路能夠走得通,依舊是 scroll事件,只不過須要作一些額外的工做。

scroll事件的問題在於,沒有判斷當前主動滾動的是哪個容器元素,只要肯定了主動滾動的容器元素,這事就好辦了,例如上述使用 wheel事件中,用鼠標拖動滾動條之因此可以使用 scroll事件,就是由於可以很容易地肯定當前主動滾動容器元素是哪個。

因此,問題的關鍵在於,如何判斷出當前主動滾動的容器元素,只要解決了這個問題,剩下的就很好辦了。

不管是鼠標滾輪滾動仍是鼠標按在滾動條上拖動滾動條滾動,都會觸發 scroll事件,而且這個時候,在座標系 Z軸上,鼠標的座標確定是位於滾動容器元素所佔的面積以內的,也就是說,在 Z軸上,鼠標確定是懸浮或者位於滾動容器元素之上。

鼠標在屏幕上移動的時候,是能夠獲取到鼠標當前座標的。

3.png

其中,clientXclientY就是當前鼠標相對於視口的座標,能夠認爲,只要這個座標在某個滾動容器的範圍內,則認爲這個容器元素就是主動滾動容器元素,容器元素的座標範圍可使用 getBoundingClientRect進行獲取。

下面是鼠標移動到 .left元素中的示例代碼:

if (e.clientX>l.left && e.clientX<l.right && e.clientY>l.top) {
    // 進入 .left元素中
}
複製代碼

這樣確實是能夠的,不過考慮到兩個滾動容器元素幾乎佔據了整個屏幕面積,因此 mousemove所要監聽的面積未免有點大,對於性能可能要求較高,因此其實能夠換成 mouseover事件,只須要監聽鼠標有沒有進入到某個滾動容器元素便可,也省去上述的座標判斷了。

l.addEventListener('mouseover',function(){
  // 進入 .left滾動容器元素內
})
複製代碼

當肯定了鼠標主動滾動的容器元素是哪個時,只須要處理這個容器的滾動事件,另一個跟隨滾動容器的滾動事件不作處理便可。

scrollBy_3

嗯,效果很不錯,性能也很好,perfect,能夠收工嘍~

那一屋!

事情沒有那麼簡單!

zj


按比例滾動

上述示例所有是在兩個滾動容器元素的內容高度徹底一致的狀況下的效果,若是這兩個滾動容器元素的內容高度不一樣呢?

那就是下面這種效果:

scrollBy_4

可見,因爲兩個滾動容器元素的內容高度不一樣,因此最大的 scrollTop也就不一樣,就會出現當其中一個 scrollTop值較小的元素滾到底時,另一個元素還停留在一半,或者當其中一個 scrollTop值較大的元素才滾到一半時,另一個元素就已經滾到底了。

這種狀況很常見,例如你用 markdown寫做時,一個一級標題標記 #在編輯模式下佔用的高度,通常都是小於預覽模式佔用的高度的,這樣就出現了左右兩側滾動高度不一致的狀況。

因此,若是將這種狀況也考慮進來的話,那麼就不能簡單地爲兩個滾動容器元素相互設置 scrollTop值那麼簡單。

雖然沒法固定住滾動容器內容的高度,可是有一點能夠肯定,滾動條最大滾動高度,或者說 scrollTop的值,確定是與滾動容器內容的高度與滾動容器自己的高度呈必定的關係。

因爲須要知道滾動容器內容的高度,還要存在滾動條,因此須要給此容器元素加個子元素,子元素高度不限,就是滾動容器內容的高度,容器高度固定,溢出滾動便可。

<div id="container">
  <div class="left">
	 <div class="child"></div>
  </div>
  <div class="right">
	  <div class="child"></div>
  </div>
</div>
複製代碼

結構示例以下:

4

經過個人觀察推論與實踐驗證,已經肯定下來了它們之間的關係,很簡單,就是最基本的加減法運算:

滾動條的最大滾動高度(scrollTopMax) = 滾動容器內容的高度(即子元素高度ch) - 滾動容器自己的高度(即容器元素高度ph)
複製代碼

math1

也就是說,若是已經肯定了滾動容器內容的高度(即子元素高度ch)與滾動容器自己的高度(即容器元素高度ph),那麼就必定能肯定滾動條的最大滾動高度(scrollTop),而這兩個高度值基本上都是能夠獲取到的,因此就能獲得 scrollTop

所以,想要讓兩個滾動元素容器等比例上下滾動,即其中一個元素滾到頭或者滾到底,另一個元素也能對應滾到頭和滾到底,那麼只要獲得這兩個滾動容器元素之間的 scrollTop最大值的比例(scale)就好了。

math2

肯定了 scale以後,實時滾動時,只須要獲取主動滾動容器元素的 scrollTop1,就能獲得另一個跟隨滾動的容器元素對應的 scrollTop2

math3

思路弄清晰了,寫代碼就是很容易的事情了,效果以下:

scrollBy_5

很順滑~

這裏寫圖片描述


小結

上述基本上已經實現了需求,可能在實踐過程當中還須要根據實際狀況來進行必定的修改,例如若是你編寫一個 markdown的在線編輯和預覽頁面,就須要根據輸入內容的高度實時更新 scale值,不過主體已經搞定,小修小改就沒什麼難度了。

另外,本文所述不只是針對兩個滾動容器元素的跟隨滾動,同時也可擴展開來,更多的元素間的跟隨滾動都是能夠根據本文思路來實現的,本文只是爲了方便講解而具體到了兩個元素上。

本文的可運行簡單示例代碼已經放到 Github上了,有興趣能夠看看,別忘了 star 啊~

相關文章
相關標籤/搜索