以前一直都沒有認真的寫過一個組件。之前在寫業務代碼的過程當中,都是用的別人封裝好的組件,此次嘗試着寫了一個圖片輪播組件,雖然比不上知名的輪播組件,但它的功能基本完整,並且在寫這個組件的過程當中,學的東西也不少,在這裏也給你們分享出來,若有疏漏,歡迎指正!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的優點來。不過,第一個組件算是寫完了,也費了一番力氣。
這是我在掘金上的第四篇文章,感謝閱讀!