前幾天美化部落格時發現滾動條在window下實在太難看,所以在基於vue的技術上尋找美化滾動條的方法。記得Element-ui原始碼中有名為 el-scrollbar
的滾動元件,雖然文件上沒有提到,但使用的人還是不少。今天記錄下原始碼的閱讀心得。
在這之前
在看苦澀的程式碼前,先大概描述一下滾動條元件的用處和行為,方便理解程式碼邏輯。
因為作業系統和瀏覽器的不同,滾動條外觀是不一樣的。需要做風格統一時,就需要做自定義滾動條。當然也可以直接修改CSS3中的 ::-webkit-scrollbar
相關屬性來達到修改原生滾動條外觀,但這個屬性部分瀏覽器上沒有能夠完美相容。
在一個固定高度的元素中,如內部內容超出了父級元素的固定高。為了讓使用者瀏覽其餘的內容,通常都會設定父級元素overflow-y: scroll
出現滾動條。允許使用者以滾動的形式來瀏覽剩下的內容。
而自定義滾動條,是先通過偏移檢視元素,達到隱藏原生滾動條的效果。同時在檢視元素的右側和下方,增加用標籤寫出的模擬滾動條。監聽模擬滾動條的事件(按下滑塊或點選軌道),來動態更新檢視視窗的scrollTop
或scrollLeft
值。同樣的,也會監聽檢視視窗的事件(滾動事件或檢視視窗的尺寸縮放事件),來同步更新自定義滾動條的狀態(滑塊所處的位置或滑塊長度)。
滾動條其實是當前已瀏覽內容的一個直觀展示,在固定元素中,如果scrollTop
發生改變往下滾動。滾動條中的滑塊也會向下移動。此時能夠通過滾動條來得知內容的已滾動程度和剩餘程度。
我們將頁面想象成一個很長的畫布,而我們能看到的是一個移動的視窗。當頁面往下滾動時,視窗在畫布中也就往下移動,來檢視被遮擋的內容。同樣的,滾動塊裡的滑塊也往下移動同樣比例的距離。所以滾動條就是一個等比例的縮小模型。
也就是說,固定元素的高度clientHeight
除以 固定元素包括溢位的總高度scrollHeight
。同等於 滑塊的高度 除以 滾動條的高度。他們的比例是一樣的。
在大概瞭解滾動條的工作內容和計算公式後,看看原始碼中是如何處理他們之間的計算關係的。
檔案
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
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;
};
複製程式碼
獲取滾動條方法會進行以下步驟
- 建立outer容器,並記錄outer容器的offsetwidth
- 設定outer容器overflow: scroll,並新建inner容器,追加到outer容器下
- 此時outer容器會帶有滾動條,記錄inner容器的offsetwitdh寬度
- 計算滾動條寬度,並返回
如果存在滾動條寬度,會將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 所做的事情。
- 接收配置引數。
- 根據配置生成wrap與view結構包裹使用的區域,根據配置新增自定義滾動條Bar。
- 對wrap進行滾動事件監聽,對view進行視窗內容改變事件監聽。
- 在滾動或視窗改變時,更新Bar元件的滑塊位置或滑塊長度。
然後來到Bar.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.