在一些支持用 markdown寫文章的網站,例如 掘金 或者 CSDN等,后臺寫作頁面,一般都是支持 markdown即時預覽的,也就是將整個頁面分成兩部分,左半部分是你輸入的 markdown文字,右半部分則即時輸出對應的預覽頁面。
本文不是闡述如何從 0實現(xiàn)這種效果的(后續(xù) 很可能 會單出文章,),拋開其他,單看頁面主體中左右兩個容器元素,即 markdown輸入框元素和預覽顯示框元素
本文要探討的是,當這兩個容器元素的內容都超出了容器高度,即都出現(xiàn)了滾動框的時候,如何在其中一個容器元素滾動時,讓另外一個元素也隨之滾動。
既然是與滾動條有關,那么首先想到 js中控制滾動條高度的一個屬性: scrollTop,只要能控制這個屬性的值,自然也就能控制滾動條的滾動了。
對于以下 DOM結構:
其中,.left元素是左半部分輸入框容器元素,.right元素是右半部分顯示框容器元素,.container是它們共同的父元素。
由于需要溢出滾動,所以還需要設置一下對應的樣式(只是關鍵樣式,非全部):
再向 .left和 .right元素中塞入足夠的內容,讓二者出現(xiàn)滾動條,
樣式是出來個大概了,下面就可以在這些 DOM上進行一系列的操作了。
大致思路,監(jiān)聽兩個容器元素的滾動事件,在其中一個元素滾動的時候,獲取這個元素的 scrollTop屬性的值,同時將此值設置為另外一個滾動元素的 scrollTop值即可。
似乎很不錯,但是現(xiàn)在是不僅想讓右邊跟隨左邊滾動,還想左邊跟隨右邊滾動,于是再加以下代碼:
看上去很不錯,然而,哪有那么簡單的事情。
這個時候你再用鼠標滾輪進行滾動的時候,卻發(fā)現(xiàn)滾動得有點吃力,兩個容器元素的滾動似乎被什么阻礙住了,很難滾動。
仔細分析,原因很簡單,當你在左邊滾動的時候,觸發(fā)了左邊的滾動事件,于是右邊跟隨滾動,但是與此同時右邊的跟隨滾動也是滾動,于是也觸發(fā)了右邊的滾動,于是左邊也要跟隨右邊滾動…然后就進入了一個類似于相互觸發(fā)的情況,所以就會發(fā)現(xiàn)滾動得很吃力。
想要解決上述問題,暫時有以下兩種方案。
由于 scroll事件不僅會被鼠標主動滾動觸發(fā),同時改變容器元素的 scrollTop也會觸發(fā),元素的主動滾動其實就是鼠標滾輪觸發(fā)的,所以可以將scroll事件換成一個對鼠標滾動敏感而不是元素滾動敏感的事件:’mousewheel’。
似乎是有點用,但是實際上還有兩個問題。
在網上找了一圈,沒有找到關于 wheel事件滾動頻率相關內容,我推測這可能就是此事件的一個 feature
鼠標每次滾動基本上都并不是以 1px為單位的,其最小單元遠比 scroll事件小的多,我用我的鼠標在 chrome瀏覽器上滾動,每次滾過的距離都恰好是 100px,不同的鼠標或者瀏覽器這個數(shù)值應該都是不一樣的。
如果你的鼠標質量比較好,齒輪比較精細,那么應該就會小于 100px, 跳動也就不會那么大,我的鼠標是公司給配的電腦自帶的,作用只限于能用,所以齒輪刻度比較大。
而 wheel事件其實真正監(jiān)聽的是鼠標滾輪滾過一個齒輪卡點的事件,這也就能解釋為何會出現(xiàn)彈跳的現(xiàn)象了。
一般來說,鼠標滾輪每滾過一個齒輪卡點,就能監(jiān)聽到一個wheel事件,從開始到結束,被鼠標主動滾動的元素已經滾動了 100px,所以另外一個跟隨滾動的容器元素也就瞬間跳動了 100px
而之所以上述 scroll事件不會讓跟隨滾動元素出現(xiàn)瞬間彈跳,則是因為跟隨滾動元素每次 scrollTop發(fā)生變化時,其值不會有 100px那么大的跨度,可能也沒有小到1px,但由于其觸發(fā)頻率高,滾動跨度小,最起碼在視覺上就是平滑滾動的了。
如果你想讓右側滾動框也平滑滾動,也是可以做到的,當每次監(jiān)聽到 wheel事件的時候,也別管它相比于上次是差了100px還是50px的,始終都讓右側的跟隨滾動框按照 10px(或者再稍大點或者稍小點的跨度,只要給人視覺上的感受是平滑滾動并且延遲不是太大就行了)來滾動,連續(xù)滾動10次,那就是100px了,同樣能到達準確的位置,例如如下代碼:
這個其實很好解決,用鼠標拖動滾動條肯定是能觸發(fā) scroll事件的,而在這種情況下,你肯定能夠很輕易地判斷出這個被拖動的滾動條是屬于哪個容器元素的,只需要處理這個容器的滾動事件,另外一個跟隨滾動容器的滾動事件不做處理即可。
wheel事件是 DOM Level3的標準事件,但是除了此事件之外,還有很多非標準事件,不同的瀏覽器內核使用不同的標準,所以可能還需要按情況來進行兼容,具體可見 MDN MouseWheelEvent
如果你難以忍受 wheel的彈跳,以及各種兼容,那么其實還有另外的路可以走得通,依舊是 scroll事件,只不過需要做一些額外的工作。
scroll事件的問題在于,沒有判斷當前主動滾動的是哪一個容器元素,只要確定了主動滾動的容器元素,這事就好辦了,例如上述使用 wheel事件中,用鼠標拖動滾動條之所以能夠使用 scroll事件,就是因為能夠很容易地確定當前主動滾動容器元素是哪一個。
所以,問題的關鍵在于,如何判斷出當前主動滾動的容器元素,只要解決了這個問題,剩下的就很好辦了。
不論是鼠標滾輪滾動還是鼠標按在滾動條上拖動滾動條滾動,都會觸發(fā) scroll事件,并且這個時候,在坐標系 Z軸上,鼠標的坐標肯定是位于滾動容器元素所占的面積之內的,也就是說,在 Z軸上,鼠標肯定是懸浮或者位于滾動容器元素之上。
鼠標在屏幕上移動的時候,是可以獲取到鼠標當前坐標的。
其中,clientX和 clientY就是當前鼠標相對于視口的坐標,可以認為,只要這個坐標在某個滾動容器的范圍內,則認為這個容器元素就是主動滾動容器元素,容器元素的坐標范圍可以使用 getBoundingClientRect進行獲取。
這樣確實是可以的,不過考慮到兩個滾動容器元素幾乎占據(jù)了整個屏幕面積,所以 mousemove所要監(jiān)聽的面積未免有點大,對于性能可能要求較高,所以其實可以換成 mouseover事件,只需要監(jiān)聽鼠標有沒有進入到某個滾動容器元素即可,也省去上述的坐標判斷了。
當確定了鼠標主動滾動的容器元素是哪一個時,只需要處理這個容器的滾動事件,另外一個跟隨滾動容器的滾動事件不做處理即可。
嗯,效果很不錯,性能也很好,perfect,可以收工嘍~
那一屋!
事情沒有那么簡單!
上面全部是在兩個滾動容器元素的內容高度完全一致的情況下的效果,如果這兩個滾動容器元素的內容高度不同呢?
可見,由于兩個滾動容器元素的內容高度不同,所以最大的 scrollTop也就不同,就會出現(xiàn)當其中一個 scrollTop值較小的元素滾到底時,另外一個元素還停留在一半,或者當其中一個 scrollTop值較大的元素才滾到一半時,另外一個元素就已經滾到底了。
這種情況很常見,例如你用 markdown寫作時,一個一級標題標記 #在編輯模式下占用的高度,一般都是小于預覽模式占用的高度的,這樣就出現(xiàn)了左右兩側滾動高度不一致的情況。
所以,如果將這種情況也考慮進來的話,那么就不能簡單地為兩個滾動容器元素相互設置 scrollTop值那么簡單。
雖然無法固定住滾動容器內容的高度,但是有一點可以確定,滾動條最大滾動高度,或者說 scrollTop的值,肯定是與滾動容器內容的高度與滾動容器本身的高度呈一定的關系。
由于需要知道滾動容器內容的高度,還要存在滾動條,所以需要給此容器元素加個子元素,子元素高度不限,就是滾動容器內容的高度,容器高度固定,溢出滾動即可。
結構示例如下:
通過我的觀察推論與實踐驗證,已經確定下來了它們之間的關系,很簡單,就是最基本的加減法運算:
也就是說,如果已經確定了滾動容器內容的高度(即子元素高度ch)與滾動容器本身的高度(即容器元素高度ph),那么就一定能確定滾動條的最大滾動高度(scrollTop),而這兩個高度值基本上都是可以獲取到的,所以就能得到 scrollTop
因此,想要讓兩個滾動元素容器等比例上下滾動,即其中一個元素滾到頭或者滾到底,另外一個元素也能對應滾到頭和滾到底,那么只要得到這兩個滾動容器元素之間的 scrollTop最大值的比例(scale)就行了。
確定了 scale之后,實時滾動時,只需要獲取主動滾動容器元素的 scrollTop1,就能得到另外一個跟隨滾動的容器元素對應的 scrollTop2:
思路弄清晰了,寫代碼就是很容易的事情了。
上述基本上已經實現(xiàn)了需求,可能在實踐過程中還需要根據(jù)實際情況來進行一定的修改,例如如果你編寫一個 markdown的在線編輯和預覽頁面,就需要根據(jù)輸入內容的高度實時更新 scale值,不過主體已經搞定,小修小改就沒什么難度了。
另外,本文所述不僅是針對兩個滾動容器元素的跟隨滾動,同時也可擴展開來,更多的元素間的跟隨滾動都是可以根據(jù)本文思路來實現(xiàn)的,本文只是為了方便講解而具體到了兩個元素上。