前言
在日常業務開發中,我們有時會遇到多個組件需要同步滾動的場景;例如在某個頁面中存在多個相關的數據表格,用戶在滾動其中一個表格時,其他表格也同步滾動保證數據行的對齊;這個功能在數據看板、儀表盤等多種場景下都很常見。
實現這種功能并非難事,主要思路為監聽各個組件的滾動事件,當某個組件主動滾動后將自身的滾動狀態同步給其他組件;但這種實現方式背后存在嚴重的隱患:當某個組件接收到來自其他組件的主動滾動信息,并據此對自身的滾動狀態加以同步后,該組件會被動觸發滾動事件,并將自身的滾動信息傳遞給主動滾動組件;在這種情況下,極易造成死循環,并在頁面上的表現為組件不停“抽搐”。
思路
要解決這個問題,我們需要理清思路;我們假設當前存在 A 和 B 兩個可滾動組件需要進行信息同步,按照上述解決方案編寫代碼后,整個事件的觸發流程如下:
A 主動滾動 -> B 同步 A 的滾動信息 -> A 同步 B 的滾動信息 -> A 主動滾動 -> 無限循環
而理想狀態下整個事件的觸發流程如下:
A 主動滾動 -> B 同步 A 的滾動信息 -> A 主動滾動 -> B 同步 A 的滾動信息
兩者中的區別在于,理想狀態下被動滾動的組件不會反復觸發滾動事件造成主動組件的滾動。要解決這個問題,我們可以通過在滾動事件中對引起滾動的來源進行區分,從而判斷是否要進行滾動信息同步。
Coding
首先編寫組件對應的滾動處理程序,當某個組件主動滾動時,scrollPart 為空,則將 scrollPart 設置為當前組件名稱;而主動滾動的組件再次觸發滾動時,將會同步滾動信息給其他組件,此時其他組件會被動觸發滾動事件;由于此前已將 scrollPart 設置為主動滾動事件的組件名稱,因此其他組件被動觸發滾動事件后會將 scrollPart 置空。
在這種情況下,即使主動滾動的組件被其他組件觸發被動滾動,也不會再次進行同步。
type ScrollPart = "compA" | "compB";
let scrollPart: ScrollPart; // 觸發原始滾動的組件名稱
// 根據組件名稱生成組件滾動事件的回調處理程序
const scrollCallbackFactory = (key: ScrollPart) => {
return () => {
if (!scrollPart) {
scrollPart = key;
} else if (scrollPart === key) {
syncScrollState();
} else {
scrollPart = undefined;
}
}
}
接下來需要添加事件監聽程序和事件信息同步程序。
const compA = document.querySelector("#compA");
const compB = document.querySelector("#compB");
compA.addEventListener("scroll", scrollCallbackFactory("compA"));
compA.addEventListener("scroll", scrollCallbackFactory("compB"));
function syncScrollState() {
switch (scrollPart) {
case "compA":
compB.scrollTop = compA.scrollTop;
break;
case "compB":
compA.scrollTop = compB.scrollTop;
break;
}
}
這種處理組件同步的方法是可擴展的,支持橫向、縱向滾動以及多個組件滾動,只需給對應組件添加事件監聽,并在 syncScrollState 函數里按照對應的組件同步滾動信息即可。