_index.js
按照惯例,嵌套组件会写一个_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
首先是整个轮播图的框架部分。
生命周期
首先我们讲讲,在组件生命周期中进行的一些处理。
created
创建的时候,设置了两个属性。
created() {
// 点击箭头的回调函数
this.throttledArrowClick = throttle(300, true, index => {
// 每隔300ms,可以调用一次 setActiveItem
this.setActiveItem(index);
});
// 鼠标停在指示器上时的回调函数
this.throttledIndicatorHover = throttle(300, index => {
// 停止调用300ms后,可以再次调用handleIndicatorHover
this.handleIndicatorHover(index);
});
},
其中,throttle
是一个工具函数,用来在一定时间内限定某函数的调用,其实现原理类似于我再underscore源码分析
里面的函数,在这不进行具体描述。值得注意的是第二个参数noTrailing
,当其设置为true
时,保证函数每隔delay
时间只能执行一次,如果设置为false
或者没有指定,则会在最后一次函数调用后的delay
时间后重置计时器。
setActiveItem
setActiveItem
是用来设置当前页的。
methods: {
setActiveItem(index) {
if (typeof index === 'string') {
// 如果索引是字符串,说明是指定名字的
const filteredItems = this.items.filter(item => item.name === index); // 周到对应的item
if (filteredItems.length > 0) {
// 如果找到的items长度大于0,取第一个的索引作为我们要使用的索引
index = this.items.indexOf(filteredItems[0]);
}
}
index = Number(index); // 索引转成数字
if (isNaN(index) || index !== Math.floor(index)) {
// 如果索引不是数字,或者不是整数
// 如果不是生产环境下,就报warn
process.env.NODE_ENV !== 'production' &&
console.warn('[Element Warn][Carousel]index must be an integer.');
// 返回
return;
}
// 获取所有项目的长度
let length = this.items.length;
if (index < 0) { // 如果索引小于0,设置当前页为最后一页
this.activeIndex = length - 1;
} else if (index >= length) { // 如果索引大于长度,设置当前页为第一页
this.activeIndex = 0;
} else { // 否则设置为索引页
this.activeIndex = index;
}
},
}
其中,activeIndex
是data
上用来标识当前页的一个属性。
data() {
return {
activeIndex: -1
}
}
当activeIndex
改变的时候,会触发监听。
watch: {
activeIndex(val, oldVal) {
this.resetItemPosition(); // 重置子项的位置
this.$emit('change', val, oldVal); // 发送change事件
}
},
其中resetItemPosition
是用来重置项目位置的方法。
methods: {
resetItemPosition() {
this.items.forEach((item, index) => {
item.translateItem(index, this.activeIndex);
});
},
}
它将data
上的items
里面的项目依次遍历并执行carousel-item
上的translateItem
方法来移动。items
是data
上的一个属性,并在carousel
挂载的时候通过updateItems
方法来初始化。一会进行介绍。
data() {
return {
items: []
}
}
#### handleIndicatorHover
处理指示器悬浮事件。
```javascript
methods: {
handleIndicatorHover(index) {
// 如果触发方式是鼠标悬浮并且index不是当前索引
if (this.trigger === 'hover' && index !== this.activeIndex) {
this.activeIndex = index; // 设置当前页为index
}
}
}
其中trigger
是触发事件的方式,默认为hover
,通过prop
传递。
props: {
trigger: {
type: String,
default: 'hover'
},
}
mounted
组件在挂载的时候进行了一些处理。
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
首先是更新子项目,获取所有子组件中的el-carousel-item
置于items
中。
methods: {
updateItems() {
this.items = this.$children.filter(child => child.$options.name === 'ElCarouselItem');
},
}
nextTick
在下次 DOM 更新循环结束之后执行延迟回调。
this.$nextTick(() => {
addResizeListener(this.$el, this.resetItemPosition); // 增加resize事件的回调为resetItemPosition
if (this.initialIndex < this.items.length && this.initialIndex >= 0) {
// 如果初始化的索引有效,则将当前页设置为初始的索引
this.activeIndex = this.initialIndex;
}
// 启动定时器
this.startTimer();
});
addResizeListener
这是用来处理resize
事件的回调的,饿了吗自己进行了处理。将有专门的工具类的分析,在这不进行展开。
startTimer
其中startTimer
是用来启动定时器的。
methods: {
startTimer() {
if (this.interval <= 0 || !this.autoPlay) return; // 如果间隔时间非正数或者设置了不自动播放,直接返回
this.timer = setInterval(this.playSlides, this.interval); // 否则每隔 interval 事件,执行playSlides函数
}
}
其中,interval
和autoPlay
都是prop
。
props: {
autoPlay: {
type: Boolean,
default: true
},
interval: {
type: Number,
default: 3000
},
}
而playSlides
是另一个方法,用来改变activeIndex
。
methods: {
playSlides() {
if (this.activeIndex < this.items.length - 1) {
this.activeIndex++;
} else {
this.activeIndex = 0;
}
},
}
beforeDestory
销毁前移除事件监听。
beforeDestroy() {
if (this.$el) removeResizeListener(this.$el, this.resetItemPosition);
}
el-carousel
最外面是一个div.el-carousel
,并在上面进行了一些处理。
<div
class="el-carousel"
:class="{ 'el-carousel--card': type === 'card' }"
@mouseenter.stop="handleMouseEnter"
@mouseleave.stop="handleMouseLeave">
</div>
动态class
会根据type
这一prop
来决定是否显示卡片化的风格。
props: {
type: String
}
鼠标进入事件
鼠标进入的时候绑定了回调函数handleMouseEnter
,并且使用stop
修饰符来阻止事件冒泡。
methods: {
handleMouseEnter() {
this.hover = true; // 设定hover为true
this.pauseTimer(); // 停止计时器
}
}
其中设置的hover
是在data
上的一个Boolean
类型的属性。
data() {
return {
hover: false
}
}
而pauseTimer
是实例上的另一个方法,用来停止计时器。
methods: {
pauseTimer() {
clearInterval(this.timer);
}
}
timer
也是在data
上的属性,用来保存计时器的id
。
data() {
return {
timer: null
}
}
鼠标离开事件
鼠标离开的时候绑定了回调函数handleMouseLeave
,并且也使用了stop
修饰符来阻止事件冒泡。
methods: {
handleMouseLeave() {
this.hover = false; // 设定hover为false
this.startTimer(); // 启动计时器
}
}
接下来,分别是轮播图的主体和指示器两部分。
container
最外层是container
,其高度可以根据传入的height
改变。
<div
class="el-carousel__container"
:style="{ height: height }">
</div
props: {
height: String
}
然后分别是前进后退两个控制按钮和轮播的内容。
控制按钮
两个控制按钮的逻辑基本是一样的,这里选择后退的按钮进行分析,另一个可以进行类推。
transition
首先最外面是一个transition
来进行动画效果。
<transition name="carousel-arrow-left">
</transition>
其效果设置如下,使用了位移和透明度的改变:
.carousel-arrow-left-enter,
.carousel-arrow-left-leave-active {
transform: translateY(-50%) translateX(-10px);
opacity: 0;
}
值得注意的是,这里其实只有X轴的偏移是有效果的,因为Y轴方向并没有改变。
button
按钮的内容主体是对应的图标,这没有什么好分析的,但它有许多属性的设置,我们将对其一一进行讲解:
<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
首先是一个名为arrow
的prop
来决定按钮的是否渲染或者是否显示。它有三种情况:
-
never
的时候,直接不渲染按钮; -
always
的时候,一直显示; -
hover
的时候,即默认的时候,悬浮在上面的时候显示。
鼠标进入
鼠标进入的时候将触发handleButtonEnter('left')
这一函数,它将对每一个轮播的项目通过itemInStage
方法处理后和方向进行对比,设置项目的hover
属性。
methods: {
handleButtonEnter(arrow) {
this.items.forEach((item, index) => {
if (arrow === this.itemInStage(item, index)) {
item.hover = true; // hover设置为true
}
});
},
itemInStage(item, index) {
const length = this.items.length;
if (index === length - 1 // 当前为最后一个项目
&& item.inStage // 当前项目在场景内
&& this.items[0].active // 第一个项目激活状态
|| (item.inStage // 当前项目在场景内
&& this.items[index + 1] // 当前项目后面有至少一个项目
&& this.items[index + 1].active) // 当前项目后面一个项目处于激活状态
) {
return 'left'; // 返回left
} else if
(index === 0 // 当前为第一个项目
&& item.inStage // 当前项目的inStage为true
&& this.items[length - 1].active // 最后一个项目处于激活状态
|| (item.inStage // 当前项目在场景内
&& this.items[index - 1] // 当前项目前面有至少一个项目
&& this.items[index - 1].active) // 当前项目的前一个项目处于激活状态
) {
return 'right';
}
return false;
},
}
鼠标离开
鼠标离开时触发handleButtonLeave
函数,将所有项目的hover
设置为false
。
methods: {
handleButtonLeave() {
this.items.forEach(item => {
item.hover = false;
});
},
}
click
单击时触发throttleArrowClick
函数并阻止事件冒泡,该函数每隔300ms
可以调用setActiveItem
一次,从而改变当前页。
轮播内容
轮播内容是一个slot
,用于放置carousel-item
。
<slot></slot>
指示器
指示器是一个无序列表,我们还是由外向内进行分析。
ul
<ul
class="el-carousel__indicators"
v-if="indicatorPosition !== 'none'"
:class="{
'el-carousel__indicators--outside'
: indicatorPosition === 'outside'
|| type === 'card'
}">
</ul>
ul
会根据indicatorPosition
的设置进行一些设置,它有几种情况:
-
none
的时候,直接不渲染指示器; -
outside
的时候,会显示在轮播图框下方; - 默认的时候,会显示在轮播图的下方。
此外,当type
设置为type
的时候,也会显示在轮播图框的下方。
li
li
标签将通过v-for
根据轮播图项目进行渲染。
<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
标签的内容是一个button
,没有什么处理,所有的处理都直接设置在li
标签上,我们将一一进行讲解。
is-active
如果当前的index
和activeIndex
相等,说明当前的指示器是当前页的指示器,加上is-active
类。
鼠标进入
鼠标进入的时候将触发throttledIndicatorHover(index)
,它在300ms
内只能调用handleIndicatorHover
一次,它会在trigger
为hover
的时候将当前页切换到鼠标进入的指示器对应的页上。
click
单击的时候会触发handleIndicatorClick(index)
,直接改变当前页。
methods: {
handleIndicatorClick(index) {
this.activeIndex = index;
},
}
其他
此外还提供了prev
和next
两个方法来切换当前页。
methods: {
prev() {
this.setActiveItem(this.activeIndex - 1);
},
next() {
this.setActiveItem(this.activeIndex + 1);
},
}
还有一个handleItemChange
供carousel-item
调用。
methods: {
handleItemChange() {
debounce(100, () => {
this.updateItems();
});
},
}
debounce
保证了如果在100ms
内再次调用函数将重置计时器,再等100ms
,只有在100ms
内不再被调用才会执行updateItems
。
carousel-item
轮播图的子项目。
生命周期
created
创建的时候调用了父组件的handleItemChange
,这会更新items
里面的内容。
created() {
this.$parent && this.$parent.handleItemChange();
},
destroyed
销毁的时候也是调用父组件的handleItemChange
。
destroyed() {
this.$parent && this.$parent.handleItemChange();
}
包裹
最外层是一个div.el-carousel__item
并且有一些设置:
<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
根据ready
的值决定是否显示。
动态class
- 根据
active
决定is-active
类; - 根据父组件的
type
是不是card
决定el-carousel__item-card
类; - 根据
inStage
决定is-in-stage
类; - 根据
hover
决定is-hover
类。
click
单击的时候会触发handleItemClick
事件,会将点击的页面设置为当前活跃的页面。
methods: {
handleItemClick() {
const parent = this.$parent;
if (parent && parent.type === 'card') {
const index = parent.items.indexOf(this);
parent.setActiveItem(index);
}
}
}
动态style
设置transform
属性,根据translate
和scale
两个值来改变效果。
内容
内容是一个遮罩mask
和slot
,前者会在card
模式下且当前页不是活跃页的时候显现,后者用于定制轮播图的内容。
<div
v-if="$parent.type === 'card'"
v-show="!active"
class="el-carousel__mask">
</div>
<slot></slot>
其他
剩下还有三个方法,用来处理轮播的效果。
processIndex
对当前索引进行处理,其中最后两个else if
是为了将所有的轮播平分。
processIndex(index, activeIndex, length) {
if (activeIndex === 0 && index === length - 1) {
return -1; // 活跃页是第一页,当前页是最后一页,返回-1,这样相差为1,表示二者相邻且在左侧
} else if (activeIndex === length - 1 && index === 0) {
return length; // 活跃页最后一页,当前页是第一页,返回总页数,这样相差也在1以内
} else if (index < activeIndex - 1 && activeIndex - index >= length / 2) {
return length + 1; // 如果,当前页在活跃页前一页的前面,并且之间的间隔在一半页数即以上,则返回页数长度+1,这样它们会被置于最右侧
} else if (index > activeIndex + 1 && index - activeIndex >= length / 2) {
return -2; // 如果,当前页在活跃页后一页的后面,并且之间的间隔在一般页数即以上,则返回-2,这样它们会被置于最左侧
}
return index; // 其他的返回原值
},
calculateTranslate
计算偏移距离,我们来分析一下为什么要这么计算,首先我们要知道,正常情况下,卡片模式下,当前页轮播图占整体宽度的一半,而它两侧的图,会再乘以CARD_SCALE
记为s
,我们把整体宽度分为4份记为w
,我们来计算一下,在场景里面这几页的偏移:
- 当前活跃页的宽度应当为
2w
,因为它居中所以左侧距离整体的距离应当为(4w - 2w) / 2
,则为w
; - 前一页的宽度应当为
w * 2 * s
,因为是先偏移再缩放,我们计算偏移距离的时候应当反过来计算,即如果缩放后正好是最左侧,那么不缩放的时候大小应当为w * 2
,多出的宽度为2w - 2ws
,则左侧超出了w - ws
,且应当为负数,因此偏移距离为(s - 1) * w
; - 同理后一页应当向右多偏移
w - ws
,故偏移距离应当为2w + w - ws
,即(3 - s) * w
。
可以看出,他们有一个共同的因子w
,然后系数依次为1
、-1 * (1 - s)
,1 * (3 - s)
,这里要用一个式子来表示,可以简单的看做一个线性方程f(x) = (2 - s) * x + 1
,具体计算过程,太过简单,在此不细说。
再往前的页面的需要偏移缩放后,右边贴在轮播图框的左边的框。最终其左边框距离轮播图框的左边框2ws
,然后再加上缩放的距离w - ws
,一共是w+ws
,即(1+s)*w
,因为是向左,所以是负数。
再往后的页面,最终其左边框贴着轮播图框的右边的框,相当于4w
个距离,然后放大后向左缩进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
这是用来移动轮播图子项目的方法。
translateItem(index, activeIndex) {
const parentWidth = this.$parent.$el.offsetWidth; // 获取父组件的宽度
const length = this.$parent.items.length; // 获取所有轮播页面的个数
if (this.$parent.type === 'card') { // 如果是card模式
if (index !== activeIndex && length > 2) { // 当前索引不是活跃索引,且所有页面多于2页
index = this.processIndex(index, activeIndex, length); // 对当前索引进行处理
}
this.inStage = Math.round(Math.abs(index - activeIndex)) <= 1; // 活跃页及前后两页应当被展示
this.active = index === activeIndex; // 当前索引等于活跃页的索引的话,说明当前是活跃的页面
this.translate = this.calculateTranslate(index, activeIndex, parentWidth); // 计算偏移量
this.scale = this.active ? 1 : CARD_SCALE; // 计算缩放大小,后者是事先定义的常量0.83
} else { // 不是card模式
this.active = index === activeIndex; // 当前索引是活跃页的索引的话,说明当前是活跃的页面
this.translate = parentWidth * (index - activeIndex); // 偏移偏差数量的宽度
}
this.ready = true; // 准备就绪
},