在一些(xie)支持用 markdown寫(xie)(xie)文章的(de)網站,例如 掘金 或(huo)者 CSDN等,后(hou)臺寫(xie)(xie)作(zuo)頁面,一般都是支持 markdown即(ji)時預覽(lan)的(de),也就是將(jiang)整個(ge)頁面分(fen)(fen)成(cheng)兩部(bu)分(fen)(fen),左半部(bu)分(fen)(fen)是你輸入的(de) markdown文字,右半部(bu)分(fen)(fen)則即(ji)時輸出(chu)對應的(de)預覽(lan)頁面。
本文(wen)不是闡述如何從(cong) 0實現(xian)這種效果的(后(hou)續 很可能 會單(dan)出文(wen)章,),拋開其他,單(dan)看頁面主體(ti)中左右兩個(ge)容器(qi)元(yuan)(yuan)素(su),即 markdown輸入(ru)框元(yuan)(yuan)素(su)和預覽顯示(shi)框元(yuan)(yuan)素(su)
本文(wen)要探(tan)討的是(shi),當這兩(liang)個容器(qi)元素(su)(su)的內容都(dou)超出了容器(qi)高度,即都(dou)出現(xian)了滾(gun)動(dong)框的時候,如何在其(qi)中一(yi)個容器(qi)元素(su)(su)滾(gun)動(dong)時,讓另外一(yi)個元素(su)(su)也隨之滾(gun)動(dong)。
既然是與滾(gun)動(dong)條(tiao)有關(guan),那么首先(xian)想到 js中控制(zhi)(zhi)滾(gun)動(dong)條(tiao)高度的(de)一個(ge)屬性: scrollTop,只要能控制(zhi)(zhi)這個(ge)屬性的(de)值,自(zi)然也就能控制(zhi)(zhi)滾(gun)動(dong)條(tiao)的(de)滾(gun)動(dong)了。
對于以下 DOM結構:
其中,.left元素(su)(su)是(shi)左半部分輸入框(kuang)容(rong)器(qi)元素(su)(su),.right元素(su)(su)是(shi)右(you)半部分顯示(shi)框(kuang)容(rong)器(qi)元素(su)(su),.container是(shi)它(ta)們共同的(de)父(fu)元素(su)(su)。
由于(yu)需要溢出(chu)滾動,所(suo)以還需要設置一下對應的樣式(只是關鍵樣式,非全(quan)部):
再向 .left和(he) .right元素中(zhong)塞(sai)入(ru)足夠的內容,讓二者出現(xian)滾動(dong)條(tiao),
樣式是出來個大概了,下面就可以在這些 DOM上進行一系列的操(cao)作了。
大致(zhi)思(si)路,監聽兩個容(rong)器(qi)元素(su)的(de)(de)滾(gun)動(dong)事件,在其中(zhong)一(yi)個元素(su)滾(gun)動(dong)的(de)(de)時(shi)(shi)候,獲取這(zhe)個元素(su)的(de)(de) scrollTop屬性(xing)的(de)(de)值(zhi),同時(shi)(shi)將(jiang)此值(zhi)設置為另外一(yi)個滾(gun)動(dong)元素(su)的(de)(de) scrollTop值(zhi)即可。
似乎很不錯,但是現在是不僅想讓右邊跟隨左邊滾動(dong),還想左邊跟隨右邊滾動(dong),于是再加以(yi)下代(dai)碼:
看上去很(hen)不錯(cuo),然而(er),哪有那(nei)么簡單的(de)事情(qing)。
這個時候你再用鼠標(biao)滾(gun)輪進行滾(gun)動(dong)的(de)時候,卻發現(xian)滾(gun)動(dong)得有(you)點吃力,兩個容器元素的(de)滾(gun)動(dong)似(si)乎被什么阻(zu)礙住了(le),很(hen)難滾(gun)動(dong)。
仔細分析,原因很簡單,當你在左(zuo)邊(bian)滾(gun)動的時候,觸(chu)(chu)發(fa)了左(zuo)邊(bian)的滾(gun)動事件,于(yu)(yu)是(shi)右(you)邊(bian)跟(gen)隨(sui)(sui)滾(gun)動,但是(shi)與此(ci)同(tong)時右(you)邊(bian)的跟(gen)隨(sui)(sui)滾(gun)動也是(shi)滾(gun)動,于(yu)(yu)是(shi)也觸(chu)(chu)發(fa)了右(you)邊(bian)的滾(gun)動,于(yu)(yu)是(shi)左(zuo)邊(bian)也要跟(gen)隨(sui)(sui)右(you)邊(bian)滾(gun)動…然后就進入了一個類似于(yu)(yu)相互觸(chu)(chu)發(fa)的情況(kuang),所以(yi)就會發(fa)現(xian)滾(gun)動得很吃力(li)。
想要解(jie)決(jue)上(shang)述問題,暫時有以(yi)下兩種方案。
由于 scroll事(shi)(shi)件(jian)不僅(jin)會(hui)被鼠標(biao)主動(dong)(dong)(dong)(dong)滾動(dong)(dong)(dong)(dong)觸(chu)發,同時改變容器元素的 scrollTop也(ye)會(hui)觸(chu)發,元素的主動(dong)(dong)(dong)(dong)滾動(dong)(dong)(dong)(dong)其實就是(shi)鼠標(biao)滾輪觸(chu)發的,所(suo)以(yi)可以(yi)將scroll事(shi)(shi)件(jian)換(huan)成(cheng)一個對(dui)鼠標(biao)滾動(dong)(dong)(dong)(dong)敏感(gan)而不是(shi)元素滾動(dong)(dong)(dong)(dong)敏感(gan)的事(shi)(shi)件(jian):’mousewheel’。
似(si)乎(hu)是有(you)點用,但(dan)是實際上還有(you)兩個問題。
在網上找了一圈,沒有(you)找到關(guan)于(yu) wheel事(shi)件滾動頻率(lv)相關(guan)內容,我推測這可(ke)能就(jiu)是此事(shi)件的一個 feature
鼠(shu)標(biao)(biao)(biao)每次滾動基本上(shang)都并不是(shi)以 1px為(wei)單位(wei)的,其最(zui)小單元遠比 scroll事件(jian)小的多,我(wo)用我(wo)的鼠(shu)標(biao)(biao)(biao)在 chrome瀏覽(lan)器上(shang)滾動,每次滾過(guo)的距(ju)離都恰好是(shi) 100px,不同的鼠(shu)標(biao)(biao)(biao)或者瀏覽(lan)器這(zhe)個數值應該都是(shi)不一樣的。
如果(guo)你的鼠標(biao)質量比(bi)較(jiao)(jiao)好,齒輪比(bi)較(jiao)(jiao)精(jing)細(xi),那么應該就(jiu)會小于 100px, 跳動(dong)也就(jiu)不會那么大,我的鼠標(biao)是(shi)公司給配的電(dian)腦自帶的,作用只限于能(neng)用,所以齒輪刻(ke)度比(bi)較(jiao)(jiao)大。
而 wheel事件其實(shi)真正監聽的(de)是(shi)鼠標滾輪(lun)滾過(guo)一個齒輪(lun)卡點的(de)事件,這也就能解(jie)釋為何會出現彈跳的(de)現象了(le)。
一般來說(shuo),鼠(shu)標(biao)滾(gun)(gun)輪每滾(gun)(gun)過一個齒輪卡點(dian),就(jiu)能監聽到一個wheel事件(jian),從開始(shi)到結束(shu),被鼠(shu)標(biao)主動滾(gun)(gun)動的(de)元素(su)已(yi)經(jing)滾(gun)(gun)動了 100px,所(suo)以另外(wai)一個跟隨滾(gun)(gun)動的(de)容(rong)器元素(su)也就(jiu)瞬間跳動了 100px
而之(zhi)所以上述 scroll事件不會讓跟隨滾動(dong)元素(su)出(chu)現瞬間彈跳(tiao),則是(shi)因為(wei)跟隨滾動(dong)元素(su)每次 scrollTop發(fa)生變化時,其值不會有 100px那么大的跨度,可(ke)能也沒有小(xiao)到(dao)1px,但由于其觸發(fa)頻率高,滾動(dong)跨度小(xiao),最(zui)起碼在視覺上就是(shi)平(ping)滑滾動(dong)的了。
如(ru)果你(ni)想(xiang)讓右(you)側滾(gun)動框(kuang)也平滑滾(gun)動,也是可以做到的,當每次監聽到 wheel事件(jian)的時(shi)候,也別(bie)管它相比于上次是差了(le)100px還是50px的,始終都讓右(you)側的跟隨(sui)滾(gun)動框(kuang)按照 10px(或(huo)者再稍(shao)大點(dian)或(huo)者稍(shao)小點(dian)的跨度,只要給人視覺上的感受是平滑滾(gun)動并且延遲(chi)不是太大就行了(le))來滾(gun)動,連續滾(gun)動10次,那就是100px了(le),同樣能(neng)到達準確的位置(zhi),例如(ru)如(ru)下(xia)代碼:
這個其(qi)實很好解決,用鼠標拖動(dong)(dong)滾(gun)(gun)動(dong)(dong)條肯(ken)定是能(neng)(neng)觸發 scroll事(shi)件(jian)的(de)(de),而(er)在這種情(qing)況下,你肯(ken)定能(neng)(neng)夠很輕易(yi)地判斷出這個被拖動(dong)(dong)的(de)(de)滾(gun)(gun)動(dong)(dong)條是屬于哪個容(rong)器(qi)元素的(de)(de),只(zhi)需要(yao)處理(li)這個容(rong)器(qi)的(de)(de)滾(gun)(gun)動(dong)(dong)事(shi)件(jian),另外一個跟隨滾(gun)(gun)動(dong)(dong)容(rong)器(qi)的(de)(de)滾(gun)(gun)動(dong)(dong)事(shi)件(jian)不做(zuo)處理(li)即(ji)可。
wheel事件是 DOM Level3的標準(zhun)事件,但是除了此事件之外,還(huan)有很多(duo)非標準(zhun)事件,不同的瀏覽(lan)器(qi)內(nei)核(he)使(shi)用不同的標準(zhun),所以可能還(huan)需要按情況(kuang)來(lai)進(jin)行兼(jian)容,具體可見 MDN MouseWheelEvent
如果你難以忍受(shou) wheel的彈跳(tiao),以及(ji)各種(zhong)兼容,那么其實還有另外的路可以走得(de)通(tong),依舊是(shi) scroll事件,只不(bu)過需要(yao)做一些額外的工作。
scroll事件(jian)的(de)問題(ti)在于,沒(mei)有判斷(duan)當前主動(dong)(dong)(dong)(dong)(dong)滾動(dong)(dong)(dong)(dong)(dong)的(de)是(shi)哪(na)一(yi)個容(rong)器(qi)元(yuan)素,只要確定了(le)主動(dong)(dong)(dong)(dong)(dong)滾動(dong)(dong)(dong)(dong)(dong)的(de)容(rong)器(qi)元(yuan)素,這事就好辦了(le),例如上述(shu)使(shi)用(yong) wheel事件(jian)中,用(yong)鼠(shu)標(biao)拖動(dong)(dong)(dong)(dong)(dong)滾動(dong)(dong)(dong)(dong)(dong)條之所以能夠(gou)(gou)使(shi)用(yong) scroll事件(jian),就是(shi)因為能夠(gou)(gou)很容(rong)易地確定當前主動(dong)(dong)(dong)(dong)(dong)滾動(dong)(dong)(dong)(dong)(dong)容(rong)器(qi)元(yuan)素是(shi)哪(na)一(yi)個。
所以,問(wen)題(ti)的(de)關鍵(jian)在于(yu),如何(he)判斷出當前(qian)主動滾動的(de)容器元素,只要解決了這(zhe)個問(wen)題(ti),剩(sheng)下的(de)就很好辦了。
不(bu)論是鼠標(biao)滾(gun)(gun)(gun)輪(lun)滾(gun)(gun)(gun)動(dong)(dong)還(huan)是鼠標(biao)按(an)在滾(gun)(gun)(gun)動(dong)(dong)條上(shang)(shang)(shang)拖動(dong)(dong)滾(gun)(gun)(gun)動(dong)(dong)條滾(gun)(gun)(gun)動(dong)(dong),都會觸發 scroll事件,并且這個時候,在坐(zuo)標(biao)系 Z軸上(shang)(shang)(shang),鼠標(biao)的(de)坐(zuo)標(biao)肯定是位于滾(gun)(gun)(gun)動(dong)(dong)容(rong)器元素所(suo)占的(de)面積之內的(de),也(ye)就是說,在 Z軸上(shang)(shang)(shang),鼠標(biao)肯定是懸浮或(huo)者位于滾(gun)(gun)(gun)動(dong)(dong)容(rong)器元素之上(shang)(shang)(shang)。
鼠標在(zai)屏幕上移動的時(shi)候,是可以獲(huo)取(qu)到鼠標當前坐標的。
其中,clientX和 clientY就是當前鼠(shu)標相對于視口的(de)坐標,可以(yi)認(ren)為,只要(yao)這個(ge)坐標在某個(ge)滾動(dong)容(rong)器的(de)范圍內,則認(ren)為這個(ge)容(rong)器元(yuan)素就是主動(dong)滾動(dong)容(rong)器元(yuan)素,容(rong)器元(yuan)素的(de)坐標范圍可以(yi)使用 getBoundingClientRect進行(xing)獲(huo)取(qu)。
這樣確實是可以的,不過考慮(lv)到(dao)兩(liang)個(ge)滾動(dong)容器元(yuan)素(su)(su)幾乎占據了整個(ge)屏幕面積,所以 mousemove所要監(jian)聽的面積未免有(you)點大,對于性能可能要求較高,所以其實可以換(huan)成 mouseover事(shi)件,只需要監(jian)聽鼠標有(you)沒有(you)進入到(dao)某個(ge)滾動(dong)容器元(yuan)素(su)(su)即可,也省去上述(shu)的坐標判斷了。
當確定了鼠標主動(dong)滾(gun)動(dong)的容器元素是(shi)哪一個時,只需(xu)要處(chu)理(li)這個容器的滾(gun)動(dong)事件(jian),另(ling)外一個跟隨滾(gun)動(dong)容器的滾(gun)動(dong)事件(jian)不做處(chu)理(li)即(ji)可。
嗯,效果很不錯(cuo),性能(neng)也很好(hao),perfect,可以(yi)收工嘍(lou)~
那(nei)一屋!
事情沒有那么(me)簡單!
上(shang)面(mian)全(quan)部是在兩個滾動(dong)容器元素(su)的(de)內容高度完全(quan)一(yi)致的(de)情(qing)況下的(de)效(xiao)果(guo),如果(guo)這兩個滾動(dong)容器元素(su)的(de)內容高度不同(tong)呢?
可見,由(you)于兩個(ge)(ge)(ge)滾(gun)(gun)動容(rong)器元(yuan)素(su)的(de)(de)(de)內容(rong)高度不同(tong)(tong),所以(yi)最(zui)大的(de)(de)(de) scrollTop也就(jiu)(jiu)不同(tong)(tong),就(jiu)(jiu)會出現當其中一個(ge)(ge)(ge) scrollTop值較(jiao)小(xiao)的(de)(de)(de)元(yuan)素(su)滾(gun)(gun)到(dao)底時(shi),另(ling)外一個(ge)(ge)(ge)元(yuan)素(su)還(huan)停(ting)留在一半,或者當其中一個(ge)(ge)(ge) scrollTop值較(jiao)大的(de)(de)(de)元(yuan)素(su)才滾(gun)(gun)到(dao)一半時(shi),另(ling)外一個(ge)(ge)(ge)元(yuan)素(su)就(jiu)(jiu)已經(jing)滾(gun)(gun)到(dao)底了。
這(zhe)種情況(kuang)很常見,例(li)如你用 markdown寫作時(shi),一(yi)個一(yi)級標(biao)題(ti)標(biao)記(ji) #在編輯模(mo)式下(xia)占用的高(gao)度(du),一(yi)般都是小于預覽模(mo)式占用的高(gao)度(du)的,這(zhe)樣(yang)就(jiu)出現了左右兩側滾動高(gao)度(du)不一(yi)致的情況(kuang)。
所以,如果將(jiang)這(zhe)種情(qing)況也考慮進來的話,那(nei)么就不能簡單地為兩(liang)個滾(gun)動容器元素(su)相互(hu)設置 scrollTop值那(nei)么簡單。
雖然無法固(gu)定(ding)住滾(gun)動容器(qi)內容的高(gao)度,但是(shi)有一(yi)點可以確定(ding),滾(gun)動條最(zui)大滾(gun)動高(gao)度,或(huo)者說 scrollTop的值,肯定(ding)是(shi)與滾(gun)動容器(qi)內容的高(gao)度與滾(gun)動容器(qi)本身的高(gao)度呈一(yi)定(ding)的關(guan)系(xi)。
由(you)于需要知道(dao)滾動容(rong)(rong)(rong)器內容(rong)(rong)(rong)的高(gao)(gao)(gao)度(du),還要存在滾動條,所(suo)以需要給此容(rong)(rong)(rong)器元(yuan)素加(jia)個子元(yuan)素,子元(yuan)素高(gao)(gao)(gao)度(du)不限,就是滾動容(rong)(rong)(rong)器內容(rong)(rong)(rong)的高(gao)(gao)(gao)度(du),容(rong)(rong)(rong)器高(gao)(gao)(gao)度(du)固定,溢出滾動即可。
結構示例如(ru)下:
通過我的(de)觀察推(tui)論與(yu)實(shi)踐(jian)驗證,已經確定下來了它們之間的(de)關(guan)系,很簡單,就是最基本的(de)加減法運(yun)算:
也(ye)就是說,如果已(yi)經確定了滾動容(rong)器內容(rong)的高(gao)(gao)度(即子(zi)元(yuan)素(su)高(gao)(gao)度ch)與滾動容(rong)器本身的高(gao)(gao)度(即容(rong)器元(yuan)素(su)高(gao)(gao)度ph),那么(me)就一定能確定滾動條的最大滾動高(gao)(gao)度(scrollTop),而這(zhe)兩個(ge)高(gao)(gao)度值基本上都是可以(yi)獲取到(dao)的,所以(yi)就能得到(dao) scrollTop
因此,想要讓兩個(ge)(ge)滾(gun)動元(yuan)(yuan)素(su)容器(qi)等比例(li)上下滾(gun)動,即其(qi)中一個(ge)(ge)元(yuan)(yuan)素(su)滾(gun)到(dao)(dao)頭或(huo)者滾(gun)到(dao)(dao)底,另外一個(ge)(ge)元(yuan)(yuan)素(su)也(ye)能對應滾(gun)到(dao)(dao)頭和(he)滾(gun)到(dao)(dao)底,那么只要得到(dao)(dao)這兩個(ge)(ge)滾(gun)動容器(qi)元(yuan)(yuan)素(su)之間的 scrollTop最大值的比例(li)(scale)就(jiu)行了。
確定了 scale之后,實時(shi)滾動(dong)時(shi),只需(xu)要獲取主動(dong)滾動(dong)容器元素的 scrollTop1,就能得(de)到另外(wai)一個跟隨滾動(dong)的容器元素對應(ying)的 scrollTop2:
思路弄清(qing)晰了(le)(le),寫(xie)代碼就是很容易的事(shi)情了(le)(le)。
上述(shu)基本上已經實現了需(xu)求,可能在(zai)實踐過程(cheng)中還需(xu)要(yao)根(gen)據實際(ji)情(qing)況來進行一定的(de)修(xiu)改(gai),例如如果你編寫(xie)一個 markdown的(de)在(zai)線編輯和(he)預(yu)覽頁面,就需(xu)要(yao)根(gen)據輸入內容的(de)高度(du)實時更新 scale值(zhi),不過主體已經搞定,小修(xiu)小改(gai)就沒什么難(nan)度(du)了。
另外(wai),本文(wen)所述不僅是針對兩個滾(gun)動容(rong)器元素(su)(su)(su)的(de)跟隨滾(gun)動,同時也可擴展開來(lai)(lai),更多的(de)元素(su)(su)(su)間(jian)的(de)跟隨滾(gun)動都是可以根據本文(wen)思路來(lai)(lai)實現的(de),本文(wen)只(zhi)是為(wei)了方便講解而具體到了兩個元素(su)(su)(su)上。