前幾天美化博客時發現滾動條在window下實在太難看,因此在基於vue的技術上尋找美化滾動條的方法。記得Element-ui源碼中有名爲 el-scrollbar
的滾動組件,雖然文檔上沒有提到,但使用的人仍是很多。今天記錄下源碼的閱讀心得。html
在看苦澀的代碼前,先大概描述一下滾動條組件的用處和行爲,方便理解代碼邏輯。vue
由於操做系統和瀏覽器的不一樣,滾動條外觀是不同的。須要作風格統一時,就須要作自定義滾動條。固然也能夠直接修改CSS3中的 ::-webkit-scrollbar
相關屬性來達到修改原生滾動條外觀,但這個屬性部分瀏覽器上沒有可以完美兼容。node
在一個固定高度的元素中,如內部內容超出了父級元素的固定高。爲了讓用戶瀏覽其他的內容,一般都會設置父級元素overflow-y: scroll
出現滾動條。容許用戶以滾動的形式來瀏覽剩下的內容。react
而自定義滾動條,是先經過偏移視圖元素,達到隱藏原生滾動條的效果。同時在視圖元素的右側和下方,增長用標籤寫出的模擬滾動條。監聽模擬滾動條的事件(按下滑塊或點擊軌道),來動態更新視圖窗口的scrollTop
或scrollLeft
值。一樣的,也會監聽視圖窗口的事件(滾動事件或視圖窗口的尺寸縮放事件),來同步更新自定義滾動條的狀態(滑塊所處的位置或滑塊長度)。web
滾動條實際上是當前已瀏覽內容的一個直觀展現,在固定元素中,若是scrollTop
發生改變往下滾動。滾動條中的滑塊也會向下移動。此時可以經過滾動條來得知內容的已滾動程度和剩餘程度。數組
咱們將頁面想象成一個很長的畫布,而咱們能看到的是一個移動的窗口。當頁面往下滾動時,窗口在畫布中也就往下移動,來查看被遮擋的內容。一樣的,滾動塊裏的滑塊也往下移動一樣比例的距離。因此滾動條就是一個等比例的縮小模型。瀏覽器
也就是說,固定元素的高度clientHeight
除以 固定元素包括溢出的總高度scrollHeight
。同等於 滑塊的高度 除以 滾動條的高度。他們的比例是同樣的。閉包
在大概瞭解滾動條的工做內容和計算公式後,看看源碼中是如何處理他們之間的計算關係的。app
scrollbar組件在 package/scrollbar/index.js
中被導出,其中 package/scrollbar/src
是代碼的核心部分,入口文件是 main.js
。dom
<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;
};
複製代碼
獲取滾動條方法會進行如下步驟
若是存在滾動條寬度,會將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的當前展現位置。
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);
},
複製代碼
方法中比較多的公式計算,一時之間比較難理解。下圖是各變量的圖示,接着咱們一個一個拆解。
點擊元素 實則就是軌道區域,其實公式能夠換成這樣看,會更加容易理解。
鼠標點擊位置距離瀏覽器窗口頂部的距離 減去 滾動條區域距離瀏覽器窗口頂部的距離
由於根據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.