前幾天美化博客時發現滾動條在window下實在太難看,因此在基於vue的技術上尋找美化滾動條的方法。記得Element-ui源碼中有名爲 el-scrollbar
的滾動組件,雖然文檔上沒有提到,但使用的人仍是很多。今天記錄下源碼的閱讀心得。html
在看苦澀的代碼前,先大概描述一下滾動條組件的用處和行爲,方便理解代碼邏輯。vue
由於操做系統和瀏覽器的不一樣,滾動條外觀是不同的。須要作風格統一時,就須要作自定義滾動條。固然也能夠直接修改CSS3中的 ::-webkit-scrollbar
相關屬性來達到修改原生滾動條外觀,但這個屬性部分瀏覽器上沒有可以完美兼容。node
在一個固定高度的元素中,如內部內容超出了父級元素的固定高。爲了讓用戶瀏覽其他的內容,一般都會設置父級元素overflow-y: scroll
出現滾動條。容許用戶以滾動的形式來瀏覽剩下的內容。web
而自定義滾動條,是先經過偏移視圖元素,達到隱藏原生滾動條的效果。同時在視圖元素的右側和下方,增長用標籤寫出的模擬滾動條。監聽模擬滾動條的事件(按下滑塊或點擊軌道),來動態更新視圖窗口的scrollTop
或scrollLeft
值。一樣的,也會監聽視圖窗口的事件(滾動事件或視圖窗口的尺寸縮放事件),來同步更新自定義滾動條的狀態(滑塊所處的位置或滑塊長度)。數組
滾動條實際上是當前已瀏覽內容的一個直觀展現,在固定元素中,若是scrollTop
發生改變往下滾動。滾動條中的滑塊也會向下移動。此時可以經過滾動條來得知內容的已滾動程度和剩餘程度。瀏覽器
咱們將頁面想象成一個很長的畫布,而咱們能看到的是一個移動的窗口。當頁面往下滾動時,窗口在畫布中也就往下移動,來查看被遮擋的內容。一樣的,滾動塊裏的滑塊也往下移動一樣比例的距離。因此滾動條就是一個等比例的縮小模型。閉包
也就是說,固定元素的高度clientHeight
除以 固定元素包括溢出的總高度scrollHeight
。同等於 滑塊的高度 除以 滾動條的高度。他們的比例是同樣的。app
在大概瞭解滾動條的工做內容和計算公式後,看看源碼中是如何處理他們之間的計算關係的。dom
scrollbar組件在 package/scrollbar/index.js
中被導出,其中 package/scrollbar/src
是代碼的核心部分,入口文件是 main.js
。函數
<el-scrollbar> <div style="height: 300px;"> <div style="height: 600px;"></div> </div> </el-scrollbar>
使用自定義標籤 el-scrollbar
裹住使用的區域,scrollbar 組件會生成
view 和 wrap 兩個父級元素包裹插槽中的內容,還有兩種類型的自定義滾動條 horizontal 和 vertical。
main.js默認導出一個對象,接收一系列配置。
name: 'ElScrollbar', components: { // 滾動條組件,擁有水平與垂直兩種形態 Bar }, props: { native: Boolean, // 是否使用原生滾動條,即不附加自定義滾動條 wrapStyle: {}, // wrap的內聯樣式 wrapClass: {}, // wrap的樣式名 viewClass: {}, // view的樣式名 viewStyle: {}, // view的內聯樣式 noresize: Boolean, // 當container尺寸發生變化時,自動更新滾動條組件的狀態 tag: { // 組件最外層的標籤屬性,默認爲 div type: String, default: 'div' } }, data() { return { sizeWidth: '0', // 水平滾動條的寬度 sizeHeight: '0', // 垂直滾動條的高度 moveX: 0, // 垂直滾動條的移動比例 moveY: 0 // 水平滾動條的移動比例 }; },
組件在render函數中生成結構。
tips:若是在.vue文件中同時存在 template
和 render
函數,組件實例會先取 template
模板來渲染組件模板,而不採用 render
函數
render函數一開始會經過 scrollbarWidth 方法來計算當前瀏覽器的滾動條寬度。
render(h) { // 獲取瀏覽器的滾動條寬度 let gutter = scrollbarWidth(); // wrap內聯樣式 let style = this.wrapStyle; ...
scrollbarWidth 方法在 scrollbar-width.js 中被默認導出。
import Vue from 'vue'; // 閉包變量,用於記錄滾動條寬度 let scrollBarWidth; export default function() { // 若是在服務端運行,返回 0 if (Vue.prototype.$isServer) return 0; // 如存在滾動條寬度,直接返回 if (scrollBarWidth !== undefined) return scrollBarWidth; // 建立outer標籤並隱藏 const outer = document.createElement('div'); outer.className = 'el-scrollbar__wrap'; outer.style.visibility = 'hidden'; outer.style.width = '100px'; outer.style.position = 'absolute'; outer.style.top = '-9999px'; document.body.appendChild(outer); // 記錄沒有滾動內容的寬度 const widthNoScroll = outer.offsetWidth; // 設置外層div滾動屬性 outer.style.overflow = 'scroll'; // 建立inner標籤,並追加到outer標籤中 const inner = document.createElement('div'); inner.style.width = '100%'; outer.appendChild(inner); // 此時outer已經能夠滾動,記錄下inner元素的寬度 const widthWithScroll = inner.offsetWidth; // 銷燬outer元素 outer.parentNode.removeChild(outer); // 滾動條寬度 = 沒有滾動條時的outer寬度 減去 有滾動條的outer中的inner寬度 scrollBarWidth = widthNoScroll - widthWithScroll; // 返回滾動條寬度 return scrollBarWidth; };
獲取滾動條方法會進行如下步驟
從而得出此時的瀏覽器滾動條寬度爲 100 - 83 = 17 像素
若是存在滾動條寬度,會將wrap設置偏移,達到隱藏原生滾動條的效果。
// 若是存在滾動條寬度 if (gutter) { // 設置偏移寬度,隱藏原生滾動條 const gutterWith = `-${gutter}px`; const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`; // 根據配置類型,生成樣式 /** * 如是對象數組屬性 Array<Object> [{"background": "red"}, {"color": "red"}] * 則會被轉爲對象 {background: "red", color: "red"} */ if (Array.isArray(this.wrapStyle)) { style = toObject(this.wrapStyle); style.marginRight = style.marginBottom = gutterWith; } // 如是字符串,直接拼接 else if (typeof this.wrapStyle === "string") { style += gutterStyle; } // 不然直接賦值 else { style = gutterStyle; } }
接着生成view結構,設置配置的樣式名和內聯樣式,插槽中的默認內容會放入view下,同時給view增長ref索引,用於後續的事件綁定。
// 生成view const view = h( // view的標籤類型 this.tag, // view的屬性 { class: ["el-scrollbar__view", this.viewClass], style: this.viewStyle, ref: "resize" }, // 接收的插槽內容 this.$slots.default );
接着生成wrap結構,設置配置的樣式名和內聯樣式,同時監聽滾動事件
// 生成wrap,並監聽滾動事件 const wrap = ( <div ref="wrap" style={style} onScroll={this.handleScroll} class={[ this.wrapClass, "el-scrollbar__wrap", gutter ? "" : "el-scrollbar__wrap--hidden-default" ]} > {[view]} </div> );
接着根據 native 配置,拼接組件的最終結構。
// 若是不使用原生滾動條,則添加自定義滾動條 if (!this.native) { /** * 使用自定義滾動條 * <div class="el-scrollbar__wrap"> * <div class="el-scrollbar__view"></div> * </div> * <bar> * <bar> */ nodes = [ wrap, <Bar move={this.moveX} size={this.sizeWidth} />, <Bar vertical move={this.moveY} size={this.sizeHeight} /> ]; } else { /** * 不然使用原生滾動條 * * <div class="el-scrollbar__wrap"> wrap並沒有監聽滾動事件 * <div class="el-scrollbar__view"></div> * </div> */ nodes = [ <div ref="wrap" class={[this.wrapClass, "el-scrollbar__wrap"]} style={style} > {[view]} </div> ]; } // 返回最終結構 return h("div", { class: "el-scrollbar" }, nodes); // render函數結束
在組件 mounted 和 beforeDestroy 時,根據配置進行事件監聽。
mounted() { // 如使用原生滾動條,返回 if (this.native) return; // 在下一更新循環結束執行更新方法 this.$nextTick(this.update); // 根據配置進行監聽內容窗口大小重置事件,執行更新方法 !this.noresize && addResizeListener(this.$refs.resize, this.update); }, beforeDestroy() { // 如使用原生滾動條,返回 if (this.native) return; // 根據配置移除監聽內容窗口大小重置事件的執行更新方法 !this.noresize && removeResizeListener(this.$refs.resize, this.update); }
addResizeListener 方法在 resize-event.js 中被導出,方法接收兩個參數。監聽的DOM節點和回調事件。
/** * 窗口縮放執行回調 */ function resizeHandler(entries) { // entry是ResizeObserver構造函數執行時傳入的參 for (let entry of entries) { // 取出以前聲明的回調函數數組 const listeners = entry.target.__resizeListeners__ || []; // 遍歷執行回調 if (listeners.length) { listeners.forEach(fn => { fn(); }); } } } /** * 添加尺寸改變時事件監聽 * @param {HTMLDivElement} element 元素 * @param {Function} fn 回調 */ const addResizeListener = function(element, fn) { if (!element.__resizeListeners__) { // 設置當前元素的事件回調數組 element.__resizeListeners__ = []; // 實例化Resize觀察者對象 element.__ro__ = new ResizeObserver(resizeHandler); // 開始觀察指定的目標元素,當元素尺寸改變時,會執行resizeHandler方法 element.__ro__.observe(element); window.ro = element.__ro__; } // 往回調數組中添加本次監聽事件 element.__resizeListeners__.push(fn); }; /** * 移除尺寸改變時事件監聽 * @param {HTMLDivElement} element 元素 * @param {Function} fn 回調 */ const removeResizeListener = function(element, fn) { if (!element || !element.__resizeListeners__) return; // 數組中移除 element.__resizeListeners__.splice( element.__resizeListeners__.indexOf(fn), 1 ); // 取消目標對象上全部對element的觀察 if (!element.__resizeListeners__.length) { element.__ro__.disconnect(); } };
這樣,main.js的實例化過程就結束了。接着咱們看wrap綁定的滾動回調handleScroll方法,和生命週期鉤子中見到的update方法。
在wrap窗口滾動時,會執行method中的handleScroll方法,更新data中的moveY和moveX屬性。
moveY和moveX會做爲配置屬性傳給Bar滾動條組件,實時更新Bar的 translateY(moveY%)
或 translateX(moveX%)
做爲滑塊的滾動位置。
handleScroll() { const wrap = this.wrap; this.moveY = (wrap.scrollTop * 100) / wrap.clientHeight; this.moveX = (wrap.scrollLeft * 100) / wrap.clientWidth; },
moveY和modeX的計算邏輯,一開始看着有點迷糊。
可是調轉一下計算順序,就恍然大悟了。
handleScroll() { const wrap = this.wrap; this.moveY = (wrap.scrollTop / wrap.clientHeight) * 100; this.moveX = (wrap.scrollLeft / wrap.clientWidth) * 100; },
這裏是在求滾動高度與可見高度的比例。
上面咱們已經知道,固定元素的高度clientHeight
除以 固定元素包括溢出的總高度scrollHeight
。同等於 滑塊的高度 除以 滾動條的高度。
因此當scrollTop
發生改變時,咱們可以計算出比例關係來更新滑塊的正確位置。
假設咱們的wrap高度爲300px,當前的滾動高 scrollTop
爲0,滾動塊的位置是貼緊頂部的,此時Bar組件的 translateY
是 0%。
注意,圖中右邊的滾動條和左側的視圖內容,並不真正同高。僅僅是比例尺關係。
當向下滾動時,scrollTop
恰好爲300px(一個Wrap的高度),側邊的滾動塊也應該往下移動恰好一個身位。也就是滾動塊的自身的高度。
當wrap區域往下滾動恰好一整個wrap的高度時,側邊的滾動塊也會往下移動一整個滾動塊的長度。此時Bar組件的 translateY
應該是 100%。
計算公式成立:scrollTop
(300px)/ scrollHeight
(300px)* 100 = 100。
這裏乘100是由於Bar組件中 translateY
是以百分比爲單位設置屬性。
繼續滾動到底部時,此時的scrollTop
已經爲550px,根據公式計算,550 / 300 * 100 滾動塊的位置爲 translateY(183.333%)。約要偏移1.8個滾動塊自身的長度,Bar才能反映出wrap中container的當前展現位置。
update 方法負責更新 Bar 的滑塊長度,在 mounted 生命週期鉤子中,會根據 noresize
配置對view模板進行選擇性監聽窗口大小改變事件,當內容窗口大小發生改變時,會執行 update 方法。
update() { let heightPercentage, widthPercentage; const wrap = this.wrap; if (!wrap) return; heightPercentage = (wrap.clientHeight * 100) / wrap.scrollHeight; widthPercentage = (wrap.clientWidth * 100) / wrap.scrollWidth; this.sizeHeight = heightPercentage < 100 ? heightPercentage + "%" : ""; this.sizeWidth = widthPercentage < 100 ? widthPercentage + "%" : ""; }
update方法中,會計算出滾動塊的百分比高度,而後賦值給sizeHeight或sizeWidth。更新Bar的滾動塊寬度或高度。
heightPercentage是由 可見區域高度 / 總滾動高度,計算出的佔比。和滑塊在滾動條軌道中的佔比是同樣的。
this.sizeHeight = heightPercentage < 100 ? heightPercentage + "%" : "";
在計算sizeHeight時作了大於100判斷,當尺寸改變後的內容大於滾動高度,說明就不須要滾動塊了。
至此,main.js 中的全部邏輯都已通過完了。簡單總結一下 main.js 所作的事情。
而後來到Bar.js,在點擊滑塊和軌道時,如何處理視圖窗口的更新。
Bar組件接收三個屬性vertical,size,move,並在計算屬性中添加了當前滾動塊類型的屬性集合bar
,與父組件的wrap
索引。
export default { name: 'Bar', props: { // 是否垂直滾動條 vertical: Boolean, // size 對應的是 水平滾動條的 width 或 垂直滾動條的height size: String, // move 用於 translateX 或 translateY 屬性中 move: Number }, computed: { /** * 從BAR_MAP中返回一個的新對象,垂直滾動條屬性集合 或 水平滾動條屬性集合 */ bar() { return BAR_MAP[this.vertical ? 'vertical' : 'horizontal']; }, // 父組件的wrap,用於鼠標拖動滑塊後更新 wrap 的 scrollTop 值 wrap() { return this.$parent.wrap; } }, ... }
bar
會返回當前滾動條類型的滾動條屬性集合,並在後續的操做中取對應的值做爲更新。
const BAR_MAP = { // 垂直滾動塊的屬性 vertical: { offset: 'offsetHeight', scroll: 'scrollTop', scrollSize: 'scrollHeight', size: 'height', key: 'vertical', axis: 'Y', client: 'clientY', direction: 'top' }, // 水平滾動塊的屬性 horizontal: { offset: 'offsetWidth', scroll: 'scrollLeft', scrollSize: 'scrollWidth', size: 'width', key: 'horizontal', axis: 'X', client: 'clientX', direction: 'left' } };
在render函數中,會對軌道區域和滑塊進行鼠標按下事件進行監聽,並對滑塊進行內聯樣式綁定,在 size, move, bar 等屬性發生改變時,動態的改變滑塊的位置或長度。
render(h) { // size: 'width' || 'height' // move: 滾動塊的位置,單位爲百分比 // bar: 垂直滾動條屬性集合 或 水平滾動條屬性集合 const { size, move, bar } = this; return ( <div class={['el-scrollbar__bar', 'is-' + bar.key]} // 滾動條區域監聽 鼠標按下事件 onMousedown={this.clickTrackHandler} > <div ref="thumb" class="el-scrollbar__thumb" // 滾動塊監聽 鼠標按下事件 onMousedown={this.clickThumbHandler} style={renderThumbStyle({ size, move, bar })}> </div> </div> ); }
咱們以垂直類型的Bar組件爲例,首先看綁定在軌道區域的鼠標點擊事件回調 clickTrackHandler 方法。
在點擊軌道區域時,滑塊會快速定位到該位置,而且更新視圖的scrollTop
。這就是 clickTrackHandler 處理的事情。
// 對按下 滾動條區域 的某一個位置進行快速定位 clickTrackHandler(e) { /** * getBoundingClientRect() 方法返回元素的大小及其相對於瀏覽器頁面的位置。 * this.bar.direction = "top" * this.bar.client = "clientY" * e.clientY 是事件觸發時,鼠標指針相對於瀏覽器窗口頂部的距離。 */ // 偏移量 絕對值 (當前元素距離瀏覽器窗口的 頂部/左側 距離 減去 當前點擊的位置距離瀏覽器窗口的 頂部/左側 距離) const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]); // 滑動塊一半高度 const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2); // 計算點擊後,根據 偏移量 計算在 滾動條區域的總高度 中的佔比,也就是 滾動塊 所處的位置 const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]); // 設置外殼的 scrollHeight 或 scrollWidth 新值。達到滾動內容的效果 this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100); },
方法中比較多的公式計算,一時之間比較難理解。下圖是各變量的圖示,接着咱們一個一個拆解。
在方法中,第一步會計算滑塊的偏移量(offset)。代碼中的偏移量計算公式是:
點擊元素距離瀏覽器窗口頂部的距離 減去 鼠標點擊位置距離瀏覽器窗口頂部的距離,再求結果的絕對值。
點擊元素 實則就是軌道區域,其實公式能夠換成這樣看,會更加容易理解。
鼠標點擊位置距離瀏覽器窗口頂部的距離 減去 滾動條區域距離瀏覽器窗口頂部的距離
由於根據scrollBar組件的使用位置不一樣(有的包裹整個頁面窗口,有的包裹一小塊菜單區域),滾動條區域也不必定徹底貼緊瀏覽器窗口的頂部。因此這邊須要用 鼠標點擊位置距離瀏覽器窗口頂部的距離e[this.bar.client]
將 滾動條區域距離瀏覽器窗口頂部的距離e.target.getBoundingClientRect()[this.bar.direction]
減去,才能得出準確的 偏移量offset
。
/** * getBoundingClientRect() 方法返回元素的大小及其相對於瀏覽器頁面的位置。 * this.bar.direction = "top" * this.bar.client = "clientY" * e.clientY 是事件觸發時,鼠標指針相對於瀏覽器窗口頂部的距離。 */ // 偏移量 絕對值 (當前元素距離瀏覽器窗口的 垂直/水平 座標 減去 當前點擊的位置距離瀏覽器窗口的 垂直/水平 座標) const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]);
接下來計算的是滑動塊一半的高度,用於後續邏輯處理。
// 滑動塊一半高度 const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2);
根據瀏覽器滾動條操做行爲,通常咱們點擊軌道某個點時,滑塊的中心總會在咱們的落點位置。
在用偏移量 offset
減去滾動塊的一半高度 thumbHalf
後得出 滑塊總移動的長度。再用 滑塊總移動的長度 除 滾動區域的總高度,得出 滾動比例thumbPositionPercentage
。
得出 滾動比例 後,由於滾動條和視圖是一個縮放的比例尺關係。此時用 滾動比例 乘 wrap的 scrollHeight 得出滾動距離,再對 wrap 的 scrollTop 進行賦值,視圖便滾動到須要更新展現的內容中。
// 計算點擊後,根據 偏移量 計算在 滾動條區域的總高度 中的佔比,也就是 滾動塊 所處的位置 const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]); // 設置外殼的 scrollTop 或 scrollLeft 新值。達到滾動內容的效果 this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
接下來是滑塊監聽的鼠標按下事件,clickThumbHandler。
clickThumbHandler 方法會在鼠標按下滑塊時,監聽鼠標移動事件和鼠標按鍵釋放事件,更新滑塊位置的同時,也更新視圖窗口的滾動位置。
// 按下滑動塊 clickThumbHandler(e) { /** * 防止右鍵單擊滑動塊 * e.ctrlKey: 檢測事件發生時Ctrl鍵是否被按住了 * e.button: 指示當事件被觸發時哪一個鼠標按鍵被點擊 0,鼠標左鍵;1,鼠標中鍵;2,鼠標右鍵 */ if (e.ctrlKey || e.button === 2) { return; } // 開始記錄拖拽 this.startDrag(e); // 記錄點擊滑塊時的位置距滾動塊底部的距離 this[this.bar.axis] = ( // 滑塊的高度 e.currentTarget[this.bar.offset] - // 點擊滑塊距離頂部的位置 減去 滑塊元素距離頂部的位置 (e[this.bar.client] - e.currentTarget.getBoundingClientRect()[this.bar.direction]) ); },
開始先判斷是否鼠標右鍵觸發的事件,如真返回。接着執行 startDrag 方法。
最後會計算點擊滑塊時的位置距滾動塊底部的距離。而後賦值給this[this.bar.axis]
,由於當前滾動條類型是垂直滾動條,因此this.bar.axis
從計算屬性中獲取爲字符串 Y
,this['Y']
會用於後續的計算。this['Y']
的計算公式爲:滑塊的高度 減去 (點擊滑塊的位置距離頁面窗口頂部的距離 clientY
減去 滑塊元素距離頁面窗口頂部的距離Rect.top
)
this.bar.axis
從計算屬性中獲取,返回的是字符串,X 或 Y。但在Bar組件的 data 中,並無對 this['X']
或 this['Y']
這兩個屬性進行聲明。
緣由是由於Bar組件有兩種類型,垂直或水平。因此做者沒有選擇一開始就聲明,而是經過後續的操做再動態掛上 X 或 Y 屬性
須要注意的是,這樣動態添加的屬性,並非一個響應式的屬性。即未被vue進行getter/setter
重寫,在數據發生改變後視圖是不會同步更新的。
可是這裏僅僅用於數據層面上的使用,並不在視圖上使用。問題不大。
具體能夠查閱文檔, 深刻響應式原理
)
在startDrag方法中,會記錄按下狀態,並監聽鼠標移動和鼠標按鈕鬆開事件。
// 開始拖拽 startDrag(e) { // 中止後續的相同事件函數執行 e.stopImmediatePropagation(); // 記錄按下狀態 this.cursorDown = true; // 監聽鼠標移動事件 on(document, 'mousemove', this.mouseMoveDocumentHandler); // 監聽鼠標按鍵鬆開事件 on(document, 'mouseup', this.mouseUpDocumentHandler); // 拖拽滾動塊時,此時禁止鼠標長按劃過文本選中。 document.onselectstart = () => false; },
on方法和off方法在 utils/dom
中被導出,在導出時會對環境進行兼容處理,導出對應的事件監聽處理函數。
/* istanbul ignore next */ export const on = (function() { // 查詢實例是否在服務端運行,與是否支持 addEventListener,返回對應處理監聽函數 if (!isServer && document.addEventListener) { return function(element, event, handler) { if (element && event && handler) { // 適用於現代瀏覽器的監聽事件 addEventListener element.addEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event && handler) { // 用於 ie 部分版本瀏覽器的監聽事件 attachEvent element.attachEvent('on' + event, handler); } }; } })(); /* istanbul ignore next */ export const off = (function() { // 查詢實例是否在服務端運行,與是否支持 removeEventListener,返回對應處理監聽函數 if (!isServer && document.removeEventListener) { return function(element, event, handler) { if (element && event) { // 適用於現代瀏覽器的移除事件監聽 removeEventListener element.removeEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event) { // 用於 ie 部分版本瀏覽器的移除事件監聽 detachEvent element.detachEvent('on' + event, handler); } }; } })();
在鼠標移動時,會執行mouseMoveDocumentHandler事件。
方法進入會判斷cursorDown
和this.['Y']
是否存在,若是爲假。說明方法並非正常操做觸發,結束返回。
在鼠標的不斷移動中,計算按住滑塊移動時的位置距離軌道頂部的實際距離offset
,同時用以前記錄下來的this['Y']
計算出按下滑塊時距離滑塊頂部的距離thumbClickPosition
。
此時offset
減去thumbClickPosition
,就是滑塊在軌道中實際移動的距離。再用此值除以軌道長度。即是滾動比例thumbPositionPercentage
。
最後用thumbPositionPercentage
乘視圖窗口的滾動高度,即是視圖窗口須要更新滾動的距離。
// 按下滾動條,而且鼠標移動時 mouseMoveDocumentHandler(e) { // 若是按下狀態爲假,返回 if (this.cursorDown === false) return; // 點擊位置時距滾動塊底部的距離 const prevPage = this[this.bar.axis]; if (!prevPage) return; // (滑塊距離頁面頂部的距離 減 鼠標移動時距離頂部的距離) * -1 const offset = ((this.$el.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]) * -1); // 按下滑塊位置距離滑塊頂部的距離 const thumbClickPosition = (this.$refs.thumb[this.bar.offset] - prevPage); // 滑動距離在滾動軌道長度的佔比 const thumbPositionPercentage = ((offset - thumbClickPosition) * 100 / this.$el[this.bar.offset]); // 根據比例,更新視圖窗口的滾動距離 this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100); },
在鼠標鬆開時,重置各記錄的狀態,並取消監聽的鼠標移動事件。
// 按下滾動條,而且鼠標鬆開 mouseUpDocumentHandler(e) { // 重置按下狀態 this.cursorDown = false; // 重置當前點擊在滾動塊的位置 this[this.bar.axis] = 0; // 移除監聽鼠標移動事件 off(document, 'mousemove', this.mouseMoveDocumentHandler); // 拖拽結束,此時容許鼠標長按劃過文本選中。 document.onselectstart = null; }
源碼到這裏已經所有解讀結束,因我的水平有限,不免會有不許確或者存在歧義的地方,但願可以不吝賜教,共同交流進步。
祝你有個愉快的勞動節假期。:)
Have a nice day.
)