以前一直都没有认真的写过一个组件。之前在写业务代码的过程当中,都是用的别人封装好的组件,此次尝试着写了一个图片轮播组件,虽然比不上知名的轮播组件,但它的功能基本完整,并且在写这个组件的过程当中,学的东西也不少,在这里也给你们分享出来,若有疏漏,欢迎指正!css
在制做这个组件以前,笔者google了很多关于轮播的文章,发现实现一个轮播的思路虽然各有不一样,可是大的逻辑其实差很少,本文主要依据慕课网上焦点轮播图特效这节课,不过慕课网主要用原生JS写,而笔者则用Vue进行了重构,而且进行了一点修改。完成后的组件效果图以下:html
咱们先看下原理图:vue
图中红线区域便是咱们看到的图片,这个轮播只展现5张图片,可是在它的首尾各还有两张图片,在图1前面放置了图5,在图5后面放置了图1,之因此这么作,是为了作无限滚动。无限滚动的原理在于:当整个图向左侧滚动到右边的图5时,会继续向前走到图1,在彻底显示出图1后,会以肉眼看不到的速度向右侧拉回到最左边的图1。 这样,即便再向左侧滑动看到的就是图2了。git
以下图:在最后的图1完成过渡彻底显示出来后,再将整个列表瞬间向右拉到左侧的图1。另外一张边界图图5的滚动也是,不过方向相反。github
<template>
<div id="slider">
<div class="window"> // window上图中红线框
<ul class="container" :style="containerStyle"> //注意这里的:style //这是图片列表,排成一排
<li> //列表最前面的辅助图,它和图5同样,用于无限滚动
<img :src="sliders[sliders.length - 1].img" alt="">
</li>
<li v-for="(item, index) in sliders" :key="index"> //经过v-for渲染的须要展现的5张图
<img :src="item.img" alt="">
</li>
<li> //列表最后面的辅助图,它和图1同样,用于无限滚动
<img :src="sliders[0].img" alt="">
</li>
</ul>
<ul class="direction"> //两侧的箭头
<li class="left">
<svg class="icon" width="30px" height="30.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l337.373-337.373c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0l-360 360c-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z" /></svg>
</li>
<li class="right">
<svg class="icon" width="30px" height="30.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.497-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255l-360 360c-6.249 6.249-14.439 9.373-22.628 9.373z" /></svg>
</li>
</ul>
<ul class="dots"> //下面的小圆点
<li v-for="(dot, i) in sliders" :key="i"
:class="{dotted: i === (currentIndex-1)}"
>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'slider',
data () {
return {
sliders:[
{
img:'../../static/images/1.jpg'
},
{
img:'../../static/images/2.jpg'
},
{
img:'../../static/images/3.jpg'
},
{
img:'../../static/images/4.jpg'
},
{
img:'../../static/images/5.jpg'
}
],
currentIndex:1,
distance:-600
}
},
computed:{
containerStyle() { //这里用了计算属性,用transform来移动整个图片列表
return {
transform:`translate3d(${this.distance}px, 0, 0)`
}
}
}
}
</script>
复制代码
好了,布局大概就是这样,效果图以下: web
上面的代码已经作了注释,有几个点在这里再提一下:chrome
:style="containerStyle"
,这是一个计算属性,用transform:translate3d(${this.distance, 0, 0})
来控制左右移动distance
和currentIndex
是关键,distance
控制着移动的距离,默认是-600,显示7张图片中的第二张,也就是图1。currentIndex
是window显示的图片的索引,这里默认是1,也是7张图片中第2张。distance
会愈来愈小;当点击左侧的箭头,container向右移动,distance
会愈来愈大,方向不要弄错咱们在左侧和右侧的箭头上添加点击事件:浏览器
<ul class="direction">
<li class="left" @click="move(600, 1)">
<svg class="icon" width="30px" height="30.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l337.373-337.373c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0l-360 360c-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z" /></svg>
</li>
<li class="right" @click="move(600, -1)">
<svg class="icon" width="30px" height="30.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.497-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255l-360 360c-6.249 6.249-14.439 9.373-22.628 9.373z" /></svg>
</li>
</ul>
......
methods:{
move(offset, direction) {
this.distance += this.distance * direction
if (this.distance < -3000) this.distance = -600
if (this.distance > -600) this.distance = -3000
}
}
复制代码
解释下上面的代码:点击左侧或者右侧的箭头,调用move函数,move接收偏移量offset和方向direction两个参数。direction只传两个值,1表示container向右移动,-1表示container向左移动;偏移量是600,也就是一张图片的宽度。若是移动到7张图片的最后一张,就把container拉到7张图片里的第二张;若是移动到7张图片里第一张,就把container拉到7张图片里的第5张。bash
效果:异步
能够看到,图片切换效果已经出来了,可是下面的小圆点没有跟着变换。接下来咱们把这个效果加上。从上面的html代码能够看到,:class="{dotted: i === (currentIndex - 1)}"
,小圆点的切换效果和data里的currentIndex值相关,咱们只要随着图片切换变更currentIndex值就能够了。
修改move方法里的代码:
......
move(offset, direction) {
direction === -1 ? this.currentIndex++ : this.currentIndex--
if (this.currentIndex > 5) this.currentIndex = 1
if (this.currentIndex < 1) this.currentIndex = 5
this.distance = this.distance + offset * direction
if (this.distance < -3000) this.distance = -600
if (this.distance > -600) this.distance = -3000
}
复制代码
上面的添加的三行代码很好理解,若是是点击右侧箭头,container就是向左移动,this.currentIndex
就是减1,反之就是加1。
效果:
能够看到,小圆点的切换效果已经出来了。
上面的代码已经实现了切换,可是没有动画效果,显的很是生硬,接下来就是给每一个图片的切换过程添加过渡效果。
这个轮播组件笔者并无使用Vue自带的class钩子,也没有直接使用css的transition属性,而是用慕课网原做者讲的setTimeout方法加递归来实现。
其实我也试过使用Vue的钩子,可是总有一些小问题解决不掉;好比下面找到的这个例子:例子
这个例子在过渡的边界上有一些问题,我也遇到了,并且仍是时有时无。而若是使用css的transition过渡方法,在处理边界的无限滚动上总会在chrome浏览器上有一下闪动,即便添加了-webkit-transform-style:preserve-3d;
和-webkit-backface-visibility:hidden
也仍是没用,并且要配合transition的transitionend
事件对于IE浏览器的支持也不怎么好。
若是你们有看到更好的办法,请在评论中留言哦~
下面咱们来写这个过渡效果,主要是改写:
methods:{
move(offset, direction) {
direction === -1 ? this.currentIndex++ : this.currentIndex--
if (this.currentIndex > 5) this.currentIndex = 1
if (this.currentIndex < 1) this.currentIndex = 5
const destination = this.distance + offset * direction
this.animate(destination, direction)
},
animate(des, direc) {
if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
this.distance += 30 * direc
window.setTimeout(() => {
this.animate(des, direc)
}, 20)
} else {
this.distance = des
if (des < -3000) this.distance = -600
if (des > -600) this.distance = -3000
}
}
}
复制代码
上面的代码是这个轮播我以为最麻烦、也是最难理解的地方。
来理解一下:首先,咱们对于move方法进行了改写,由于要一点点的移动,因此要先算出要移动到的目标距离。而后,咱们写一个animate函数来实现这个过渡。这个animate函数接收两个参数,一个是要移动到的距离,另外一个是方向。若是咱们点击了右侧的箭头,container要向左侧移动,要是没有移动到目标距离,就在this.distance
减去必定的距离,若是减去后仍是没有到达,在20毫米之后再调用这个this.animate
,如此不断移动,就造成了过渡效果。而若是移动到了目标距离,那就将目标距离赋值给this.distance
,而后再进行边界和无限滚动的判断。
固然,使用window.setInterval()
也能够实现这个效果,并且会稍微好理解一点,由于没有用到递归:
methods:{
move(offset, direction) {
direction === -1 ? this.currentIndex++ : this.currentIndex--
if (this.currentIndex > 5) this.currentIndex = 1
if (this.currentIndex < 1) this.currentIndex = 5
const destination = this.distance + offset * direction
this.animate(destination, direction)
},
animate(des, direc) {
const temp = window.setInterval(() => {
if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
this.distance += 30 * direc
} else {
window.clearInterval(temp)
this.distance = des
if (des < -3000) this.distance = -600
if (des > -600) this.distance = -3000
}
}, 20)
}
}
复制代码
实现出来的效果以下:
写到这里,效果是出来了,可是会有一点问题,若是屡次快速点击,就会有可能出现下面这种状况:
出现这种状况的缘由很简单,由于是使用定时器过渡,因此连续快速点击就会出现错乱,简单节流一下就行了:在过渡完成以前点击箭头无效,其实就是设了一个闸,第一次点击把闸打开,在闸再次打开以前,让一部分代码没法执行,而后再在恰当的时机把闸打开。
咱们把这个闸设在move函数里:
move(offset, direction) {
if (!this.transitionEnd) return //这里是闸
this.transitionEnd = false //开闸之后再把闸关上
direction === -1 ? this.currentIndex++ : this.currentIndex--
if (this.currentIndex > 5) this.currentIndex = 1
if (this.currentIndex < 1) this.currentIndex = 5
const destination = this.distance + offset * direction
this.animate(destination, direction)
}
复制代码
this.transitionEnd
是这个闸的钥匙,咱们把它放到data里:
this.transitionEnd: true
复制代码
这个闸一开始默认的状态是开着的,第一次点击之后,这个闸就关上了,this.tranisitonEnd = false
,在再次打开以前,后面的代码都执行不了。接下来就是在恰当的时机把这个闸打开,而这个恰当的时机就是过渡完成时,也就是在animate函数
里:
animate(des, direc) {
if (this.temp) {
window.clearInterval(this.temp)
this.temp = null
}
this.temp = window.setInterval(() => {
if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
this.distance += 30 * direc
} else {
this.transitionEnd = true //闸再次打开
window.clearInterval(this.temp)
this.distance = des
if (des < -3000) this.distance = -600
if (des > -600) this.distance = -3000
}
}, 20)
}
复制代码
这下快速点击就没有以前的那个问题了:
到目前为止的代码:
<template>
<div id="slider">
<div class="window">
<ul class="container" :style="containerStyle">
<li>
<img :src="sliders[sliders.length - 1].img" alt="">
</li>
<li v-for="(item, index) in sliders" :key="index">
<img :src="item.img" alt="">
</li>
<li>
<img :src="sliders[0].img" alt="">
</li>
</ul>
<ul class="direction">
<li class="left" @click="move(600, 1)">
<svg class="icon" width="30px" height="30.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l337.373-337.373c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0l-360 360c-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z" /></svg>
</li>
<li class="right" @click="move(600, -1)">
<svg class="icon" width="30px" height="30.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.497-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255l-360 360c-6.249 6.249-14.439 9.373-22.628 9.373z" /></svg>
</li>
</ul>
<ul class="dots">
<li v-for="(dot, i) in sliders" :key="i"
:class="{dotted: i === (currentIndex-1)}"
>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'slider',
data () {
return {
sliders:[
{
img:'../../static/images/1.jpg'
},
{
img:'../../static/images/2.jpg'
},
{
img:'../../static/images/3.jpg'
},
{
img:'../../static/images/4.jpg'
},
{
img:'../../static/images/5.jpg'
}
],
currentIndex:1,
distance:-600,
transitionEnd: true
}
},
computed:{
containerStyle() {
return {
transform:`translate3d(${this.distance}px, 0, 0)`
}
}
},
methods:{
move(offset, direction) {
if (!this.transitionEnd) return
this.transitionEnd = false
direction === -1 ? this.currentIndex++ : this.currentIndex--
if (this.currentIndex > 5) this.currentIndex = 1
if (this.currentIndex < 1) this.currentIndex = 5
const destination = this.distance + offset * direction
this.animate(destination, direction)
},
animate(des, direc) {
if (this.temp) {
window.clearInterval(this.temp)
this.temp = null
}
this.temp = window.setInterval(() => {
if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
this.distance += 30 * direc
} else {
this.transitionEnd = true
window.clearInterval(this.temp)
this.distance = des
if (des < -3000) this.distance = -600
if (des > -600) this.distance = -3000
}
}, 20)
}
}
}
</script>
复制代码
接下来咱们要实现点击下面的小圆点来实现过渡和图片切换。
<ul class="dots">
<li v-for="(dot, i) in sliders" :key="i"
:class="{dotted: i === (currentIndex-1)}"
@click = jump(i+1)>
</li>
</ul>
复制代码
在点击小圆点的时候咱们调用jump
函数,并将索引i+1
传给它。这里须要特别注意,小圆点的索引和图片对应的索引不一致,图片共7张,而5个小圆点对应的是图片中中间的5张,因此咱们才传i+1
。
jump(index) {
const direction = index - this.currentIndex >= 0 ? -1 : 1 //获取滑动方向
const offset = Math.abs(index - this.currentIndex) * 600 //获取滑动距离
this.move(offset, direction)
}
复制代码
上面的代码有一个问题,在jump函数里调用move方法,move里对于currentIndex的都是+1
,而点击小圆点多是将currentIndex
加或者减好多个,因此要对move里的代码修改下:
direction === -1 ? this.currentIndex += offset/600 : this.currentIndex -= offset/600
复制代码
改一行,根据offset算出currentIndex就好了。
可是又有一个问题,长距离切换速度太慢,以下:
因此咱们须要控制一下速度,让滑动一张图片耗费的时间和滑动多张图片耗费的时间同样,给move和animate函数添加一个speed参数,还要再算一下:
jump(index) {
const direction = index - this.currentIndex >= 0 ? -1 : 1
const offset = Math.abs(index - this.currentIndex) * 600
const jumpSpeed = Math.abs(index - this.currentIndex) === 0 ? this.speed : Math.abs(index - this.currentIndex) * this.speed
this.move(offset, direction, jumpSpeed)
}
复制代码
前面的写的差很少了,到这里就很是简单了,写一个函数play:
play() {
if (this.timer) {
window.clearInterval(this.timer)
this.timer = null
}
this.timer = window.setInterval(() => {
this.move(600, -1, this.speed)
}, 4000)
}
复制代码
除了初始化之后自动播放,还要经过mouseover和mouseleave来控制暂停与播放:
stop() {
window.clearInterval(this.timer)
this.timer = null
}
复制代码
window.onblur
和window.onfocus
写到这里,基本功能都差很少了。可是若是把页面切换到别的页面,致使轮播图所在页面失焦,过一段时间再切回来会发现轮播狂转。缘由是页面失焦之后,setInterval中止运行,可是若是切回来就会一次性把该走的一次性走完。解决的方法也很简单,当页面失焦时中止轮播,页面聚焦时开始轮播。
window.onblur = function() { this.stop() }.bind(this)
window.onfocus = function() { this.play() }.bind(this)
复制代码
window.setInterval()
小坑当定时器window.setInterval()
在多个异步回调中使用时,就有可能在某种机率下开启多个执行队列,因此为了保险起见,不只应该在该清除时清除定时器,还要在每次使用以前也清除一遍。
props: {
initialSpeed: {
type: Number,
default: 30
},
initialInterval: {
type: Number,
default: 4
}
},
data() {
......
speed: this.initialSpeed
},
computed:{
interval() {
return this.initialInterval * 1000
}
}
复制代码
而后再在相应的地方修改下就能够了。
完整的代码以下:
<template>
<div id="slider">
<div class="window" @mouseover="stop" @mouseleave="play">
<ul class="container" :style="containerStyle">
<li>
<img :src="sliders[sliders.length - 1].img" alt="">
</li>
<li v-for="(item, index) in sliders" :key="index">
<img :src="item.img" alt="">
</li>
<li>
<img :src="sliders[0].img" alt="">
</li>
</ul>
<ul class="direction">
<li class="left" @click="move(600, 1, speed)">
<svg class="icon" width="30px" height="30.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l337.373-337.373c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0l-360 360c-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z" /></svg>
</li>
<li class="right" @click="move(600, -1, speed)">
<svg class="icon" width="30px" height="30.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.497-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255l-360 360c-6.249 6.249-14.439 9.373-22.628 9.373z" /></svg>
</li>
</ul>
<ul class="dots">
<li v-for="(dot, i) in sliders" :key="i"
:class="{dotted: i === (currentIndex-1)}"
@click = jump(i+1)
>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'slider',
props: {
initialSpeed: {
type: Number,
default: 30
},
initialInterval: {
type: Number,
default: 4
}
},
data () {
return {
sliders:[
{
img:'../../static/images/1.jpg'
},
{
img:'../../static/images/2.jpg'
},
{
img:'../../static/images/3.jpg'
},
{
img:'../../static/images/4.jpg'
},
{
img:'../../static/images/5.jpg'
}
],
currentIndex:1,
distance:-600,
transitionEnd: true,
speed: this.initialSpeed
}
},
computed:{
containerStyle() {
return {
transform:`translate3d(${this.distance}px, 0, 0)`
}
},
interval() {
return this.initialInterval * 1000
}
},
mounted() {
this.init()
},
methods:{
init() {
this.play()
window.onblur = function() { this.stop() }.bind(this)
window.onfocus = function() { this.play() }.bind(this)
},
move(offset, direction, speed) {
if (!this.transitionEnd) return
this.transitionEnd = false
direction === -1 ? this.currentIndex += offset/600 : this.currentIndex -= offset/600
if (this.currentIndex > 5) this.currentIndex = 1
if (this.currentIndex < 1) this.currentIndex = 5
const destination = this.distance + offset * direction
this.animate(destination, direction, speed)
},
animate(des, direc, speed) {
if (this.temp) {
window.clearInterval(this.temp)
this.temp = null
}
this.temp = window.setInterval(() => {
if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
this.distance += speed * direc
} else {
this.transitionEnd = true
window.clearInterval(this.temp)
this.distance = des
if (des < -3000) this.distance = -600
if (des > -600) this.distance = -3000
}
}, 20)
},
jump(index) {
const direction = index - this.currentIndex >= 0 ? -1 : 1
const offset = Math.abs(index - this.currentIndex) * 600
const jumpSpeed = Math.abs(index - this.currentIndex) === 0 ? this.speed : Math.abs(index - this.currentIndex) * this.speed
this.move(offset, direction, jumpSpeed)
},
play() {
if (this.timer) {
window.clearInterval(this.timer)
this.timer = null
}
this.timer = window.setInterval(() => {
this.move(600, -1, this.speed)
}, this.interval)
},
stop() {
window.clearInterval(this.timer)
this.timer = null
}
}
}
</script>
复制代码
大概写完了这个组件,发现其实还有许多地方能够优化,this.distance
和this.currentIndex
耦合性很高,彻底能够经过计算属性连到一块儿。还有过渡方式,用定时器的方法仍是有些生硬,没有发挥出Vue的优点来。不过,第一个组件算是写完了,也费了一番力气。
这是我在掘金上的第四篇文章,感谢阅读!