一、组件介绍
官网链接:Carousel 组件 | Element (gitee.io)。
Carousel组件也称作轮播图,常用于网站首页进行Banner的轮播展示。
Carousel需要与Carousel-item组件配合使用。
1.1 属性
1.1.1 显示类
- height: string类型, 设置走马灯的高度;
- type: string类型,走马灯的类型,可以设置成card;
- indicator-position:string类型,设置指示器的位置,可设置成outside或none;
- direction: string类型,设置显示方向,可选值为
horizontal/vertical,默认是horizontal;
1.1.2 控制类
- initial-index: number类型,设置初始状态时展示的图片索引;
- trigger: string类型;设置指示器的触发方式,默认是hover,可以设置成click;
- autoplay: boolean类型,是否自动切换,默认为true;
- interval: number类型,自动切换间隔,单位是ms,默认是3000(即3s);
- arrow:string类型,设置左右切换箭头显示的时机,可选择为
always/hover/never,默认是hover; - loop:boolean类型,设置循环展示,默认是true;
- pause-on-hover: boolean类型,hover时暂停切换,默认是true;
1.2 事件
- change: 图片切换时触发,参数为
当前激活的Index,之前激活的Index
1.3 可调用API
- setActiveItem:切换幻灯片;参数:幻灯片索引或
carousel-item的name属性值; - prev:切换到上一张;
- next: 切换到下一张;
二、源码分析
2.1 Carousel 组件
2.1.1 template
<template>
<div
ref="root"
:class="carouselClasses"
// 鼠标移入移出事件,用于暂停/重启轮播计时器
@mouseenter.stop="handleMouseEnter"
@mouseleave.stop="handleMouseLeave"
>
<div class="el-carousel__container" :style="{ height: height }">
// 左右切换箭头
<transition v-if="arrowDisplay" name="carousel-arrow-left">
<button
v-show="
(arrow === 'always' || data.hover) &&
(props.loop || data.activeIndex > 0)
"
type="button"
class="el-carousel__arrow el-carousel__arrow--left"
@mouseenter="handleButtonEnter('left')"
@mouseleave="handleButtonLeave"
// 节流处理
@click.stop="throttledArrowClick(data.activeIndex - 1)"
>
<i class="el-icon-arrow-left"></i>
</button>
</transition>
<transition v-if="arrowDisplay" name="carousel-arrow-right">
<button
v-show="
(arrow === 'always' || data.hover) &&
(props.loop || data.activeIndex < items.length - 1)
"
type="button"
class="el-carousel__arrow el-carousel__arrow--right"
@mouseenter="handleButtonEnter('right')"
@mouseleave="handleButtonLeave"
@click.stop="throttledArrowClick(data.activeIndex + 1)"
>
<i class="el-icon-arrow-right"></i>
</button>
</transition>
<slot></slot>
</div>
// 指示器
<ul v-if="indicatorPosition !== 'none'" :class="indicatorsClasses">
<li
v-for="(item, index) in items"
:key="index"
:class="[
'el-carousel__indicator',
'el-carousel__indicator--' + direction,
{ 'is-active': index === data.activeIndex },
]"
@mouseenter="throttledIndicatorHover(index)"
@click.stop="handleIndicatorClick(index)"
>
<button class="el-carousel__button">
<span v-if="hasLabel">{{ item.label }}</span>
</button>
</li>
</ul>
</div>
</template>
2.1.2 script
// 部分核心源码
setup(props: ICarouselProps, { emit }) {
// data
const data = reactive<{
activeIndex: number
containerWidth: number
timer: null | ReturnType<typeof setInterval>
hover: boolean
}>({
activeIndex: -1,
containerWidth: 0,
timer: null,
hover: false,
})
// refs
const root = ref(null)
// 存储CarouselItem子组件数据
const items = ref<CarouselItem[]>([])
// 计算属性,控制左右切换箭头是否展示,不展示的情况:arrow传值为never,diretcion为vertical
const arrowDisplay = computed(
() => props.arrow !== 'never' && props.direction !== 'vertical',
)
// 计算属性,carousel-item是否有lable属性
const hasLabel = computed(() => {
return items.value.some(item => item.label.toString().length > 0)
})
// 计算属性,carousel的class
const carouselClasses = computed(() => {
const classes = ['el-carousel', 'el-carousel--' + props.direction]
if (props.type === 'card') {
classes.push('el-carousel--card')
}
return classes
})
// 计算属性 指示器的class
const indicatorsClasses = computed(() => {
const classes = [
'el-carousel__indicators',
'el-carousel__indicators--' + props.direction,
]
if (hasLabel.value) {
classes.push('el-carousel__indicators--labels')
}
if (props.indicatorPosition === 'outside' || props.type === 'card') {
classes.push('el-carousel__indicators--outside')
}
return classes
})
// methods
// carousel-item注册方法,通过provide/inject模式在carousel-item组件中执行
function addItem(item) {
items.value.push(item)
}
// carousel-item卸载方法
function removeItem(uid) {
const index = items.value.findIndex(item => item.uid === uid)
if (index !== -1) {
items.value.splice(index, 1)
if(data.activeIndex === index) next()
}
}
// 节流处理,避免因幻灯片在轮播,同时用户点击左右箭头切换而导致的闪动
const throttledArrowClick = throttle(
index => {
setActiveItem(index)
},
300,
{ trailing: true },
)
// 节流处理,指示器hover事件的切换
const throttledIndicatorHover = throttle(index => {
handleIndicatorHover(index)
}, 300)
// 清除计时器
function pauseTimer() {
if (data.timer) {
clearInterval(data.timer)
data.timer = null
}
}
// 启动计时器
function startTimer() {
if (props.interval <= 0 || !props.autoplay || data.timer) return
// 定时器的回调中执行playSides
data.timer = setInterval(() => playSlides(), props.interval)
}
// 设置activeIndex
const playSlides = () => {
if (data.activeIndex < items.value.length - 1) {
data.activeIndex = data.activeIndex + 1
} else if (props.loop) {
//循环播放
data.activeIndex = 0
}
}
// 设置激活对象,组件内部可调用,也可通过ref供使用者调用
function setActiveItem(index) {
// string类型的参数,主要是用户调用API时可以传入carousel-item 的 name 属性值
if (typeof index === 'string') {
const filteredItems = items.value.filter(item => item.name === index)
if (filteredItems.length > 0) {
index = items.value.indexOf(filteredItems[0])
}
}
index = Number(index)
if (isNaN(index) || index !== Math.floor(index)) {
console.warn('[Element Warn][Carousel]index must be an integer.')
return
}
let length = items.value.length
const oldIndex = data.activeIndex
if (index < 0) {
data.activeIndex = props.loop ? length - 1 : 0
} else if (index >= length) {
data.activeIndex = props.loop ? 0 : length - 1
} else {
data.activeIndex = index
}
if (oldIndex === data.activeIndex) {
// 重新设置幻灯片位置
resetItemPosition(oldIndex)
}
}
// 设置carouse-item的位置
function resetItemPosition(oldIndex) {
items.value.forEach((item, index) => {
// translateItem是carousel-item子组件的方法,用于设置幻灯片的偏移位置
item.translateItem(index, data.activeIndex, oldIndex)
})
}
// instage表示card模式下,幻灯片是否是当前播放的
function itemInStage(item, index) {
const length = items.value.length
if (
(index === length - 1 && item.inStage && items.value[0].active) ||
(item.inStage &&
items.value[index + 1] &&
items.value[index + 1].active)
) {
return 'left'
} else if (
(index === 0 && item.inStage && items.value[length - 1].active) ||
(item.inStage &&
items.value[index - 1] &&
items.value[index - 1].active)
) {
return 'right'
}
return false
}
// 鼠标移入,暂停计时器
function handleMouseEnter() {
data.hover = true
if (props.pauseOnHover) {
pauseTimer()
}
}
// 鼠标移出,开启计时器
function handleMouseLeave() {
data.hover = false
startTimer()
}
// 鼠标进入左右切换箭头
function handleButtonEnter(arrow) {
if (props.direction === 'vertical') return
items.value.forEach((item, index) => {
if (arrow === itemInStage(item, index)) {
item.hover = true
}
})
}
// 鼠标离开左右切换箭头
function handleButtonLeave() {
if (props.direction === 'vertical') return
items.value.forEach(item => {
item.hover = false
})
}
// 指示器点击事件,将activeIndex设置成点击的index
function handleIndicatorClick(index) {
data.activeIndex = index
}
// 指示器Hover事件,如果trigger设置成hover,则将activeIndex设置成点击的index
function handleIndicatorHover(index) {
if (props.trigger === 'hover' && index !== data.activeIndex) {
data.activeIndex = index
}
}
// 切换至上一张方法
function prev() {
setActiveItem(data.activeIndex - 1)
}
// 切换至下一张方法
function next() {
setActiveItem(data.activeIndex + 1)
}
// watch
// 监测activeIndex的变化,
watch(
() => data.activeIndex,
(current, prev) => {
resetItemPosition(prev)
if (prev > -1) {
// 发射change事件
emit('change', current, prev)
}
},
)
// 监测 autoplay变化,启动/暂停计时器
watch(
() => props.autoplay,
current => {
current ? startTimer() : pauseTimer()
},
)
// 监测loop变化,
watch(
() => props.loop,
() => {
setActiveItem(data.activeIndex)
},
)
// lifecycle
onMounted(() => {
nextTick(() => {
// 监听resize事件
addResizeListener(root.value, resetItemPosition)
// initialIndex在合理范围时,activeIndex设置成initialIndex
if (
props.initialIndex < items.value.length &&
props.initialIndex >= 0
) {
data.activeIndex = props.initialIndex
}
// 启动定时器
startTimer()
})
})
onBeforeUnmount(() => {
// 卸载时,清除事件监听
if (root.value) removeResizeListener(root.value, resetItemPosition)
pauseTimer()
})
// provide,向子组件提供数据
provide<InjectCarouselScope>('injectCarouselScope', {
root,
direction: props.direction,
type: props.type,
items,
loop: props.loop,
addItem,
removeItem,
setActiveItem,
})
},
2.2 Carousel-item 组件
2.2.1 template
<template>
<div
v-show="data.ready"
class="el-carousel__item"
:class="{
'is-active': data.active,
'el-carousel__item--card': type === 'card',
'is-in-stage': data.inStage,
'is-hover': data.hover,
'is-animating': data.animating,
}"
:style="itemStyle"
@click="handleItemClick"
>
<div
v-if="type === 'card'"
v-show="!data.active"
class="el-carousel__mask"
></div>
<slot></slot>
</div>
</template>
2.2.2 script
setup(props: ICarouselItemProps) {
// 获取组件实例
const instance = getCurrentInstance()
// data
const data = reactive({
hover: false,
translate: 0,
scale: 1,
active: false,
ready: false,
inStage: false,
animating: false,
})
// inject 父组件传递的数据
const injectCarouselScope: InjectCarouselScope = inject(
'injectCarouselScope',
)
// computed
// 父组件的方向
const parentDirection = computed(() => {
return injectCarouselScope.direction
})
// 动态行内样式,
const itemStyle = computed(() => {
const translateType =
parentDirection.value === 'vertical' ? 'translateY' : 'translateX'
const value = `${translateType}(${data.translate}px) scale(${data.scale})`
// eg:style="transform: translateX(673px) scale(1);"
const style: PartialCSSStyleDeclaration = {
transform: value,
}
// autoprefixer是一个工具方法,用于添加浏览器兼容前缀,如ms-,webkit-等
return autoprefixer(style)
})
// methods
// 循环播放的情况下,根据传入的index、当前激活的index和items的length计算出合理的索引
function processIndex(index, activeIndex, length) {
if (activeIndex === 0 && index === length - 1) {
return -1
} else if (activeIndex === length - 1 && index === 0) {
return length
} else if (index < activeIndex - 1 && activeIndex - index >= length / 2) {
return length + 1
} else if (index > activeIndex + 1 && index - activeIndex >= length / 2) {
return -2
}
return index
}
// 计算card模式下的位移距离
function calcCardTranslate(index, activeIndex) {
const parentWidth = injectCarouselScope.root.value?.offsetWidth || 0
// inStage是指card模式下,当前播放的幻灯片
if (data.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
}
}
// 计算位移距离:位移距离 = 父容器的宽/高 * (index和activeIndex差值)
function calcTranslate(index, activeIndex, isVertical) {
const distance = (isVertical ? injectCarouselScope.root.value?.offsetHeight : injectCarouselScope.root.value?.offsetWidth) || 0
return distance * (index - activeIndex)
}
const translateItem = (
index: number,
activeIndex: number,
oldIndex: number,
) => {
const parentType = injectCarouselScope.type
const length = injectCarouselScope.items.value.length
// 给当前幻灯片和前一个幻灯片加上animating属性
if (parentType !== 'card' && oldIndex !== undefined) {
data.animating = index === activeIndex || index === oldIndex
}
// loop情况下调用processIndex计算实际的Index
if (index !== activeIndex && length > 2 && injectCarouselScope.loop) {
index = processIndex(index, activeIndex, length)
}
// card模式
if (parentType === 'card') {
// card模式不支持垂直形式
if (parentDirection.value === 'vertical') {
console.warn(
'[Element Warn][Carousel]vertical direction is not supported in card mode',
)
}
data.inStage = Math.round(Math.abs(index - activeIndex)) <= 1
data.active = index === activeIndex
// 调用calcCardTranslate计算偏移距离
data.translate = calcCardTranslate(index, activeIndex)
data.scale = data.active ? 1 : CARD_SCALE
} else {
// 普通模式
data.active = index === activeIndex
const isVertical = parentDirection.value === 'vertical'
// 计算偏移距离
data.translate = calcTranslate(index, activeIndex, isVertical)
}
data.ready = true
}
// 点击幻灯片
function handleItemClick() {
if (injectCarouselScope && injectCarouselScope.type === 'card') {
const index = injectCarouselScope.items.value
.map(d => d.uid)
.indexOf(instance.uid)
// 调用父组件的setActiveItem方法
injectCarouselScope.setActiveItem(index)
}
}
// lifecycle
onMounted(() => {
// 调用父组件的addItem方式进行注册
if (injectCarouselScope.addItem) {
injectCarouselScope.addItem({
uid: instance.uid,
...props,
...toRefs(data),
translateItem,
})
}
})
onUnmounted(() => {
// 取消注册
if (injectCarouselScope.removeItem) {
injectCarouselScope.removeItem(instance.uid)
}
})
return {
data,
itemStyle,
translateItem,
type: injectCarouselScope.type,
handleItemClick,
}
},
}
2.3 总结
- provide/inject进行父子组件间数据交互;
- 父组件负责维护activeIndex,提供setActiveItem方法设置激活的幻灯片;
- 子组件提供translateItem方法,根据自身index和activeIndex,包括父组件的宽/高,计算出偏移位置;
- 走马灯的样式原理是:所有幻灯片平铺/垂直展示,通过translateX/Y调整每个幻灯片的偏移距离