_index.js
按照慣例,嵌套組件會(huì)寫一個(gè)_index.js。
import ElCarousel from './src/main';
import ElCarouselItem from './src/item';
export default function(Vue) {
Vue.component(ElCarousel.name, ElCarousel);
Vue.component(ElCarouselItem.name, ElCarouselItem);
};
export { ElCarousel, ElCarouselItem };
Carousel
首先是整個(gè)輪播圖的框架部分。
生命周期
首先我們講講,在組件生命周期中進(jìn)行的一些處理。
created
創(chuàng)建的時(shí)候,設(shè)置了兩個(gè)屬性。
created() {
// 點(diǎn)擊箭頭的回調(diào)函數(shù)
this.throttledArrowClick = throttle(300, true, index => {
// 每隔300ms,可以調(diào)用一次 setActiveItem
this.setActiveItem(index);
});
// 鼠標(biāo)停在指示器上時(shí)的回調(diào)函數(shù)
this.throttledIndicatorHover = throttle(300, index => {
// 停止調(diào)用300ms后,可以再次調(diào)用handleIndicatorHover
this.handleIndicatorHover(index);
});
},
其中,throttle是一個(gè)工具函數(shù),用來在一定時(shí)間內(nèi)限定某函數(shù)的調(diào)用,其實(shí)現(xiàn)原理類似于我再underscore源碼分析里面的函數(shù),在這不進(jìn)行具體描述。值得注意的是第二個(gè)參數(shù)noTrailing,當(dāng)其設(shè)置為true時(shí),保證函數(shù)每隔delay時(shí)間只能執(zhí)行一次,如果設(shè)置為false或者沒有指定,則會(huì)在最后一次函數(shù)調(diào)用后的delay時(shí)間后重置計(jì)時(shí)器。
setActiveItem
setActiveItem是用來設(shè)置當(dāng)前頁的。
methods: {
setActiveItem(index) {
if (typeof index === 'string') {
// 如果索引是字符串,說明是指定名字的
const filteredItems = this.items.filter(item => item.name === index); // 周到對(duì)應(yīng)的item
if (filteredItems.length > 0) {
// 如果找到的items長(zhǎng)度大于0,取第一個(gè)的索引作為我們要使用的索引
index = this.items.indexOf(filteredItems[0]);
}
}
index = Number(index); // 索引轉(zhuǎn)成數(shù)字
if (isNaN(index) || index !== Math.floor(index)) {
// 如果索引不是數(shù)字,或者不是整數(shù)
// 如果不是生產(chǎn)環(huán)境下,就報(bào)warn
process.env.NODE_ENV !== 'production' &&
console.warn('[Element Warn][Carousel]index must be an integer.');
// 返回
return;
}
// 獲取所有項(xiàng)目的長(zhǎng)度
let length = this.items.length;
if (index < 0) { // 如果索引小于0,設(shè)置當(dāng)前頁為最后一頁
this.activeIndex = length - 1;
} else if (index >= length) { // 如果索引大于長(zhǎng)度,設(shè)置當(dāng)前頁為第一頁
this.activeIndex = 0;
} else { // 否則設(shè)置為索引頁
this.activeIndex = index;
}
},
}
其中,activeIndex是data上用來標(biāo)識(shí)當(dāng)前頁的一個(gè)屬性。
data() {
return {
activeIndex: -1
}
}
當(dāng)activeIndex改變的時(shí)候,會(huì)觸發(fā)監(jiān)聽。
watch: {
activeIndex(val, oldVal) {
this.resetItemPosition(); // 重置子項(xiàng)的位置
this.$emit('change', val, oldVal); // 發(fā)送change事件
}
},
其中resetItemPosition是用來重置項(xiàng)目位置的方法。
methods: {
resetItemPosition() {
this.items.forEach((item, index) => {
item.translateItem(index, this.activeIndex);
});
},
}
它將data上的items里面的項(xiàng)目依次遍歷并執(zhí)行carousel-item上的translateItem方法來移動(dòng)。items是data上的一個(gè)屬性,并在carousel掛載的時(shí)候通過updateItems方法來初始化。一會(huì)進(jìn)行介紹。
data() {
return {
items: []
}
}
#### handleIndicatorHover
處理指示器懸浮事件。
```javascript
methods: {
handleIndicatorHover(index) {
// 如果觸發(fā)方式是鼠標(biāo)懸浮并且index不是當(dāng)前索引
if (this.trigger === 'hover' && index !== this.activeIndex) {
this.activeIndex = index; // 設(shè)置當(dāng)前頁為index
}
}
}
其中trigger是觸發(fā)事件的方式,默認(rèn)為hover,通過prop傳遞。
props: {
trigger: {
type: String,
default: 'hover'
},
}
mounted
組件在掛載的時(shí)候進(jìn)行了一些處理。
mounted() {
this.updateItems();
this.$nextTick(() => {
addResizeListener(this.$el, this.resetItemPosition);
if (this.initialIndex < this.items.length && this.initialIndex >= 0) {
this.activeIndex = this.initialIndex;
}
this.startTimer();
});
},
updateItems
首先是更新子項(xiàng)目,獲取所有子組件中的el-carousel-item置于items中。
methods: {
updateItems() {
this.items = this.$children.filter(child => child.$options.name === 'ElCarouselItem');
},
}
nextTick
在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)。
this.$nextTick(() => {
addResizeListener(this.$el, this.resetItemPosition); // 增加resize事件的回調(diào)為resetItemPosition
if (this.initialIndex < this.items.length && this.initialIndex >= 0) {
// 如果初始化的索引有效,則將當(dāng)前頁設(shè)置為初始的索引
this.activeIndex = this.initialIndex;
}
// 啟動(dòng)定時(shí)器
this.startTimer();
});
addResizeListener
這是用來處理resize事件的回調(diào)的,餓了嗎自己進(jìn)行了處理。將有專門的工具類的分析,在這不進(jìn)行展開。
startTimer
其中startTimer是用來啟動(dòng)定時(shí)器的。
methods: {
startTimer() {
if (this.interval <= 0 || !this.autoPlay) return; // 如果間隔時(shí)間非正數(shù)或者設(shè)置了不自動(dòng)播放,直接返回
this.timer = setInterval(this.playSlides, this.interval); // 否則每隔 interval 事件,執(zhí)行playSlides函數(shù)
}
}
其中,interval和autoPlay都是prop。
props: {
autoPlay: {
type: Boolean,
default: true
},
interval: {
type: Number,
default: 3000
},
}
而playSlides是另一個(gè)方法,用來改變activeIndex。
methods: {
playSlides() {
if (this.activeIndex < this.items.length - 1) {
this.activeIndex++;
} else {
this.activeIndex = 0;
}
},
}
beforeDestory
銷毀前移除事件監(jiān)聽。
beforeDestroy() {
if (this.$el) removeResizeListener(this.$el, this.resetItemPosition);
}
el-carousel
最外面是一個(gè)div.el-carousel,并在上面進(jìn)行了一些處理。
<div
class="el-carousel"
:class="{ 'el-carousel--card': type === 'card' }"
@mouseenter.stop="handleMouseEnter"
@mouseleave.stop="handleMouseLeave">
</div>
動(dòng)態(tài)class
會(huì)根據(jù)type這一prop來決定是否顯示卡片化的風(fēng)格。
props: {
type: String
}
鼠標(biāo)進(jìn)入事件
鼠標(biāo)進(jìn)入的時(shí)候綁定了回調(diào)函數(shù)handleMouseEnter,并且使用stop修飾符來阻止事件冒泡。
methods: {
handleMouseEnter() {
this.hover = true; // 設(shè)定hover為true
this.pauseTimer(); // 停止計(jì)時(shí)器
}
}
其中設(shè)置的hover是在data上的一個(gè)Boolean類型的屬性。
data() {
return {
hover: false
}
}
而pauseTimer是實(shí)例上的另一個(gè)方法,用來停止計(jì)時(shí)器。
methods: {
pauseTimer() {
clearInterval(this.timer);
}
}
timer也是在data上的屬性,用來保存計(jì)時(shí)器的id。
data() {
return {
timer: null
}
}
鼠標(biāo)離開事件
鼠標(biāo)離開的時(shí)候綁定了回調(diào)函數(shù)handleMouseLeave,并且也使用了stop修飾符來阻止事件冒泡。
methods: {
handleMouseLeave() {
this.hover = false; // 設(shè)定hover為false
this.startTimer(); // 啟動(dòng)計(jì)時(shí)器
}
}
接下來,分別是輪播圖的主體和指示器兩部分。
container
最外層是container,其高度可以根據(jù)傳入的height改變。
<div
class="el-carousel__container"
:style="{ height: height }">
</div
props: {
height: String
}
然后分別是前進(jìn)后退兩個(gè)控制按鈕和輪播的內(nèi)容。
控制按鈕
兩個(gè)控制按鈕的邏輯基本是一樣的,這里選擇后退的按鈕進(jìn)行分析,另一個(gè)可以進(jìn)行類推。
transition
首先最外面是一個(gè)transition來進(jìn)行動(dòng)畫效果。
<transition name="carousel-arrow-left">
</transition>
其效果設(shè)置如下,使用了位移和透明度的改變:
.carousel-arrow-left-enter,
.carousel-arrow-left-leave-active {
transform: translateY(-50%) translateX(-10px);
opacity: 0;
}
值得注意的是,這里其實(shí)只有X軸的偏移是有效果的,因?yàn)閅軸方向并沒有改變。
button
按鈕的內(nèi)容主體是對(duì)應(yīng)的圖標(biāo),這沒有什么好分析的,但它有許多屬性的設(shè)置,我們將對(duì)其一一進(jìn)行講解:
<button
v-if="arrow !== 'never'"
v-show="arrow === 'always' || hover"
@mouseenter="handleButtonEnter('left')"
@mouseleave="handleButtonLeave"
@click.stop="throttledArrowClick(activeIndex - 1)"
class="el-carousel__arrow el-carousel__arrow--left">
<i class="el-icon-arrow-left"></i>
</button>
arrow
首先是一個(gè)名為arrow的prop來決定按鈕的是否渲染或者是否顯示。它有三種情況:
-
never的時(shí)候,直接不渲染按鈕; -
always的時(shí)候,一直顯示; -
hover的時(shí)候,即默認(rèn)的時(shí)候,懸浮在上面的時(shí)候顯示。
鼠標(biāo)進(jìn)入
鼠標(biāo)進(jìn)入的時(shí)候?qū)⒂|發(fā)handleButtonEnter('left')這一函數(shù),它將對(duì)每一個(gè)輪播的項(xiàng)目通過itemInStage方法處理后和方向進(jìn)行對(duì)比,設(shè)置項(xiàng)目的hover屬性。
methods: {
handleButtonEnter(arrow) {
this.items.forEach((item, index) => {
if (arrow === this.itemInStage(item, index)) {
item.hover = true; // hover設(shè)置為true
}
});
},
itemInStage(item, index) {
const length = this.items.length;
if (index === length - 1 // 當(dāng)前為最后一個(gè)項(xiàng)目
&& item.inStage // 當(dāng)前項(xiàng)目在場(chǎng)景內(nèi)
&& this.items[0].active // 第一個(gè)項(xiàng)目激活狀態(tài)
|| (item.inStage // 當(dāng)前項(xiàng)目在場(chǎng)景內(nèi)
&& this.items[index + 1] // 當(dāng)前項(xiàng)目后面有至少一個(gè)項(xiàng)目
&& this.items[index + 1].active) // 當(dāng)前項(xiàng)目后面一個(gè)項(xiàng)目處于激活狀態(tài)
) {
return 'left'; // 返回left
} else if
(index === 0 // 當(dāng)前為第一個(gè)項(xiàng)目
&& item.inStage // 當(dāng)前項(xiàng)目的inStage為true
&& this.items[length - 1].active // 最后一個(gè)項(xiàng)目處于激活狀態(tài)
|| (item.inStage // 當(dāng)前項(xiàng)目在場(chǎng)景內(nèi)
&& this.items[index - 1] // 當(dāng)前項(xiàng)目前面有至少一個(gè)項(xiàng)目
&& this.items[index - 1].active) // 當(dāng)前項(xiàng)目的前一個(gè)項(xiàng)目處于激活狀態(tài)
) {
return 'right';
}
return false;
},
}
鼠標(biāo)離開
鼠標(biāo)離開時(shí)觸發(fā)handleButtonLeave函數(shù),將所有項(xiàng)目的hover設(shè)置為false。
methods: {
handleButtonLeave() {
this.items.forEach(item => {
item.hover = false;
});
},
}
click
單擊時(shí)觸發(fā)throttleArrowClick函數(shù)并阻止事件冒泡,該函數(shù)每隔300ms可以調(diào)用setActiveItem一次,從而改變當(dāng)前頁。
輪播內(nèi)容
輪播內(nèi)容是一個(gè)slot,用于放置carousel-item。
<slot></slot>
指示器
指示器是一個(gè)無序列表,我們還是由外向內(nèi)進(jìn)行分析。
ul
<ul
class="el-carousel__indicators"
v-if="indicatorPosition !== 'none'"
:class="{
'el-carousel__indicators--outside'
: indicatorPosition === 'outside'
|| type === 'card'
}">
</ul>
ul會(huì)根據(jù)indicatorPosition的設(shè)置進(jìn)行一些設(shè)置,它有幾種情況:
-
none的時(shí)候,直接不渲染指示器; -
outside的時(shí)候,會(huì)顯示在輪播圖框下方; - 默認(rèn)的時(shí)候,會(huì)顯示在輪播圖的下方。
此外,當(dāng)type設(shè)置為type的時(shí)候,也會(huì)顯示在輪播圖框的下方。
li
li標(biāo)簽將通過v-for根據(jù)輪播圖項(xiàng)目進(jìn)行渲染。
<li
v-for="(item, index) in items"
class="el-carousel__indicator"
:class="{ 'is-active': index === activeIndex }"
@mouseenter="throttledIndicatorHover(index)"
@click.stop="handleIndicatorClick(index)">
<button class="el-carousel__button"></button>
</li>
li標(biāo)簽的內(nèi)容是一個(gè)button,沒有什么處理,所有的處理都直接設(shè)置在li標(biāo)簽上,我們將一一進(jìn)行講解。
is-active
如果當(dāng)前的index和activeIndex相等,說明當(dāng)前的指示器是當(dāng)前頁的指示器,加上is-active類。
鼠標(biāo)進(jìn)入
鼠標(biāo)進(jìn)入的時(shí)候?qū)⒂|發(fā)throttledIndicatorHover(index),它在300ms內(nèi)只能調(diào)用handleIndicatorHover一次,它會(huì)在trigger為hover的時(shí)候?qū)?dāng)前頁切換到鼠標(biāo)進(jìn)入的指示器對(duì)應(yīng)的頁上。
click
單擊的時(shí)候會(huì)觸發(fā)handleIndicatorClick(index),直接改變當(dāng)前頁。
methods: {
handleIndicatorClick(index) {
this.activeIndex = index;
},
}
其他
此外還提供了prev和next兩個(gè)方法來切換當(dāng)前頁。
methods: {
prev() {
this.setActiveItem(this.activeIndex - 1);
},
next() {
this.setActiveItem(this.activeIndex + 1);
},
}
還有一個(gè)handleItemChange供carousel-item調(diào)用。
methods: {
handleItemChange() {
debounce(100, () => {
this.updateItems();
});
},
}
debounce保證了如果在100ms內(nèi)再次調(diào)用函數(shù)將重置計(jì)時(shí)器,再等100ms,只有在100ms內(nèi)不再被調(diào)用才會(huì)執(zhí)行updateItems。
carousel-item
輪播圖的子項(xiàng)目。
生命周期
created
創(chuàng)建的時(shí)候調(diào)用了父組件的handleItemChange,這會(huì)更新items里面的內(nèi)容。
created() {
this.$parent && this.$parent.handleItemChange();
},
destroyed
銷毀的時(shí)候也是調(diào)用父組件的handleItemChange。
destroyed() {
this.$parent && this.$parent.handleItemChange();
}
包裹
最外層是一個(gè)div.el-carousel__item并且有一些設(shè)置:
<div
v-show="ready"
class="el-carousel__item"
:class="{
'is-active': active,
'el-carousel__item--card': $parent.type === 'card',
'is-in-stage': inStage,
'is-hover': hover
}"
@click="handleItemClick"
:style="{
msTransform: `translateX(${ translate }px) scale(${ scale })`,
webkitTransform: `translateX(${ translate }px) scale(${ scale })`,
transform: `translateX(${ translate }px) scale(${ scale })`
}">
</div>
show
根據(jù)ready的值決定是否顯示。
動(dòng)態(tài)class
- 根據(jù)
active決定is-active類; - 根據(jù)父組件的
type是不是card決定el-carousel__item-card類; - 根據(jù)
inStage決定is-in-stage類; - 根據(jù)
hover決定is-hover類。
click
單擊的時(shí)候會(huì)觸發(fā)handleItemClick事件,會(huì)將點(diǎn)擊的頁面設(shè)置為當(dāng)前活躍的頁面。
methods: {
handleItemClick() {
const parent = this.$parent;
if (parent && parent.type === 'card') {
const index = parent.items.indexOf(this);
parent.setActiveItem(index);
}
}
}
動(dòng)態(tài)style
設(shè)置transform屬性,根據(jù)translate和scale兩個(gè)值來改變效果。
內(nèi)容
內(nèi)容是一個(gè)遮罩mask和slot,前者會(huì)在card模式下且當(dāng)前頁不是活躍頁的時(shí)候顯現(xiàn),后者用于定制輪播圖的內(nèi)容。
<div
v-if="$parent.type === 'card'"
v-show="!active"
class="el-carousel__mask">
</div>
<slot></slot>
其他
剩下還有三個(gè)方法,用來處理輪播的效果。
processIndex
對(duì)當(dāng)前索引進(jìn)行處理,其中最后兩個(gè)else if是為了將所有的輪播平分。
processIndex(index, activeIndex, length) {
if (activeIndex === 0 && index === length - 1) {
return -1; // 活躍頁是第一頁,當(dāng)前頁是最后一頁,返回-1,這樣相差為1,表示二者相鄰且在左側(cè)
} else if (activeIndex === length - 1 && index === 0) {
return length; // 活躍頁最后一頁,當(dāng)前頁是第一頁,返回總頁數(shù),這樣相差也在1以內(nèi)
} else if (index < activeIndex - 1 && activeIndex - index >= length / 2) {
return length + 1; // 如果,當(dāng)前頁在活躍頁前一頁的前面,并且之間的間隔在一半頁數(shù)即以上,則返回頁數(shù)長(zhǎng)度+1,這樣它們會(huì)被置于最右側(cè)
} else if (index > activeIndex + 1 && index - activeIndex >= length / 2) {
return -2; // 如果,當(dāng)前頁在活躍頁后一頁的后面,并且之間的間隔在一般頁數(shù)即以上,則返回-2,這樣它們會(huì)被置于最左側(cè)
}
return index; // 其他的返回原值
},
calculateTranslate
計(jì)算偏移距離,我們來分析一下為什么要這么計(jì)算,首先我們要知道,正常情況下,卡片模式下,當(dāng)前頁輪播圖占整體寬度的一半,而它兩側(cè)的圖,會(huì)再乘以CARD_SCALE記為s,我們把整體寬度分為4份記為w,我們來計(jì)算一下,在場(chǎng)景里面這幾頁的偏移:
- 當(dāng)前活躍頁的寬度應(yīng)當(dāng)為
2w,因?yàn)樗又兴宰髠?cè)距離整體的距離應(yīng)當(dāng)為(4w - 2w) / 2,則為w; - 前一頁的寬度應(yīng)當(dāng)為
w * 2 * s,因?yàn)槭窍绕圃倏s放,我們計(jì)算偏移距離的時(shí)候應(yīng)當(dāng)反過來計(jì)算,即如果縮放后正好是最左側(cè),那么不縮放的時(shí)候大小應(yīng)當(dāng)為w * 2,多出的寬度為2w - 2ws,則左側(cè)超出了w - ws,且應(yīng)當(dāng)為負(fù)數(shù),因此偏移距離為(s - 1) * w; - 同理后一頁應(yīng)當(dāng)向右多偏移
w - ws,故偏移距離應(yīng)當(dāng)為2w + w - ws,即(3 - s) * w。
可以看出,他們有一個(gè)共同的因子w,然后系數(shù)依次為1、-1 * (1 - s),1 * (3 - s),這里要用一個(gè)式子來表示,可以簡(jiǎn)單的看做一個(gè)線性方程f(x) = (2 - s) * x + 1,具體計(jì)算過程,太過簡(jiǎn)單,在此不細(xì)說。
再往前的頁面的需要偏移縮放后,右邊貼在輪播圖框的左邊的框。最終其左邊框距離輪播圖框的左邊框2ws,然后再加上縮放的距離w - ws,一共是w+ws,即(1+s)*w,因?yàn)槭窍蜃?,所以是?fù)數(shù)。
再往后的頁面,最終其左邊框貼著輪播圖框的右邊的框,相當(dāng)于4w個(gè)距離,然后放大后向左縮進(jìn)w - ws,綜上為4w - w + ws,即(3 + s) * w。
calculateTranslate(index, activeIndex, parentWidth) {
if (this.inStage) {
return parentWidth * ((2 - CARD_SCALE) * (index - activeIndex) + 1) / 4;
} else if (index < activeIndex) {
return -(1 + CARD_SCALE) * parentWidth / 4;
} else {
return (3 + CARD_SCALE) * parentWidth / 4;
}
},
translateItem
這是用來移動(dòng)輪播圖子項(xiàng)目的方法。
translateItem(index, activeIndex) {
const parentWidth = this.$parent.$el.offsetWidth; // 獲取父組件的寬度
const length = this.$parent.items.length; // 獲取所有輪播頁面的個(gè)數(shù)
if (this.$parent.type === 'card') { // 如果是card模式
if (index !== activeIndex && length > 2) { // 當(dāng)前索引不是活躍索引,且所有頁面多于2頁
index = this.processIndex(index, activeIndex, length); // 對(duì)當(dāng)前索引進(jìn)行處理
}
this.inStage = Math.round(Math.abs(index - activeIndex)) <= 1; // 活躍頁及前后兩頁應(yīng)當(dāng)被展示
this.active = index === activeIndex; // 當(dāng)前索引等于活躍頁的索引的話,說明當(dāng)前是活躍的頁面
this.translate = this.calculateTranslate(index, activeIndex, parentWidth); // 計(jì)算偏移量
this.scale = this.active ? 1 : CARD_SCALE; // 計(jì)算縮放大小,后者是事先定義的常量0.83
} else { // 不是card模式
this.active = index === activeIndex; // 當(dāng)前索引是活躍頁的索引的話,說明當(dāng)前是活躍的頁面
this.translate = parentWidth * (index - activeIndex); // 偏移偏差數(shù)量的寬度
}
this.ready = true; // 準(zhǔn)備就緒
},