前幾天美化博客時(shí)發(fā)現(xiàn)滾動(dòng)條在window下實(shí)在太難看,所以在基于vue的技術(shù)上尋找美化滾動(dòng)條的方法。記得Element-ui源碼中有名為 el-scrollbar
的滾動(dòng)組件囤采,雖然文檔上沒有提到厦坛,但使用的人還是不少。今天記錄下源碼的閱讀心得。
在這之前
在看苦澀的代碼前永淌,先大概描述一下滾動(dòng)條組件的用處和行為,方便理解代碼邏輯佩耳。
因?yàn)椴僮飨到y(tǒng)和瀏覽器的不同遂蛀,滾動(dòng)條外觀是不一樣的。需要做風(fēng)格統(tǒng)一時(shí)干厚,就需要做自定義滾動(dòng)條李滴。當(dāng)然也可以直接修改CSS3中的 ::-webkit-scrollbar
相關(guān)屬性來(lái)達(dá)到修改原生滾動(dòng)條外觀,但這個(gè)屬性部分瀏覽器上沒有能夠完美兼容蛮瞄。
在一個(gè)固定高度的元素中所坯,如內(nèi)部?jī)?nèi)容超出了父級(jí)元素的固定高。為了讓用戶瀏覽其余的內(nèi)容挂捅,通常都會(huì)設(shè)置父級(jí)元素overflow-y: scroll
出現(xiàn)滾動(dòng)條芹助。允許用戶以滾動(dòng)的形式來(lái)瀏覽剩下的內(nèi)容。
而自定義滾動(dòng)條闲先,是先通過(guò)偏移視圖元素状土,達(dá)到隱藏原生滾動(dòng)條的效果。同時(shí)在視圖元素的右側(cè)和下方伺糠,增加用標(biāo)簽寫出的模擬滾動(dòng)條声诸。監(jiān)聽模擬滾動(dòng)條的事件(按下滑塊或點(diǎn)擊軌道),來(lái)動(dòng)態(tài)更新視圖窗口的scrollTop
或scrollLeft
值退盯。同樣的彼乌,也會(huì)監(jiān)聽視圖窗口的事件(滾動(dòng)事件或視圖窗口的尺寸縮放事件),來(lái)同步更新自定義滾動(dòng)條的狀態(tài)(滑塊所處的位置或滑塊長(zhǎng)度)渊迁。
滾動(dòng)條其實(shí)是當(dāng)前已瀏覽內(nèi)容的一個(gè)直觀展示慰照,在固定元素中,如果scrollTop
發(fā)生改變往下滾動(dòng)琉朽。滾動(dòng)條中的滑塊也會(huì)向下移動(dòng)毒租。此時(shí)能夠通過(guò)滾動(dòng)條來(lái)得知內(nèi)容的已滾動(dòng)程度和剩余程度。
我們將頁(yè)面想象成一個(gè)很長(zhǎng)的畫布箱叁,而我們能看到的是一個(gè)移動(dòng)的窗口墅垮。當(dāng)頁(yè)面往下滾動(dòng)時(shí),窗口在畫布中也就往下移動(dòng)耕漱,來(lái)查看被遮擋的內(nèi)容算色。同樣的,滾動(dòng)塊里的滑塊也往下移動(dòng)同樣比例的距離螟够。所以滾動(dòng)條就是一個(gè)等比例的縮小模型灾梦。
也就是說(shuō)峡钓,固定元素的高度clientHeight
除以 固定元素包括溢出的總高度scrollHeight
。同等于 滑塊的高度 除以 滾動(dòng)條的高度若河。他們的比例是一樣的能岩。
在大概了解滾動(dòng)條的工作內(nèi)容和計(jì)算公式后,看看源碼中是如何處理他們之間的計(jì)算關(guān)系的萧福。
文件
scrollbar組件在 package/scrollbar/index.js
中被導(dǎo)出拉鹃,其中 package/scrollbar/src
是代碼的核心部分,入口文件是 main.js
鲫忍。
結(jié)構(gòu)
<el-scrollbar>
<div style="height: 300px;">
<div style="height: 600px;"></div>
</div>
</el-scrollbar>
使用自定義標(biāo)簽 el-scrollbar
裹住使用的區(qū)域毛俏,scrollbar 組件會(huì)生成
view 和 wrap 兩個(gè)父級(jí)元素包裹插槽中的內(nèi)容,還有兩種類型的自定義滾動(dòng)條 horizontal 和 vertical饲窿。
main.js
main.js默認(rèn)導(dǎo)出一個(gè)對(duì)象,接收一系列配置焕蹄。
name: 'ElScrollbar',
components: {
// 滾動(dòng)條組件逾雄,擁有水平與垂直兩種形態(tài)
Bar
},
props: {
native: Boolean, // 是否使用原生滾動(dòng)條,即不附加自定義滾動(dòng)條
wrapStyle: {}, // wrap的內(nèi)聯(lián)樣式
wrapClass: {}, // wrap的樣式名
viewClass: {}, // view的樣式名
viewStyle: {}, // view的內(nèi)聯(lián)樣式
noresize: Boolean, // 當(dāng)container尺寸發(fā)生變化時(shí)腻脏,自動(dòng)更新滾動(dòng)條組件的狀態(tài)
tag: { // 組件最外層的標(biāo)簽屬性鸦泳,默認(rèn)為 div
type: String,
default: 'div'
}
},
data() {
return {
sizeWidth: '0', // 水平滾動(dòng)條的寬度
sizeHeight: '0', // 垂直滾動(dòng)條的高度
moveX: 0, // 垂直滾動(dòng)條的移動(dòng)比例
moveY: 0 // 水平滾動(dòng)條的移動(dòng)比例
};
},
組件在render函數(shù)中生成結(jié)構(gòu)。
tips:如果在.vue文件中同時(shí)存在 template
和 render
函數(shù)永品,組件實(shí)例會(huì)先取 template
模板來(lái)渲染組件模板做鹰,而不采用 render
函數(shù)
render函數(shù)一開始會(huì)通過(guò) scrollbarWidth 方法來(lái)計(jì)算當(dāng)前瀏覽器的滾動(dòng)條寬度。
render(h) {
// 獲取瀏覽器的滾動(dòng)條寬度
let gutter = scrollbarWidth();
// wrap內(nèi)聯(lián)樣式
let style = this.wrapStyle;
...
scrollbarWidth 方法在 scrollbar-width.js 中被默認(rèn)導(dǎo)出鼎姐。
import Vue from 'vue';
// 閉包變量钾麸,用于記錄滾動(dòng)條寬度
let scrollBarWidth;
export default function() {
// 如果在服務(wù)端運(yùn)行,返回 0
if (Vue.prototype.$isServer) return 0;
// 如存在滾動(dòng)條寬度炕桨,直接返回
if (scrollBarWidth !== undefined) return scrollBarWidth;
// 創(chuàng)建outer標(biāo)簽并隱藏
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);
// 記錄沒有滾動(dòng)內(nèi)容的寬度
const widthNoScroll = outer.offsetWidth;
// 設(shè)置外層div滾動(dòng)屬性
outer.style.overflow = 'scroll';
// 創(chuàng)建inner標(biāo)簽饭尝,并追加到outer標(biāo)簽中
const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);
// 此時(shí)outer已經(jīng)可以滾動(dòng),記錄下inner元素的寬度
const widthWithScroll = inner.offsetWidth;
// 銷毀outer元素
outer.parentNode.removeChild(outer);
// 滾動(dòng)條寬度 = 沒有滾動(dòng)條時(shí)的outer寬度 減去 有滾動(dòng)條的outer中的inner寬度
scrollBarWidth = widthNoScroll - widthWithScroll;
// 返回滾動(dòng)條寬度
return scrollBarWidth;
};
獲取滾動(dòng)條方法會(huì)進(jìn)行以下步驟
- 創(chuàng)建outer容器献宫,并記錄outer容器的offsetwidth
- 設(shè)置outer容器overflow: scroll钥平,并新建inner容器,追加到outer容器下
- 此時(shí)outer容器會(huì)帶有滾動(dòng)條姊途,記錄inner容器的offsetwitdh寬度
- 計(jì)算滾動(dòng)條寬度涉瘾,并返回
從而得出此時(shí)的瀏覽器滾動(dòng)條寬度為 100 - 83 = 17 像素
如果存在滾動(dòng)條寬度,會(huì)將wrap設(shè)置偏移捷兰,達(dá)到隱藏原生滾動(dòng)條的效果立叛。
// 如果存在滾動(dòng)條寬度
if (gutter) {
// 設(shè)置偏移寬度,隱藏原生滾動(dòng)條
const gutterWith = `-${gutter}px`;
const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`;
// 根據(jù)配置類型贡茅,生成樣式
/**
* 如是對(duì)象數(shù)組屬性 Array<Object> [{"background": "red"}, {"color": "red"}]
* 則會(huì)被轉(zhuǎn)為對(duì)象 {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結(jié)構(gòu),設(shè)置配置的樣式名和內(nèi)聯(lián)樣式,插槽中的默認(rèn)內(nèi)容會(huì)放入view下彤叉,同時(shí)給view增加ref索引庶柿,用于后續(xù)的事件綁定。
// 生成view
const view = h(
// view的標(biāo)簽類型
this.tag,
// view的屬性
{
class: ["el-scrollbar__view", this.viewClass],
style: this.viewStyle,
ref: "resize"
},
// 接收的插槽內(nèi)容
this.$slots.default
);
接著生成wrap結(jié)構(gòu)秽浇,設(shè)置配置的樣式名和內(nèi)聯(lián)樣式浮庐,同時(shí)監(jiān)聽滾動(dòng)事件
// 生成wrap,并監(jiān)聽滾動(dòng)事件
const wrap = (
<div
ref="wrap"
style={style}
onScroll={this.handleScroll}
class={[
this.wrapClass,
"el-scrollbar__wrap",
gutter ? "" : "el-scrollbar__wrap--hidden-default"
]}
>
{[view]}
</div>
);
接著根據(jù) native 配置柬焕,拼接組件的最終結(jié)構(gòu)审残。
// 如果不使用原生滾動(dòng)條,則添加自定義滾動(dòng)條
if (!this.native) {
/**
* 使用自定義滾動(dòng)條
* <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 {
/**
* 否則使用原生滾動(dòng)條
*
* <div class="el-scrollbar__wrap"> wrap并無(wú)監(jiān)聽滾動(dòng)事件
* <div class="el-scrollbar__view"></div>
* </div>
*/
nodes = [
<div
ref="wrap"
class={[this.wrapClass, "el-scrollbar__wrap"]}
style={style}
>
{[view]}
</div>
];
}
// 返回最終結(jié)構(gòu)
return h("div", { class: "el-scrollbar" }, nodes);
// render函數(shù)結(jié)束
在組件 mounted 和 beforeDestroy 時(shí)斑举,根據(jù)配置進(jìn)行事件監(jiān)聽搅轿。
mounted() {
// 如使用原生滾動(dòng)條,返回
if (this.native) return;
// 在下一更新循環(huán)結(jié)束執(zhí)行更新方法
this.$nextTick(this.update);
// 根據(jù)配置進(jìn)行監(jiān)聽內(nèi)容窗口大小重置事件富玷,執(zhí)行更新方法
!this.noresize && addResizeListener(this.$refs.resize, this.update);
},
beforeDestroy() {
// 如使用原生滾動(dòng)條璧坟,返回
if (this.native) return;
// 根據(jù)配置移除監(jiān)聽內(nèi)容窗口大小重置事件的執(zhí)行更新方法
!this.noresize && removeResizeListener(this.$refs.resize, this.update);
}
addResizeListener 方法在 resize-event.js 中被導(dǎo)出,方法接收兩個(gè)參數(shù)赎懦。監(jiān)聽的DOM節(jié)點(diǎn)和回調(diào)事件雀鹃。
/**
* 窗口縮放執(zhí)行回調(diào)
*/
function resizeHandler(entries) {
// entry是ResizeObserver構(gòu)造函數(shù)執(zhí)行時(shí)傳入的參
for (let entry of entries) {
// 取出之前聲明的回調(diào)函數(shù)數(shù)組
const listeners = entry.target.__resizeListeners__ || [];
// 遍歷執(zhí)行回調(diào)
if (listeners.length) {
listeners.forEach(fn => {
fn();
});
}
}
}
/**
* 添加尺寸改變時(shí)事件監(jiān)聽
* @param {HTMLDivElement} element 元素
* @param {Function} fn 回調(diào)
*/
const addResizeListener = function(element, fn) {
if (!element.__resizeListeners__) {
// 設(shè)置當(dāng)前元素的事件回調(diào)數(shù)組
element.__resizeListeners__ = [];
// 實(shí)例化Resize觀察者對(duì)象
element.__ro__ = new ResizeObserver(resizeHandler);
// 開始觀察指定的目標(biāo)元素,當(dāng)元素尺寸改變時(shí)励两,會(huì)執(zhí)行resizeHandler方法
element.__ro__.observe(element);
window.ro = element.__ro__;
}
// 往回調(diào)數(shù)組中添加本次監(jiān)聽事件
element.__resizeListeners__.push(fn);
};
/**
* 移除尺寸改變時(shí)事件監(jiān)聽
* @param {HTMLDivElement} element 元素
* @param {Function} fn 回調(diào)
*/
const removeResizeListener = function(element, fn) {
if (!element || !element.__resizeListeners__) return;
// 數(shù)組中移除
element.__resizeListeners__.splice(
element.__resizeListeners__.indexOf(fn),
1
);
// 取消目標(biāo)對(duì)象上所有對(duì)element的觀察
if (!element.__resizeListeners__.length) {
element.__ro__.disconnect();
}
};
這樣拳芙,main.js的實(shí)例化過(guò)程就結(jié)束了棺亭。接著我們看wrap綁定的滾動(dòng)回調(diào)handleScroll方法复颈,和生命周期鉤子中見到的update方法脉执。
在wrap窗口滾動(dòng)時(shí),會(huì)執(zhí)行method中的handleScroll方法盲憎,更新data中的moveY和moveX屬性俭正。
moveY和moveX會(huì)作為配置屬性傳給Bar滾動(dòng)條組件,實(shí)時(shí)更新Bar的 translateY(moveY%)
或 translateX(moveX%)
作為滑塊的滾動(dòng)位置焙畔。
handleScroll() {
const wrap = this.wrap;
this.moveY = (wrap.scrollTop * 100) / wrap.clientHeight;
this.moveX = (wrap.scrollLeft * 100) / wrap.clientWidth;
},
moveY和modeX的計(jì)算邏輯掸读,一開始看著有點(diǎn)迷糊。
但是調(diào)轉(zhuǎn)一下計(jì)算順序宏多,就恍然大悟了儿惫。
handleScroll() {
const wrap = this.wrap;
this.moveY = (wrap.scrollTop / wrap.clientHeight) * 100;
this.moveX = (wrap.scrollLeft / wrap.clientWidth) * 100;
},
這里是在求滾動(dòng)高度與可見高度的比例。
上面我們已經(jīng)知道伸但,固定元素的高度clientHeight
除以 固定元素包括溢出的總高度scrollHeight
肾请。同等于 滑塊的高度 除以 滾動(dòng)條的高度。
所以當(dāng)scrollTop
發(fā)生改變時(shí)更胖,我們能夠計(jì)算出比例關(guān)系來(lái)更新滑塊的正確位置铛铁。
假設(shè)我們的wrap高度為300px隔显,當(dāng)前的滾動(dòng)高 scrollTop
為0,滾動(dòng)塊的位置是貼緊頂部的饵逐,此時(shí)Bar組件的 translateY
是 0%括眠。
注意,圖中右邊的滾動(dòng)條和左側(cè)的視圖內(nèi)容倍权,并不真正同高掷豺。僅僅是比例尺關(guān)系。
當(dāng)向下滾動(dòng)時(shí)薄声,scrollTop
剛好為300px(一個(gè)Wrap的高度)当船,側(cè)邊的滾動(dòng)塊也應(yīng)該往下移動(dòng)剛好一個(gè)身位。也就是滾動(dòng)塊的自身的高度默辨。
當(dāng)wrap區(qū)域往下滾動(dòng)剛好一整個(gè)wrap的高度時(shí)德频,側(cè)邊的滾動(dòng)塊也會(huì)往下移動(dòng)一整個(gè)滾動(dòng)塊的長(zhǎng)度。此時(shí)Bar組件的 translateY
應(yīng)該是 100%缩幸。
計(jì)算公式成立:scrollTop
(300px)/ scrollHeight
(300px)* 100 = 100壹置。
這里乘100是因?yàn)锽ar組件中 translateY
是以百分比為單位設(shè)置屬性。
繼續(xù)滾動(dòng)到底部時(shí)桌粉,此時(shí)的scrollTop
已經(jīng)為550px,根據(jù)公式計(jì)算衙四,550 / 300 * 100 滾動(dòng)塊的位置為 translateY(183.333%)铃肯。約要偏移1.8個(gè)滾動(dòng)塊自身的長(zhǎng)度,Bar才能反映出wrap中container的當(dāng)前展示位置传蹈。
update 方法負(fù)責(zé)更新 Bar 的滑塊長(zhǎng)度,在 mounted 生命周期鉤子中惦界,會(huì)根據(jù)
noresize
配置對(duì)view模板進(jìn)行選擇性監(jiān)聽窗口大小改變事件挑格,當(dāng)內(nèi)容窗口大小發(fā)生改變時(shí),會(huì)執(zhí)行 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方法中漂彤,會(huì)計(jì)算出滾動(dòng)塊的百分比高度,然后賦值給sizeHeight或sizeWidth灾搏。更新Bar的滾動(dòng)塊寬度或高度挫望。
heightPercentage是由 可見區(qū)域高度 / 總滾動(dòng)高度,計(jì)算出的占比狂窑。和滑塊在滾動(dòng)條軌道中的占比是一樣的媳板。
this.sizeHeight = heightPercentage < 100 ? heightPercentage + "%" : "";
在計(jì)算sizeHeight時(shí)做了大于100判斷,當(dāng)尺寸改變后的內(nèi)容大于滾動(dòng)高度泉哈,說(shuō)明就不需要滾動(dòng)塊了蛉幸。
至此破讨,main.js 中的所有邏輯都已經(jīng)過(guò)完了。簡(jiǎn)單總結(jié)一下 main.js 所做的事情奕纫。
- 接收配置參數(shù)提陶。
- 根據(jù)配置生成wrap與view結(jié)構(gòu)包裹使用的區(qū)域,根據(jù)配置添加自定義滾動(dòng)條Bar若锁。
- 對(duì)wrap進(jìn)行滾動(dòng)事件監(jiān)聽搁骑,對(duì)view進(jìn)行窗口內(nèi)容改變事件監(jiān)聽。
- 在滾動(dòng)或窗口改變時(shí)又固,更新Bar組件的滑塊位置或滑塊長(zhǎng)度仲器。
然后來(lái)到Bar.js,在點(diǎn)擊滑塊和軌道時(shí)仰冠,如何處理視圖窗口的更新乏冀。
Bar.js
Bar組件接收三個(gè)屬性vertical,size洋只,move辆沦,并在計(jì)算屬性中添加了當(dāng)前滾動(dòng)塊類型的屬性集合bar
,與父組件的wrap
索引识虚。
export default {
name: 'Bar',
props: {
// 是否垂直滾動(dòng)條
vertical: Boolean,
// size 對(duì)應(yīng)的是 水平滾動(dòng)條的 width 或 垂直滾動(dòng)條的height
size: String,
// move 用于 translateX 或 translateY 屬性中
move: Number
},
computed: {
/**
* 從BAR_MAP中返回一個(gè)的新對(duì)象肢扯,垂直滾動(dòng)條屬性集合 或 水平滾動(dòng)條屬性集合
*/
bar() {
return BAR_MAP[this.vertical ? 'vertical' : 'horizontal'];
},
// 父組件的wrap,用于鼠標(biāo)拖動(dòng)滑塊后更新 wrap 的 scrollTop 值
wrap() {
return this.$parent.wrap;
}
},
...
}
bar
會(huì)返回當(dāng)前滾動(dòng)條類型的滾動(dòng)條屬性集合担锤,并在后續(xù)的操作中取對(duì)應(yīng)的值作為更新蔚晨。
const BAR_MAP = {
// 垂直滾動(dòng)塊的屬性
vertical: {
offset: 'offsetHeight',
scroll: 'scrollTop',
scrollSize: 'scrollHeight',
size: 'height',
key: 'vertical',
axis: 'Y',
client: 'clientY',
direction: 'top'
},
// 水平滾動(dòng)塊的屬性
horizontal: {
offset: 'offsetWidth',
scroll: 'scrollLeft',
scrollSize: 'scrollWidth',
size: 'width',
key: 'horizontal',
axis: 'X',
client: 'clientX',
direction: 'left'
}
};
在render函數(shù)中,會(huì)對(duì)軌道區(qū)域和滑塊進(jìn)行鼠標(biāo)按下事件進(jìn)行監(jiān)聽肛循,并對(duì)滑塊進(jìn)行內(nèi)聯(lián)樣式綁定铭腕,在 size, move, bar 等屬性發(fā)生改變時(shí),動(dòng)態(tài)的改變滑塊的位置或長(zhǎng)度多糠。
render(h) {
// size: 'width' || 'height'
// move: 滾動(dòng)塊的位置累舷,單位為百分比
// bar: 垂直滾動(dòng)條屬性集合 或 水平滾動(dòng)條屬性集合
const { size, move, bar } = this;
return (
<div
class={['el-scrollbar__bar', 'is-' + bar.key]}
// 滾動(dòng)條區(qū)域監(jiān)聽 鼠標(biāo)按下事件
onMousedown={this.clickTrackHandler} >
<div
ref="thumb"
class="el-scrollbar__thumb"
// 滾動(dòng)塊監(jiān)聽 鼠標(biāo)按下事件
onMousedown={this.clickThumbHandler}
style={renderThumbStyle({ size, move, bar })}>
</div>
</div>
);
}
我們以垂直類型的Bar組件為例,首先看綁定在軌道區(qū)域的鼠標(biāo)點(diǎn)擊事件回調(diào) clickTrackHandler 方法夹孔。
在點(diǎn)擊軌道區(qū)域時(shí)被盈,滑塊會(huì)快速定位到該位置,并且更新視圖的scrollTop
搭伤。這就是 clickTrackHandler 處理的事情害捕。
// 對(duì)按下 滾動(dòng)條區(qū)域 的某一個(gè)位置進(jìn)行快速定位
clickTrackHandler(e) {
/**
* getBoundingClientRect() 方法返回元素的大小及其相對(duì)于瀏覽器頁(yè)面的位置。
* this.bar.direction = "top"
* this.bar.client = "clientY"
* e.clientY 是事件觸發(fā)時(shí)闷畸,鼠標(biāo)指針相對(duì)于瀏覽器窗口頂部的距離尝盼。
*/
// 偏移量 絕對(duì)值 (當(dāng)前元素距離瀏覽器窗口的 頂部/左側(cè) 距離 減去 當(dāng)前點(diǎn)擊的位置距離瀏覽器窗口的 頂部/左側(cè) 距離)
const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]);
// 滑動(dòng)塊一半高度
const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2);
// 計(jì)算點(diǎn)擊后,根據(jù) 偏移量 計(jì)算在 滾動(dòng)條區(qū)域的總高度 中的占比佑菩,也就是 滾動(dòng)塊 所處的位置
const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]);
// 設(shè)置外殼的 scrollHeight 或 scrollWidth 新值盾沫。達(dá)到滾動(dòng)內(nèi)容的效果
this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
},
方法中比較多的公式計(jì)算裁赠,一時(shí)之間比較難理解。下圖是各變量的圖示赴精,接著我們一個(gè)一個(gè)拆解佩捞。
在方法中,第一步會(huì)計(jì)算滑塊的偏移量(offset)蕾哟。代碼中的偏移量計(jì)算公式是:
點(diǎn)擊元素距離瀏覽器窗口頂部的距離 減去 鼠標(biāo)點(diǎn)擊位置距離瀏覽器窗口頂部的距離一忱,再求結(jié)果的絕對(duì)值。
點(diǎn)擊元素 實(shí)則就是軌道區(qū)域谭确,其實(shí)公式可以換成這樣看帘营,會(huì)更加容易理解。
鼠標(biāo)點(diǎn)擊位置距離瀏覽器窗口頂部的距離 減去 滾動(dòng)條區(qū)域距離瀏覽器窗口頂部的距離
因?yàn)楦鶕?jù)scrollBar組件的使用位置不同(有的包裹整個(gè)頁(yè)面窗口逐哈,有的包裹一小塊菜單區(qū)域)芬迄,滾動(dòng)條區(qū)域也不一定完全貼緊瀏覽器窗口的頂部。所以這邊需要用 鼠標(biāo)點(diǎn)擊位置距離瀏覽器窗口頂部的距離e[this.bar.client]
將 滾動(dòng)條區(qū)域距離瀏覽器窗口頂部的距離e.target.getBoundingClientRect()[this.bar.direction]
減去昂秃,才能得出準(zhǔn)確的 偏移量offset
禀梳。
/**
* getBoundingClientRect() 方法返回元素的大小及其相對(duì)于瀏覽器頁(yè)面的位置。
* this.bar.direction = "top"
* this.bar.client = "clientY"
* e.clientY 是事件觸發(fā)時(shí)肠骆,鼠標(biāo)指針相對(duì)于瀏覽器窗口頂部的距離算途。
*/
// 偏移量 絕對(duì)值 (當(dāng)前元素距離瀏覽器窗口的 垂直/水平 坐標(biāo) 減去 當(dāng)前點(diǎn)擊的位置距離瀏覽器窗口的 垂直/水平 坐標(biāo))
const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]);
接下來(lái)計(jì)算的是滑動(dòng)塊一半的高度,用于后續(xù)邏輯處理蚀腿。
// 滑動(dòng)塊一半高度
const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2);
根據(jù)瀏覽器滾動(dòng)條操作行為嘴瓤,一般我們點(diǎn)擊軌道某個(gè)點(diǎn)時(shí),滑塊的中心總會(huì)在我們的落點(diǎn)位置唯咬。
在用偏移量
offset
減去滾動(dòng)塊的一半高度 thumbHalf
后得出 滑塊總移動(dòng)的長(zhǎng)度纱注。再用 滑塊總移動(dòng)的長(zhǎng)度 除 滾動(dòng)區(qū)域的總高度畏浆,得出 滾動(dòng)比例thumbPositionPercentage
胆胰。得出 滾動(dòng)比例 后,因?yàn)闈L動(dòng)條和視圖是一個(gè)縮放的比例尺關(guān)系刻获。此時(shí)用 滾動(dòng)比例 乘 wrap的 scrollHeight 得出滾動(dòng)距離蜀涨,再對(duì) wrap 的 scrollTop 進(jìn)行賦值,視圖便滾動(dòng)到需要更新展示的內(nèi)容中蝎毡。
// 計(jì)算點(diǎn)擊后厚柳,根據(jù) 偏移量 計(jì)算在 滾動(dòng)條區(qū)域的總高度 中的占比,也就是 滾動(dòng)塊 所處的位置
const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]);
// 設(shè)置外殼的 scrollTop 或 scrollLeft 新值沐兵。達(dá)到滾動(dòng)內(nèi)容的效果
this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
接下來(lái)是滑塊監(jiān)聽的鼠標(biāo)按下事件别垮,clickThumbHandler。
clickThumbHandler 方法會(huì)在鼠標(biāo)按下滑塊時(shí)扎谎,監(jiān)聽鼠標(biāo)移動(dòng)事件和鼠標(biāo)按鍵釋放事件碳想,更新滑塊位置的同時(shí)烧董,也更新視圖窗口的滾動(dòng)位置。
// 按下滑動(dòng)塊
clickThumbHandler(e) {
/**
* 防止右鍵單擊滑動(dòng)塊
* e.ctrlKey: 檢測(cè)事件發(fā)生時(shí)Ctrl鍵是否被按住了
* e.button: 指示當(dāng)事件被觸發(fā)時(shí)哪個(gè)鼠標(biāo)按鍵被點(diǎn)擊 0胧奔,鼠標(biāo)左鍵逊移;1,鼠標(biāo)中鍵龙填;2胳泉,鼠標(biāo)右鍵
*/
if (e.ctrlKey || e.button === 2) {
return;
}
// 開始記錄拖拽
this.startDrag(e);
// 記錄點(diǎn)擊滑塊時(shí)的位置距滾動(dòng)塊底部的距離
this[this.bar.axis] = (
// 滑塊的高度
e.currentTarget[this.bar.offset] -
// 點(diǎn)擊滑塊距離頂部的位置 減去 滑塊元素距離頂部的位置
(e[this.bar.client] - e.currentTarget.getBoundingClientRect()[this.bar.direction])
);
},
開始先判斷是否鼠標(biāo)右鍵觸發(fā)的事件,如真返回岩遗。接著執(zhí)行 startDrag 方法扇商。
最后會(huì)計(jì)算點(diǎn)擊滑塊時(shí)的位置距滾動(dòng)塊底部的距離。然后賦值給this[this.bar.axis]
喘先,因?yàn)楫?dāng)前滾動(dòng)條類型是垂直滾動(dòng)條钳吟,所以this.bar.axis
從計(jì)算屬性中獲取為字符串 Y
,this['Y']
會(huì)用于后續(xù)的計(jì)算窘拯。
this['Y']
的計(jì)算公式為:滑塊的高度 減去 (點(diǎn)擊滑塊的位置距離頁(yè)面窗口頂部的距離 clientY
減去 滑塊元素距離頁(yè)面窗口頂部的距離Rect.top
)
this.bar.axis
從計(jì)算屬性中獲取红且,返回的是字符串,X 或 Y涤姊。但在Bar組件的 data 中暇番,并沒有對(duì) this['X']
或 this['Y']
這兩個(gè)屬性進(jìn)行聲明。
原因是因?yàn)?strong>Bar組件有兩種類型思喊,垂直或水平壁酬。所以作者沒有選擇一開始就聲明,而是通過(guò)后續(xù)的操作再動(dòng)態(tài)掛上 X 或 Y 屬性
需要注意的是恨课,這樣動(dòng)態(tài)添加的屬性舆乔,并不是一個(gè)響應(yīng)式的屬性。即未被vue進(jìn)行getter/setter
重寫剂公,在數(shù)據(jù)發(fā)生改變后視圖是不會(huì)同步更新的希俩。
但是這里僅僅用于數(shù)據(jù)層面上的使用,并不在視圖上使用纲辽。問(wèn)題不大颜武。
具體可以查閱文檔, 深入響應(yīng)式原理
在startDrag方法中,會(huì)記錄按下狀態(tài)拖吼,并監(jiān)聽鼠標(biāo)移動(dòng)和鼠標(biāo)按鈕松開事件鳞上。
// 開始拖拽
startDrag(e) {
// 停止后續(xù)的相同事件函數(shù)執(zhí)行
e.stopImmediatePropagation();
// 記錄按下狀態(tài)
this.cursorDown = true;
// 監(jiān)聽鼠標(biāo)移動(dòng)事件
on(document, 'mousemove', this.mouseMoveDocumentHandler);
// 監(jiān)聽鼠標(biāo)按鍵松開事件
on(document, 'mouseup', this.mouseUpDocumentHandler);
// 拖拽滾動(dòng)塊時(shí),此時(shí)禁止鼠標(biāo)長(zhǎng)按劃過(guò)文本選中吊档。
document.onselectstart = () => false;
},
on方法和off方法在 utils/dom
中被導(dǎo)出篙议,在導(dǎo)出時(shí)會(huì)對(duì)環(huán)境進(jìn)行兼容處理,導(dǎo)出對(duì)應(yīng)的事件監(jiān)聽處理函數(shù)怠硼。
/* istanbul ignore next */
export const on = (function() {
// 查詢實(shí)例是否在服務(wù)端運(yùn)行鬼贱,與是否支持 addEventListener趾断,返回對(duì)應(yīng)處理監(jiān)聽函數(shù)
if (!isServer && document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
// 適用于現(xiàn)代瀏覽器的監(jiān)聽事件 addEventListener
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
// 用于 ie 部分版本瀏覽器的監(jiān)聽事件 attachEvent
element.attachEvent('on' + event, handler);
}
};
}
})();
/* istanbul ignore next */
export const off = (function() {
// 查詢實(shí)例是否在服務(wù)端運(yùn)行,與是否支持 removeEventListener吩愧,返回對(duì)應(yīng)處理監(jiān)聽函數(shù)
if (!isServer && document.removeEventListener) {
return function(element, event, handler) {
if (element && event) {
// 適用于現(xiàn)代瀏覽器的移除事件監(jiān)聽 removeEventListener
element.removeEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event) {
// 用于 ie 部分版本瀏覽器的移除事件監(jiān)聽 detachEvent
element.detachEvent('on' + event, handler);
}
};
}
})();
在鼠標(biāo)移動(dòng)時(shí)芋酌,會(huì)執(zhí)行mouseMoveDocumentHandler事件。
方法進(jìn)入會(huì)判斷cursorDown
和this.['Y']
是否存在雁佳,如果為假脐帝。說(shuō)明方法并不是正常操作觸發(fā),結(jié)束返回糖权。
在鼠標(biāo)的不斷移動(dòng)中堵腹,計(jì)算按住滑塊移動(dòng)時(shí)的位置距離軌道頂部的實(shí)際距離offset
,同時(shí)用之前記錄下來(lái)的this['Y']
計(jì)算出按下滑塊時(shí)距離滑塊頂部的距離thumbClickPosition
星澳。
此時(shí)offset
減去thumbClickPosition
疚顷,就是滑塊在軌道中實(shí)際移動(dòng)的距離。再用此值除以軌道長(zhǎng)度禁偎。便是滾動(dòng)比例thumbPositionPercentage
腿堤。
最后用thumbPositionPercentage
乘視圖窗口的滾動(dòng)高度,便是視圖窗口需要更新滾動(dòng)的距離如暖。
// 按下滾動(dòng)條笆檀,并且鼠標(biāo)移動(dòng)時(shí)
mouseMoveDocumentHandler(e) {
// 如果按下狀態(tài)為假,返回
if (this.cursorDown === false) return;
// 點(diǎn)擊位置時(shí)距滾動(dòng)塊底部的距離
const prevPage = this[this.bar.axis];
if (!prevPage) return;
// (滑塊距離頁(yè)面頂部的距離 減 鼠標(biāo)移動(dòng)時(shí)距離頂部的距離) * -1
const offset = ((this.$el.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]) * -1);
// 按下滑塊位置距離滑塊頂部的距離
const thumbClickPosition = (this.$refs.thumb[this.bar.offset] - prevPage);
// 滑動(dòng)距離在滾動(dòng)軌道長(zhǎng)度的占比
const thumbPositionPercentage = ((offset - thumbClickPosition) * 100 / this.$el[this.bar.offset]);
// 根據(jù)比例盒至,更新視圖窗口的滾動(dòng)距離
this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
},
在鼠標(biāo)松開時(shí)酗洒,重置各記錄的狀態(tài)枷遂,并取消監(jiān)聽的鼠標(biāo)移動(dòng)事件樱衷。
// 按下滾動(dòng)條,并且鼠標(biāo)松開
mouseUpDocumentHandler(e) {
// 重置按下狀態(tài)
this.cursorDown = false;
// 重置當(dāng)前點(diǎn)擊在滾動(dòng)塊的位置
this[this.bar.axis] = 0;
// 移除監(jiān)聽鼠標(biāo)移動(dòng)事件
off(document, 'mousemove', this.mouseMoveDocumentHandler);
// 拖拽結(jié)束酒唉,此時(shí)允許鼠標(biāo)長(zhǎng)按劃過(guò)文本選中矩桂。
document.onselectstart = null;
}
源碼到這里已經(jīng)全部解讀結(jié)束,因個(gè)人水平有限黔州,難免會(huì)有不準(zhǔn)確或者存在歧義的地方耍鬓,希望能夠不吝賜教阔籽,共同交流進(jìn)步流妻。
祝你有個(gè)愉快的勞動(dòng)節(jié)假期。:)
Have a nice day.