在使用 Vue 之后,写了两个版本的轮播,一个版本利用 Vue 的动画很轻松地写出了无缝轮播,但是由于 flex 和 transform 一些错综复杂的关系,导致图片切换时图片间会有缝隙,强迫症受不了,于是写了第二个版本利用克隆 dom 的方式实现无缝轮播。简单总结两个版本轮播的实现思路。
第一个版本
这个版本是课程里实现轮播的思路,特点是充分利用 Vue 的动画,完全不操作 dom ,只操作数据。
组件基本结构
<x-slides>
<x-slides-item :index="0"><div class="img">1</div></x-slides-item>
<x-slides-item :index="1"><div class="img">2</div></x-slides-item>
<x-slides-item :index="2"><div class="img">3</div></x-slides-item>
<x-slides-item :index="3"><div class="img">4</div></x-slides-item>
</x-slides>
组件分为容器组件和单个 item 子组件,均带有一个插槽。
容器组件
容器组件的关键 css 属性:
overflow: hidden;
position: relative;
display: flex;
容器组件在 data 设置一个属性 current,item 子组件接受props :
props: {
index: {
type: Number,
required: true
}
}
由于 index 是组件切换的依赖,因此在使用时是必须传的。想过尝试在子组件的 data 中声明 index ,然后在父组件 mounted 钩子中遍历 this.$children 去设置每个子组件的 index ,这样就可以不传这个 props 了。但是这种做法并不好,首先,Vue 并不保证 mounted 钩子里子组件也一并挂载完毕,有可能会出现某个子组件未挂载完毕而没有被设置 index 的问题,要确保每个子组件都被遍历到,需要使用 nextTick 钩子;其次 this.$children 也不保证顺序,可能会出现图片顺序混乱的问题。
PS:另一种使用 nextTick 钩子的方式:
通常我们使用 nextTick 是这样,提供一个回调函数,比如在 mounted 中使用:
mounted() {
this.$nextTick(() => {
doSomething()
})
}
根据官方文档,2.1.0 起,如果没有提供回调, nextTick 将返回一个 promise ,因此上述代码也可以这样写:
async mounted() {
await this.$nextTick()
doSomething()
}
子组件
子组件模板:
<transition name="slide">
<div class="x-slides-item" v-show="index===current" :class="{reverse}">
<slot></slot>
</div>
</transition>
简单的通过 index===current 来个控制子组件的显示, index 是子组件接受的 props ,current 是子组件通过计算属性映射的容器组件的 current :
computed: {
current() {
return this.$parent.current
}
}
这样,容器通过改变 current 的值,就可以控制子组件的切换。
动画效果
此版本轮播主要就在于切换时的动画效果。
首先给子组件加上过渡效果,Vue 过渡类名对应 css 如下:
.slide-leave-active {
position: absolute;
top: 0;
left: 0;
}
.slide-enter {
transform: translateX(100%);
}
.slide-leave-to {
transform: translateX(-100%);
}
.slide-enter.reverse {
transform: translateX(-100%);
}
.slide-leave-to.reverse {
transform: translateX(100%);
}
由于绝对定位会让元素脱离文档流,如果所有子组件根元素都使用绝对定位,那么容器组件将出现高度塌陷的问题,于是采用只让正在离开的子组件绝对定位的方式来保证容器组件的高度。
切换方向改变
通过在子组件中使用 watch ,可以判断切换的方向,从而控制动画的方向
watch: {
current(val, oldVal) {
if (val - oldVal > 0) {
this.reverse = false;
}
if (val - oldVal < 0) {
this.reverse = true;
}
//最后一个到第一个
if (val - oldVal === -(this.len - 1)) {
this.reverse = false;
}
//第一个到最后一个
if (val - oldVal === this.len - 1) {
this.reverse = true;
}
}
}
当切换方向改变时,子组件 reverse 类名激活,其切换动画也会相应改变。
这种方式实现轮播非常巧妙,契合 Vue 数据驱动的思想,无任何 dom 操作。缺点是图片切换时会有间隙,在尝试多种方式无果后,采用克隆 dom 的方式实现了第二个版本的轮播。
第二个版本
组件基本结构
第二个版本的轮播结构更为简单
<x-slides>
<div class="img">1</div>
<div class="img">2</div>
<div class="img">3</div>
<div class="img">4</div>
</x-slides>
组件内部会在挂载完毕之后,克隆第一张图片放在最后一张图片后,克隆最后一张图片放在第一张图片前。
克隆操作的代码:
cloneDom() {
let nodes = this.$slots.default.filter(node => node.elm.nodeType !== 3)
nodes.forEach(node => {
node.elm.style['flex-shrink'] = 0
})
this.length=nodes.length
const first = nodes[0].elm.cloneNode(true)
const last = nodes[nodes.length - 1].elm.cloneNode(true)
this.$refs.view.prepend(last)
this.$refs.view.append(first)
}
此处两小坑,一是 Vue 单文件组件自动取消空格,因此在单文件组件中遍历插槽,其子元素个数和插入的图片数量一致,然而如果是在 HTML 文件中使用组件,插槽中将出现文本节点,因此在克隆操作前需要根据 nodeType 属性来过滤掉文本节点;二是组件使用 flex 布局,会出现压缩子元素的情况,因此在需设置子元素的 flex-shrink 为 0。
此版本轮播的切换原理为通过 translateX 属性的改变来实现,并且在第一张到最后一张和最后一张到第一张采用动画效果结束立即更换图片的方式实现,具体原理之前已经用原生 JS 实现过,不再赘述。
网友评论