Element分析(組件篇)——Carousel

_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;
    }
  },
}

其中,activeIndexdata上用來標(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)。itemsdata上的一個(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ù)
  }
}

其中,intervalautoPlay都是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è)名為arrowprop來決定按鈕的是否渲染或者是否顯示。它有三種情況:

  1. never的時(shí)候,直接不渲染按鈕;
  2. always的時(shí)候,一直顯示;
  3. 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è)置,它有幾種情況:

  1. none的時(shí)候,直接不渲染指示器;
  2. outside的時(shí)候,會(huì)顯示在輪播圖框下方;
  3. 默認(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)前的indexactiveIndex相等,說明當(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ì)在triggerhover的時(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;
  },
}

其他

此外還提供了prevnext兩個(gè)方法來切換當(dāng)前頁。

methods: {
  prev() {
    this.setActiveItem(this.activeIndex - 1);
  },

  next() {
    this.setActiveItem(this.activeIndex + 1);
  },
}

還有一個(gè)handleItemChangecarousel-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

  1. 根據(jù)active決定is-active類;
  2. 根據(jù)父組件的type是不是card決定el-carousel__item-card類;
  3. 根據(jù)inStage決定is-in-stage類;
  4. 根據(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ù)translatescale兩個(gè)值來改變效果。

內(nèi)容

內(nèi)容是一個(gè)遮罩maskslot,前者會(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)景里面這幾頁的偏移:

  1. 當(dāng)前活躍頁的寬度應(yīng)當(dāng)為2w,因?yàn)樗又兴宰髠?cè)距離整體的距離應(yīng)當(dāng)為(4w - 2w) / 2,則為w
  2. 前一頁的寬度應(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;
  3. 同理后一頁應(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)備就緒
},
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容