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