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