在如今的前端開發流程中,咱們偶爾會碰見節點滾動同步的需求,所謂的滾動同步就是對於兩個節點的滾動條位置進行同步,在滾動一個節點時同時,另外一個節點也同時滾動到相應的位置。比較常見的相似於使用 markdown
進行文章編輯的博客網站中一般會有兩邊作實時編輯對比,從而須要同步滾動條。而比較不常見的是相似於通常不太可能會在 web 端實現的數據對比工具,可是偶爾你可能會碰到某個後臺管理項目要你作一個表格數據對比工具。這個時候也要利用到節點滾動同步。可是這篇文章的標題咱們加上了 聯動 二字,這是什麼意思呢?讓咱們接着往下看吧。前端
首先,對於滾動同步的解決方案,已經有前輩作了很透徹的分析,這篇文章在於解決滾動同步更多細節上的問題,使同步事件更加的完美,你們若是隻是爲了單純實現兩個節點的滾動同步,能夠參考一下做者 @清夜 在很早以前就發佈的這篇很是詳細的掘金文章 《原生JS控制多個滾動條同步跟隨滾動》,那讓咱們長話短說,先總結一下咱們的需求。node
按照一般的代碼思想來思考,想要實現滾動同步,例如同步 dom1
和 dom2
兩個節點的滾動,你可能會第一時間想出以下的代碼:web
<div class="dom1">...</div>
<div class="dom2">...</div>
let dom1 = document.querySelector('.dom1'),
dom2 = document.querySelector('.dom2')
複製代碼
dom1.addEventListener('scroll', function () {
let top = this.scrollTop,
left = this.scrollLeft;
dom2.scrollTo(left, top)
})
dom2.addEventListener('scroll', function () {
let top = this.scrollTop,
left = this.scrollLeft;
dom1.scrollTo(left, top)
})
複製代碼
那麼在頁面上執行代碼後,你會發現鼠標拖動滾動條,確實能實現滾動同步,但當你使用滾輪的時候,反而有點滾不下去。這是由於 dom1
的滾動事件中帶動了 dom2
的滾動,而 dom2
的滾動事件中又帶動了 dom1
的滾動,兩邊會一直相互同步,相互阻礙。bash
要解決這樣的問題也很簡單,最多見的方法使用 mouseenter 對節點進行標註,標註後的節點即爲鼠標觸發滾動的節點,咱們只要讓非鼠標觸發的節點不執行同步就能夠了,如如下方法markdown
let sign = void 0 // 公共標註
const addScrollEvent = (node, eventFn) => {
if (!eventFn) return
node.addEventListener('mouseenter', e => sign = node.className) // 這裏不一樣的節點用不一樣的 class 值
let event = eventFn.bind(node)
node.addEventListener('scroll', event)
}
addScrollEvent(dom1, function (e) {
if(sign !== this.className) return // 若是滾動不是本身觸發的,直接返回
let top = this.scrollTop,
left = this.scrollLeft;
dom2.scrollTo(left, top)
})
addScrollEvent(dom2, function (e) {
if(sign !== this.className) return
let top = this.scrollTop,
left = this.scrollLeft;
dom1.scrollTo(left, top)
})
複製代碼
這個方法爲了 可複用性,我將它抽象成公用的方法,mousewheel
的方案很明顯不能用來實現這樣的功能,因此咱們就再也不贅述。框架
下面咱們就要說這篇文章的主要討論點,也是這種方法所遺漏的地方,這種遺漏所形成的問題多出現於文章開頭所說的 數據對比 需求,相似於表格數據對比查看,兩個多功能表格進行對比查看,你們都使用過這樣的表格,支持固定列,固定表頭,而後表格固定左列和固定右列和主表格滾動同步。dom
首先,我不怎麼清楚一些 UI 框架的表格滾動同步是怎麼實現的,他們有多是跟 Element UI
同樣,在有固定列的時候,固定列是一個徹底複製主表格的全部行列節點,而後三個表格堆疊在一塊兒,固定列的表格中非固定的列數據用 visibility: hidden;
隱藏。不得不說,這樣的實如今 數據量過大 的狀況下會異常卡頓。工具
那若是是按照咱們以上的方案 (用鼠標進入事件觸發進行標註) 實現主表格和固定列的滾動同步那就可能會出現這篇文章所解答的問題了,因爲咱們的滾動同步,是須要以 鼠標事件 爲前提的。咱們要同步兩個帶固定列的表格滾動條,可能會這麼想——post
首先,找到兩個表格的滾動條主體,對他們進行綁定滾動事件,用上面的方法同步兩個主體的滾動條位置,而後他們本身會同步本身的固定列,這樣應該就完成了吧。以下圖所示網站
那咱們來簡化這個流程,如圖所示,假若有 6 個 div
表明着兩個表格的左中右,咱們用上面的辦法同步每一個表格各自左中右的滾動條以後,以下圖所示
而後咱們做爲一個外面的使用者,再使用上述方式 同步兩個表格的主表部分,也就是 DEMO 中間的滾動條主體
addScrollEvent(tableMain1, function () {
if (sign !== this.className) return
let left = this.scrollLeft,
top = this.scrollTop
tableMain2.scrollTo(left, top)
})
addScrollEvent(tableMain2, function () {
if (sign !== this.className) return
let left = this.scrollLeft,
top = this.scrollTop
tableMain1.scrollTo(left, top)
})
複製代碼
隨之你就發現沒有想象中的那麼容易,問題來了。
能夠看到,咱們並不能把全部的節點經過這種方式進行同步。
緣由其實很簡單,由於咱們這樣的方式實現滾動同步須要一個共同的前提,用 mouseenter
或者 mouseover
事件來作一個標註,當另一個表格這樣實現了以後,表格三列的滾動條同步是須要以三列當中任意一列觸發鼠標事件纔會執行他們的同步代碼。以下圖所示:
既然咱們知道了爲何會形成這種情況的緣由,就意味着要實現滾動同步咱們就不能使用 mouseenter
或者 mouseover
來作標註標註的方案,也就是說咱們必須從 scroll
事件中開始而後從 scroll
事件結束,同時又不能讓他們互相觸發同步,互相阻礙滾動。因而咱們能夠這麼想,咱們的滾動事件同步,老是要經過一個開始滾動的節點,去同步其餘的節點,這和以前 標註法 的思想一致,標註法 只是鼠標事件去建立一個變量,表明滾動的起源,而後其餘節點的滾動事件中只要滾動起源不是本身就不執行同步其餘節點的滾動。
因此一樣的,咱們須要抽象出這麼一個方法,能夠傳入須要同步的節點,而後在方法中設定一個自由變量:
const syncScroller = function () {
let nodes = Array.prototype.filter.call(arguments, item => item instanceof HTMLElement)
if (!nodes.length || nodes.length === 1) return
let sign; // 用於標註
nodes.forEach((ele, index) => {
ele.addEventListener('scroll', function () { // 給每個節點綁定 scroll 事件
});
});
})
// usage
syncScroller(node1, node2, node3)
複製代碼
那麼這個標註是什麼比較好呢?還能夠跟前面代碼同樣使用 className
嗎?固然不適合,面對這樣的問題咱們其實應該問的不是滾動的起源是什麼,由於觸發滾動事件的節點就是滾動的起源,咱們反而應該問的是 何時滾動的同步纔會結束?
這其實很像一個買賣的問題,滾動的起源是負責生產的賣方,而須要同步的節點是買方。 做爲賣方,咱們最大的收益須要什麼條件? 很簡單,買方要多少,咱們就生產多少而後賣多少。
因此在這個問題中,咱們就又轉化爲了一個買方須要多少的問題,因此這個標註其實就是須要滾動同步的數量,而問題的解決方法在知道這個標註以後也十分了然——滾動的起源生產必定的數量並帶動全部節點滾動,在每一個節點滾動後這個數量減去一,直到零則結束。
因此這個方法就變成了這樣:
const syncScroller = function () {
let nodes = Array.prototype.filter.call(arguments, item => item instanceof HTMLElement)
let max = nodes.length
if (!max || max === 1) return
let sign = 0; // 用於標註
nodes.forEach((ele, index) => {
ele.addEventListener('scroll', function () { // 給每個節點綁定 scroll 事件
if (!sign) { // 標註爲 0 時 表示滾動起源
sign = max - 1;
let top = this.scrollTop
let left = this.scrollLeft
for (node of nodes) { // 同步全部除本身之外節點
if (node == this) continue;
node.scrollTo(left, top);
}
} else
-- sign; // 其餘節點滾動時 標註減一
});
});
})
// usage
syncScroller(node1, node2, node3)
複製代碼
讓咱們來看一下結果:
// 在兩個表格中用這種方法同步左中右
syncScroller(tableFixedLeft, tableMain, tableFixedRight)
let tableMain1, tableMain2 // 獲取兩個主表格的滾動主體節點
// 在外部依舊使用 標註法 同步兩個主表格
addScrollEvent(tableMain1, function () {
if (sign !== this.className) return
let left = this.scrollLeft,
top = this.scrollTop
tableMain2.scrollTo(left, top)
})
addScrollEvent(tableMain2, function () {
if (sign !== this.className) return
let left = this.scrollLeft,
top = this.scrollTop
tableMain1.scrollTo(left, top)
})
複製代碼
以下圖所示:
很是的流暢完美!能夠感覺到這個方法在寫這種須要滾動同步的組件當中很是實用,並且最重要的一點是,咱們最後這個例子也證實了咱們在組件使用這個方法至關於把三個節點綁在一塊兒,即便外界從當中單拎一個節點出來也沒有任何影響!
爲了解決表格同步這個功能我真的是想破腦殼了,做者我翻遍了全內外網的滾動同步解決方案,最後仍是本身來動手解決了。
這時候想必你們都知道標題中滾動同步的 聯動 二字是指什麼意思了,可是這個方法我在目前使用還未遇到問題,若是你使用個人方法遇到了問題,能夠在文章評論下方提出,我看到後會及時回覆你的喲(若是我解決得了的話)。
這就是本次文章要展現的全部內容了~ 以爲質量不錯的話請麻煩點個贊叭~
加油!奧裏給!