在使用 Vue 之后,寫了兩個(gè)版本的輪播,一個(gè)版本利用 Vue 的動(dòng)畫很輕松地寫出了無(wú)縫輪播,但是由于 flex 和 transform 一些錯(cuò)綜復(fù)雜的關(guān)系,導(dǎo)致圖片切換時(shí)圖片間會(huì)有縫隙,強(qiáng)迫癥受不了,于是寫了第二個(gè)版本利用克隆 dom 的方式實(shí)現(xiàn)無(wú)縫輪播。簡(jiǎn)單總結(jié)兩個(gè)版本輪播的實(shí)現(xiàn)思路。
第一個(gè)版本
這個(gè)版本是課程里實(shí)現(xiàn)輪播的思路,特點(diǎn)是充分利用 Vue 的動(dòng)畫,完全不操作 dom ,只操作數(shù)據(jù)。
組件基本結(jié)構(gòu)
<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>
組件分為容器組件和單個(gè) item 子組件,均帶有一個(gè)插槽。
容器組件
容器組件的關(guān)鍵 css 屬性:
overflow: hidden;
position: relative;
display: flex;
容器組件在 data 設(shè)置一個(gè)屬性 current,item 子組件接受props :
props: {
index: {
type: Number,
required: true
}
}
由于 index 是組件切換的依賴,因此在使用時(shí)是必須傳的。想過(guò)嘗試在子組件的 data 中聲明 index ,然后在父組件 mounted 鉤子中遍歷 this.$children 去設(shè)置每個(gè)子組件的 index ,這樣就可以不傳這個(gè) props 了。但是這種做法并不好,首先,Vue 并不保證 mounted 鉤子里子組件也一并掛載完畢,有可能會(huì)出現(xiàn)某個(gè)子組件未掛載完畢而沒(méi)有被設(shè)置 index 的問(wèn)題,要確保每個(gè)子組件都被遍歷到,需要使用 nextTick 鉤子;其次 this.$children 也不保證順序,可能會(huì)出現(xiàn)圖片順序混亂的問(wèn)題。
PS:另一種使用 nextTick 鉤子的方式:
通常我們使用 nextTick 是這樣,提供一個(gè)回調(diào)函數(shù),比如在 mounted 中使用:
mounted() {
this.$nextTick(() => {
doSomething()
})
}
根據(jù)官方文檔,2.1.0 起,如果沒(méi)有提供回調(diào), nextTick 將返回一個(gè) 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>
簡(jiǎn)單的通過(guò) index===current 來(lái)個(gè)控制子組件的顯示, index 是子組件接受的 props ,current 是子組件通過(guò)計(jì)算屬性映射的容器組件的 current :
computed: {
current() {
return this.$parent.current
}
}
這樣,容器通過(guò)改變 current 的值,就可以控制子組件的切換。
動(dòng)畫效果
此版本輪播主要就在于切換時(shí)的動(dòng)畫效果。
首先給子組件加上過(guò)渡效果,Vue 過(guò)渡類名對(duì)應(yīng) 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%);
}
由于絕對(duì)定位會(huì)讓元素脫離文檔流,如果所有子組件根元素都使用絕對(duì)定位,那么容器組件將出現(xiàn)高度塌陷的問(wèn)題,于是采用只讓正在離開的子組件絕對(duì)定位的方式來(lái)保證容器組件的高度。
切換方向改變
通過(guò)在子組件中使用 watch ,可以判斷切換的方向,從而控制動(dòng)畫的方向
watch: {
current(val, oldVal) {
if (val - oldVal > 0) {
this.reverse = false;
}
if (val - oldVal < 0) {
this.reverse = true;
}
//最后一個(gè)到第一個(gè)
if (val - oldVal === -(this.len - 1)) {
this.reverse = false;
}
//第一個(gè)到最后一個(gè)
if (val - oldVal === this.len - 1) {
this.reverse = true;
}
}
}
當(dāng)切換方向改變時(shí),子組件 reverse 類名激活,其切換動(dòng)畫也會(huì)相應(yīng)改變。
這種方式實(shí)現(xiàn)輪播非常巧妙,契合 Vue 數(shù)據(jù)驅(qū)動(dòng)的思想,無(wú)任何 dom 操作。缺點(diǎn)是圖片切換時(shí)會(huì)有間隙,在嘗試多種方式無(wú)果后,采用克隆 dom 的方式實(shí)現(xiàn)了第二個(gè)版本的輪播。
第二個(gè)版本
組件基本結(jié)構(gòu)
第二個(gè)版本的輪播結(jié)構(gòu)更為簡(jiǎn)單
<x-slides>
<div class="img">1</div>
<div class="img">2</div>
<div class="img">3</div>
<div class="img">4</div>
</x-slides>
組件內(nèi)部會(huì)在掛載完畢之后,克隆第一張圖片放在最后一張圖片后,克隆最后一張圖片放在第一張圖片前。
克隆操作的代碼:
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 單文件組件自動(dòng)取消空格,因此在單文件組件中遍歷插槽,其子元素個(gè)數(shù)和插入的圖片數(shù)量一致,然而如果是在 HTML 文件中使用組件,插槽中將出現(xiàn)文本節(jié)點(diǎn),因此在克隆操作前需要根據(jù) nodeType 屬性來(lái)過(guò)濾掉文本節(jié)點(diǎn);二是組件使用 flex 布局,會(huì)出現(xiàn)壓縮子元素的情況,因此在需設(shè)置子元素的 flex-shrink 為 0。
此版本輪播的切換原理為通過(guò) translateX 屬性的改變來(lái)實(shí)現(xiàn),并且在第一張到最后一張和最后一張到第一張采用動(dòng)畫效果結(jié)束立即更換圖片的方式實(shí)現(xiàn),具體原理之前已經(jīng)用原生 JS 實(shí)現(xiàn)過(guò),不再贅述。