滑塊組件整體來講仍是比較簡單的,可是仍是涉及到了不少原生的js知識,下圖是一個最基本的滑塊組件css
能夠看出主要分爲滑塊軌道部分和滑塊按鈕這2大部分,而滑塊軌道已滑過的藍色部分也是一個部分,包含在滑塊軌道內,而後上方的數字是Element的tooltip組件html
對於上面的組件,鼠標按住滑塊按鈕拖動即可以進行滑動,而後點擊滑塊軌道也可以將滑塊移動到指定位置,所以主要邏輯就是拖動的實現和點擊軌道的邏輯,官網代碼點此瀏覽器
簡化後的html結構以下bash
<div class="el-slider" ...
//數字輸入框
<el-input-number v-if="showInput">
</el-input-number>
//滑塊軌道
<div class="el-slider__runway"
//已經滑過的軌道
<div class="el-slider__bar" :style="barStyle">
</div>
//第一個滑塊按鈕
<slider-button></slider-button>
//第二個滑塊按鈕
<slider-button></slider-button>
//滑塊軌道的間斷點
<div class="el-slider__stop"></div>
</div>
</div>
</div>
複製代碼
上面的結構看着多,其實大多都是附屬結構,上面的輸入框就是由用戶選項開啓,而後有2個按鈕,主要是用於範圍選擇,通常狀況只用第一個按鈕,最後的間斷點其實也不多用到,上面的<slider-button>
是單獨的一個組件,由於這個組件會涉及到不少東西,因此單獨作成了一個組件app
再簡單分析下css,由圖中能夠推斷出藍色部分的已滑過背景的div確定是絕對定位的,而後滑塊按鈕也是絕對定位,而滑塊軌道相對定位,經過改變藍色部分的width來改變其長度,滑塊按鈕的位置是由left來肯定,是個百分比dom
首先先看下這個滑塊組件的用法,最基礎的組件僅僅須要以下代碼就行ide
<el-slider v-model="value1"></el-slider>
複製代碼
value1是data中的值,當滑動滑塊時這個值也會改變。咱們先從slider-button這個按鈕組件進行分析,由於它纔是核心,該組件的代碼200多行,可見不簡單啊,僅僅一個子組件就那麼多,html結構以下函數
<template>
<div
class="el-slider__button-wrapper"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@mousedown="onButtonDown"
@touchstart="onButtonDown"
:class="{ 'hover': hovering, 'dragging': dragging }"
:style="wrapperStyle"
ref="button"
tabindex="0"
@focus="handleMouseEnter"
@blur="handleMouseLeave"
@keydown.left="onLeftKeyDown"
@keydown.right="onRightKeyDown"
@keydown.down.prevent="onLeftKeyDown"
@keydown.up.prevent="onRightKeyDown"
>
<el-tooltip
placement="top"
ref="tooltip"
:popper-class="tooltipClass"
:disabled="!showTooltip">
<span slot="content">{{ formatValue }}</span>
<div class="el-slider__button" :class="{ 'hover': hovering, 'dragging': dragging }"></div>
</el-tooltip>
</div>
</template>
複製代碼
這是一個wrapper裏面嵌套了一個div做爲button的主體,最內層的div是咱們看到的按鈕,而外層的div是一個比較大一點的div,它用來響應點擊事件等。先看@mouseenter和@mouseleave,這2個方法對應的處理函數就是用來處理鼠標移動到按鈕上顯示tooltip與否源碼分析
handleMouseEnter() {
this.hovering = true;
this.displayTooltip();
},
handleMouseLeave() {
this.hovering = false;
this.hideTooltip();
},
複製代碼
接下來@mousedown="onButtonDown",@touchstart="onButtonDown"這2個都是處理鼠標按下和移動端按下的邏輯,由於拖動按鈕首先是按下按鈕再移動鼠標進行拖動,最終擡起鼠標,onButtonDown代碼以下ui
onButtonDown(event) {
if (this.disabled) return;
event.preventDefault();
this.onDragStart(event);
window.addEventListener('mousemove', this.onDragging);
window.addEventListener('touchmove', this.onDragging);
window.addEventListener('mouseup', this.onDragEnd);
window.addEventListener('touchend', this.onDragEnd);
window.addEventListener('contextmenu', this.onDragEnd);
},
複製代碼
首先若是組件禁用則直接返回,而後是preventDefault防止觸發默認事件,可是這裏爲啥要給這個按鈕preventDefautl??它只是一個普通的div而已啊,很奇怪,難道是移動端的處理?第三句this.onDragStart(event)
處理了點擊開始的邏輯,代碼以下
onDragStart(event) {
this.dragging = true;
this.isClick = true;
if (event.type === 'touchstart') {
event.clientY = event.touches[0].clientY;
event.clientX = event.touches[0].clientX;
}
if (this.vertical) {
this.startY = event.clientY;
} else {
this.startX = event.clientX;
}
this.startPosition = parseFloat(this.currentPosition);
this.newPosition = this.startPosition;
},
複製代碼
當用戶點擊滑塊按鈕時,將標誌變量dragging設爲true,標誌着進入了拖動狀態,這個變量不能少,由於在mousemove中須要進行位置的更新,而mousemove中則要判斷是否在移動狀態,是在移動狀態才能更新位置。第二句this.isClick變量表明這次按下鼠標是一次單純的點擊仍是一次拖動滑塊操做,後面會講解。而後若是是移動端touch操做,則將event.clientX和event.clientY賦值爲移動端的值,簡單回顧下clientX和clientY,這2個帶表鼠標點擊點距離瀏覽器可視區域的左側和上側的值,不包括滾動條,也就是客戶區座標
如上圖,注意clientX和offsetX的區別,offsetX指的是點擊點距離點擊元素的左側的距離。這裏爲啥要得到clientX並保存在startX中呢,是由於滑動滑塊最後擡起鼠標時,須要計算擡起鼠標時的clientX的值和startX之間的差,這個差就是x軸移動的距離。而後this.startPosition = parseFloat(this.currentPosition)
將初始位置記錄下到startPosition中,currentPosition是計算屬性
currentPosition() {
return `${ (this.value - this.min) / (this.max - this.min) * 100 }%`;
},
複製代碼
上述代碼說明currentPostion是個百分比,裏面的this.value是該組件v-model中的value,也就是父組件中的firstValue,這個firstValue又是由用戶傳入到滑塊組件的v-model中來的,這裏有點繞,總之用戶最初傳入滑塊組件的data會反映到這裏來,而後給currentPostion一個初始值, 下面看一下拖動鼠標過程當中的邏輯
onDragging(event) {
if (this.dragging) {
this.isClick = false;
this.displayTooltip();
this.$parent.resetSize();
let diff = 0;
if (event.type === 'touchmove') {
event.clientY = event.touches[0].clientY;
event.clientX = event.touches[0].clientX;
}
if (this.vertical) {
this.currentY = event.clientY;
diff = (this.startY - this.currentY) / this.$parent.sliderSize * 100;
} else {
this.currentX = event.clientX;
diff = (this.currentX - this.startX) / this.$parent.sliderSize * 100;
}
this.newPosition = this.startPosition + diff;
this.setPosition(this.newPosition);
}
複製代碼
首先必須判斷是否在拖動狀態中,若是不在則什麼都不作,而後this.isClick = false
將是不是點擊操做這個flag記爲false,說明一但開始拖動,那麼就不是一次點擊操做。接下來this.displayTooltip()
用於顯示tooltip.而後this.$parent.resetSize()
調用了父組件的resetSize方法,父組件就是slider組件,這個reset方法用於計算父組件的寬度
resetSize() {
if (this.$refs.slider) {
this.sliderSize = this.$refs.slider[`client${ this.vertical ? 'Height' : 'Width' }`];
}
},
複製代碼
this.$refs.slider
獲取到了滑塊軌道的dom元素,,而後後面[`client${ this.vertical ? 'Height' : 'Width' }`]
獲取到了它的客戶區寬度或者高度,clientWidth表示元素的內部寬度,包含width,padding,不包含border和margin以及滾動條寬度·
接着聲明瞭一個diff變量,diff在下面被更新
this.currentX = event.clientX;
diff = (this.currentX - this.startX) / this.$parent.sliderSize * 100;
複製代碼
diff算出來就是鼠標移動的距離佔滑塊軌道的百分比,注意多是負值,這裏就用到了sliderSize。後面一句this.newPosition = this.startPosition + diff
則是聲明瞭滑塊按鈕的新位置(百分比),它就是初始位置加上diff,這個好理解,一樣這個值可能小於或者大於100,最後調用setPostion進行位置更新。因此拖動滑塊的過程就是不斷獲取最新位置並進行位置更新操做。
來看setPostion具體幹了啥
setPosition(newPosition) {
if (newPosition === null || isNaN(newPosition)) return;
if (newPosition < 0) {
newPosition = 0;
} else if (newPosition > 100) {
newPosition = 100;
}
const lengthPerStep = 100 / ((this.max - this.min) / this.step);
const steps = Math.round(newPosition / lengthPerStep);
let value = steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min;
value = parseFloat(value.toFixed(this.precision));
this.$emit('input', value);
this.$nextTick(() => {
this.$refs.tooltip && this.$refs.tooltip.updatePopper();
});
if (!this.dragging && this.value !== this.oldValue) {
this.oldValue = this.value;
}
}
複製代碼
第一個if代表若是newPosition爲非數字的狀況,則不作處理,那麼什麼狀況下newPostion會不是數字呢?看了下多是豎向模式下用戶能夠設置滑塊軌道的高度,若是此時設置的值不當則可能出現非數字的狀況。
第二個if else規定了newPosition只能在0-100間,當鼠標一直往左拖或者右拖時會出現newPostion<0或者>100的狀況。而後const lengthPerStep = 100 / ((this.max - this.min) / this.step)
計算出了每個步長對應的滑塊軌道長度的百分比,裏面的max和min是滑塊組件的最大值和最小值,滑塊被均分爲100份長度。const steps = Math.round(newPosition / lengthPerStep)
計算出了一共須要的步數,向下取整。而後let value = steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min
一句話計算出了最終滑塊的值,而後經過emit將該值傳遞給父組件,而後父組件繼續emit將該值傳遞給滑塊組件的父組件,從而更新了用戶傳入的v-model的值,下面是一個nextTick,由於值改變了就要更新tooltip,那麼用nextTick是爲了保證獲取的數據是dom更新後的
而後上面的代碼僅僅更新了用戶傳入的value,那麼滑塊的實際移動時怎麼是實現的呢?:style="wrapperStyle"
滑塊button的這個sytle綁定就是實現
wrapperStyle() {
return this.vertical ? { bottom: this.currentPosition } : { left: this.currentPosition };
}
currentPosition() {
return `${ (this.value - this.min) / (this.max - this.min) * 100 }%`;
},
複製代碼
wrapperStyle是個計算屬性,返回了currentPostion這個計算屬性,currentPosition又是經過this.value來計算的,因此就明白了緣由,用戶拖動滑塊時會把value經過emit傳遞給父組件,最終更新了用戶傳入的值,而後反過來又觸發了<slider-button>
的計算屬性從而更新了wrapperStyle
//slider組件的代碼
<slider-button
:vertical="vertical"
v-model="firstValue"
:tooltip-class="tooltipClass"
ref="button1">
</slider-button>
複製代碼
//slider組件的代碼
watch: {
value(val, oldVal) {
if (this.dragging ||
Array.isArray(val) &&
Array.isArray(oldVal) &&
val.every((item, index) => item === oldVal[index])) {
return;
}
this.setValues();
},
複製代碼
上述2段代碼說明了數據傳遞的流程。有點繞,firstValue是在setValues這個方法裏被更新的,而滑塊組件對用戶傳入的v-model的value進行了watch,當value變化時就觸發setValues方法從而更新firstValue,進而更新滑塊按鈕的位置。
而後滑塊按鈕這個內置組件的最外層div裏面竟然綁定了鍵盤操做以及丟失焦點和得到焦點方法
<div
class="el-slider__button-wrapper"
...
tabindex="0"
@focus="handleMouseEnter"
@blur="handleMouseLeave"
@keydown.left="onLeftKeyDown"
@keydown.right="onRightKeyDown"
@keydown.down.prevent="onLeftKeyDown"
@keydown.up.prevent="onRightKeyDown"
>
複製代碼
主要這裏設置了tabindex屬性,爲0表示最後才能經過tab鍵訪問到該div,這麼一來經過鍵盤的上下左右鍵也可以控制滑塊了,注意focus和blur方法只有在tabindex屬性存在且不爲-1時才能觸發(經過tab觸發),看下onLeftKeyDown的代碼
onRightKeyDown() {
if (this.disabled) return;
this.newPosition = parseFloat(this.currentPosition) + this.step / (this.max - this.min) * 100;
this.setPosition(this.newPosition);
},
複製代碼
鍵盤按下左鍵時會使滑塊組件的值減小一個步長的長度,this.step / (this.max - this.min) * 100
計算出了一個步長佔滑塊總長度的百分比(0-100間的整數),而後聽過setPosition進行值的更新
當用戶點擊滑塊軌道時能夠將滑塊按鈕移動到指定位置,這須要給滑塊軌道綁定click事件
<div class="el-slider__runway"
:class="{ 'show-input': showInput, 'disabled': sliderDisabled }"
:style="runwayStyle"
@click="onSliderClick"
ref="slider">
複製代碼
下面進入onSliderClick方法
onSliderClick(event) {
if (this.sliderDisabled || this.dragging) return;
this.resetSize();
if (this.vertical) {
const sliderOffsetBottom = this.$refs.slider.getBoundingClientRect().bottom;
this.setPosition((sliderOffsetBottom - event.clientY) / this.sliderSize * 100);
} else {
const sliderOffsetLeft = this.$refs.slider.getBoundingClientRect().left;
this.setPosition((event.clientX - sliderOffsetLeft) / this.sliderSize * 100);
}
this.emitChange();
},
複製代碼
首先判斷是否禁用或者是否在拖動中,若是是則直接返回。這個this.dragging的值是由子組件滑塊按鈕內部的dragging傳遞給父組件的,當點擊滑塊按鈕時click事件會冒泡到滑塊軌道上,因此這裏須要判斷。而後是計算滑塊軌道長度(clientWidth,像素),接下來的if else是判斷組件的方向,分爲垂直和水平,若是是水平的話,則經過getBoundingClientRect().left
獲取滑塊軌道距離客戶區的左側距離,而後用event.clientX - sliderOffsetLeft
得到到鼠標點擊位置到滑塊軌道左側的距離,也就是目標位置距離軌道左側的距離,而後將其換算爲百分比,最後經過setPosition更新位置。這裏的setPosition在上面的分析中出現過,不過不是同一個,上面那個是子組件的setPosition,如今這個是父組件的setPosition
setPosition(percent) {
const targetValue = this.min + percent * (this.max - this.min) / 100;
if (!this.range) {
this.$refs.button1.setPosition(percent);
return;
}
let button;
if (Math.abs(this.minValue - targetValue) < Math.abs(this.maxValue - targetValue)) {
button = this.firstValue < this.secondValue ? 'button1' : 'button2';
} else {
button = this.firstValue > this.secondValue ? 'button1' : 'button2';
}
this.$refs[button].setPosition(percent);
},
複製代碼
這段代碼值得研究,首先計算實際的目標值,這個值只用於選擇範圍的狀況下,所謂選擇範圍就是以下的模式
就是有2個按鈕,控制最小值和最大值。當!this.range也就是否是選擇範圍模式時,直接調用子組件button1的setPosition設置按鈕的位置。後面的if else就比較繞了,這裏涉及到4種狀況,先看下圖 中間紅色中軸線平分藍色條,當鼠標點擊到綠色箭頭區域時實際上該移動minValue那個按鈕,若是是紅色箭頭區域處,該移動maxValue按鈕,這就是經過Math.abs(this.minValue - targetValue) < Math.abs(this.maxValue - targetValue)
來肯定。而後裏面又是個三目運算符,
button = this.firstValue < this.secondValue ? 'button1' : 'button2'
這裏就很奇怪了,firstValue和secondValue指的是2個按鈕的對應的值,分別綁定button1和button2,初始狀態下firstValue對應用戶傳入範圍的較小值,secondValue爲較大值。
注意到你能夠將左側的firstValue的button1一直往右側拖動,直到它大於了右側的secondValue的button2。這個時候你再點擊綠色箭頭區域,那麼移動的按鈕確定應該是左側的button2,不然就會出bug。反之移動button1.因此這個三目運算符不能少!最後經過調用子組件的setPosition更新位置