Vue實現內部組件輪播切換效果

對於那些不須要路由的內部組件,在切換的時候但願增長一個輪播過渡的效果,效果以下:javascript


咱們能夠引入一個輪播組件,可是有個問題,一般輪播組件都會把全部的slide都渲染出來再進行切換,這樣就致使全部的資源都會觸發加載,這可能不是咱們所期待的,畢竟若是slide比較多的狀況須要一次性加載的圖片等資源太多了。因此咱們能夠手動簡單地寫一個,知足需求便可。css

如今一步步來實現這個功能,先寫一個實現基本切換的demo.html

1. 實現切換

先用vue-cli搭建一個工程腳手架,使用如下命令:前端

npm install -g vue-cli
vue init webpack slide-demo # 運行後router等都選擇no複製代碼

這樣就搭了一個webpack + vue的工程,進入slide-demo目錄,查看src/App.vue,這個文件是初始化工具提供的,是整個頁面的組件。還有一個src/components目錄,這個是放子組件的目錄。vue

在這個目錄裏面新建3個組件:task-1.vue、task-2.vue、task-3.vue,而後在App.vue裏面import進來,以下App.vue所示:java

<script> // import HelloWorld from './components/HelloWorld' import Task1 from "./components/task-1"; import Task2 from "./components/task-2"; import Task3 from "./components/task-3"; export default { name: 'App', components: { Task1, Task2, Task3 } } </script>複製代碼

咱們的數據格式questions是這樣的:webpack

[{index: 1, type: 1, content: ''}, {index: 2, type: 1, content: ''}, 
 {index: 3, type: 2, content: ''}, {index: 4, type: 3, content: ''}]複製代碼

它是一個數組,數組裏的每一個元素表明每道題,每道題都有一個類型,如選擇題、填空題、判斷題等,分別對應上面的task-一、task-二、task-3,咱們用一個currentIndex變量表示當前是在哪道題,初始化爲0,以下代碼所示(添加到App.vue裏面):git

data() {
        return {
            currentIndex: 0
        };
    },
    created() {
        // 請求question數據
        this.questions = [
            {index: 1, type: 1, question: ''}, /*...*/];
    },複製代碼

經過改變currentIndex的值,從而切到一下題即下一個組件,要怎麼實現這個切換的效果呢?github

可使用Vue自定義的一個全局組件component,給合它的is屬性,達到動態改變組件的目的,以下代碼所示:web

<template> <div id="app"> <div class="task-container"> <component :is="'task-' + questions[currentIndex].type" </component> </div> </div> </template>複製代碼

當currentIndex增長時,就會改變:is裏面的值,依次從task-1變到task-二、task-3等,這樣component就會換成相應的task組件。

接着,再添加一個切換到下一題的按鈕,在這個按鈕的響應函數裏面改變currentIndex的值。同時把question的數據傳給component:

<template> <div id="app"> <div class="task-container"> <component :is="'task-' + questions[currentIndex].type" :question="questions[currentIndex]"></component> <button class="next-question" @click="nextQuestion">下一題</button> </div> </div> </template>複製代碼

響應函數nextQuestion實現以下:

methods: {
    nextQuestion() {
        this.currentIndex = (this.currentIndex + 1) 
               % this.questions.length;
    }
},複製代碼

具體每一個task的實現參考如task-1.vue示例:

<template>
<section>
    <h2>{{question.index}}. 選擇題</h2>
    <div>{{content}}</div>
</section>
</template>
<script>
export default {
    props: ["question"]
}
</script>複製代碼

最後的效果以下(加上題目內容):


2. 添加輪播切換效果

輪播切換一般是把全部的slide都拼起來,拼成一張長長的橫圖,而後改變這個橫圖在顯示容器裏面的位置,如老牌jQuery插件flipsnap.js,它是把全部的slide都float: left,造成一張長圖,而後改變這張長圖的translate值,達到切換的目的。這個插件的缺點是沒有辦法從最後一張切回第一張,解決這個問題的方法之一是不斷地移動DOM:每次切的時候都把第一張移到最後一張的後面,這樣就實現了最後一張點下一張的時候回到第一張的目的,可是這樣移來移去地對性能消耗比較大,不是很優雅。另一個輪播插件jssor slider,它也是把全部的slide都渲染出來,而後每次切換的時候都動態地計算每張slide的translate的值,而不是總體長圖的位置,這樣就不用移動DOM節點,相對較爲優雅。還有不少Vue的輪播插件的實現也是相似上面提到的方式。

無論怎麼樣,上面的輪播模式都不太適用於咱們的場景,其中一個是這種答題的場景不須要切回上一題,每道題作完就不能回去了,更重要的一個是咱們不但願一次性把全部的slide都渲染出來,這樣會致使每張幻燈片裏的資源都觸發加載,就好比img標籤雖然你把它display: none了,可是隻要它的src是一個正常的url,它就會請求加載。 因爲slide每每會比較多,就不使用這種輪播插件了。

還可使用Vue自帶的transition,可是transition的問題是,切下一個的時候,上一個不見了,由於被銷燬了,只有下一個的動畫,而且不能預加載下一個slide的資源。

因此咱們手動實現一個。

個人想法是每次都準備兩個slide,第1個slide是當前展現用的,第2個slide拼在它的後面,準備切過來,當第2個slide切過來以後,刪掉第1個slide,而後在第2個的後面再接第3個slide,不斷地重複這個過程。若是咱們沒有使用Vue,而是本身增刪DOM,那麼沒什麼問題,能夠很任性地本身發揮。使用Vue能夠怎麼優雅地實現這個功能呢

在上面一個component的基礎上,再添加一個component,剛開始第1個component是當前展現的,而第2個component是拼在它右邊的,當第2個切過去以後,就把第1個移到第2的後面,同時把內容改爲第3個slide的內容,依此類推。使用Vue不太好動態地改DOM,可是能夠藉助jssor slider的思想,不移動DOM,只是改變component的translate的值。

給其中一個component套一個next-task的類,具備這個類的組件就表示它是下一張要出現的,它須要translateX(100%),以下代碼所示:

<template> <div id="app"> <div class="task-container"> <component :is="'task-' + questions[currentIndex].type" ></component> <component :is="'task-' + questions[currentIndex + 1].type" class="next-task"></component> </div> </div> </template> <style> .next-task { display: none; transform: translateX(100%); /* 添加一個動畫,當改變transform的值時就會觸發這個動畫 */ transition: transform 0.5s ease; } </style>複製代碼

上面代碼把具備.next-task類的component隱藏了,這樣是作個優化,由於display: none的元素只會構建DOM,不會進行layout和render渲染。

因此就把問題轉換成怎麼在這兩個component之間,切換next-task的類。一開始next-task是在第2個,當第2個切過來以後,next-task變成加在第1個上面,這樣輪流交替。

進而,發現一個規律,若是currentIndex是偶數話,如o、二、4…,那麼next-task是加在第2個component的,而若是currentIndex是奇數,則next-task是加在第1個component的。因此能夠根據currentIndex的奇偶性切換。

以下代碼所示:

<template> <div id="app"> <div class="task-container"> <component :is="'task-' + questions[evenIndex].type" :class="{'next-task': nextIndex === evenIndex}" ref="evenTask"></component> <component :is="'task-' + questions[oddIndex].type" :class="{'next-task': nextIndex === oddIndex} ref="oddTask"></component> </div> </div> </template> <script> export default { name: 'App', data() { return { currentIndex: 0, // 當前顯示的index nextIndex: 1, // 表示下一張index,值爲currentIndex + 1 evenIndex: 0, // 偶數的index,值爲currentIndex或者是currentIndex + 1 oddIndex: 1 // 奇數的index,值同上 }; }, }複製代碼

第1個component用來顯示偶數的slide,第2個是用來顯示奇數的slide(分別用一個evenIndex和oddIndex表明),若是nextIndex是偶數的,那麼偶數的component就會有一個next-task的類,反之則亦然。而後在下一題按鈕的響應函數裏面改變這幾個index的值:

methods: {
    nextQuestion() {
        this.currentIndex = (this.currentIndex + 1) 
            % this.questions.length;
        this._slideToNext();
    },
    // 切到下一步的動畫效果
    _slideToNext() {

    }
}複製代碼

nextQuestion函數可能還有其它一些處理,在它裏面調一下_slideToNext函數,這個函數的實現以下:

_slideToNext() {
    // 當前slide的類型(currentIndex已經加1了,這裏要反一下)
    let currentType = this.currentIndex % 2 ? "even" : "odd",
        // 下一個slide的類型
        nextType =  this.currentIndex % 2 ? "odd": "even";
    // 獲取下一個slide的dom元素
    let $nextSlide = this.$refs[`${nextType}Task`].$el;
    $nextSlide.style.display = "block";
    // 把下一個slide的translate值置爲0,本來是translateX(100%)
    $nextSlide.style.transform = "translateX(0)";
    // 等動畫結束後更新數據
    setTimeout(() => {
        this.nextIndex = (this.currentIndex + 1) 
            % this.questions.length;
        // 本來的next是當前顯示的slide
        this[`${nextType}Index`] = this.currentIndex;
        // 而本來的current slide要顯示下下張的內容了
        this[`${currentType}Index`] = this.nextIndex;
    }, 500);
}複製代碼

代碼把下一個slide的display改爲block,並把它的translateX的值置爲0,這個時候不能立刻更新數據觸發DOM更新,要等到下一個slide移過去的動畫結束以後再開始操做,因此加了一個setTimeout,在回調裏面調換nextTask的類,加到本來的current slide,並把它的內容置成下下張的內容。這些都是經過改變相應的index完成的。

這樣基本上就完成了,可是咱們發現一個問題,切是切過去了,就是沒有動畫效果。這個是由於從display: none變到display: block是沒有動畫的,要麼改爲visibility: hidden到visible,要麼觸發動畫的操做加到$nextTick或者setTimeout 0裏面,考慮到性能問題,這裏使用第二種方案:

$nextSlide.style.display = "block";
// 這裏使用setimeout,由於$nextTick有時候沒有動畫,非必現
setTimeout(() => {
    $nextSlide.style.transform = "translateX(0)";
    // ...
}, 0);複製代碼

通過這樣的處理以後,點下一題就有動畫了,可是又發現一個問題,就是偶數的next-task會被蓋住,由於偶數的是使用第一個component,它是會被第二個compoent蓋住的,因此須要給它加一個z-index:

.next-task { 
    display: none;
    transform: translateX(100%);
    transition: transform 0.5s ease;
    z-index: 2;
}複製代碼

這個問題還比較好處理,另一個不太好處理的問題是:動畫的時間是0.5s,若是用戶點下一題的速度很快在0.5s以內,上面的代碼執行就會有問題,會致使數據錯亂。若是每次切到下一題以後按鈕初始化都是disabled,由於當前題還沒答,只有答了才能變成可點狀態,能夠保證0.5s的時間是夠的,那麼就能夠不用考慮這種狀況。可是若是須要處理這種狀況呢?

3. 解決點擊過快的問題

我想到兩個方法,第一個方法是用一個sliding的變量標誌當前是不是在進行切換的動畫,若是是的話,點擊按鈕的時候就直接更新數據,同時把setTimeout 0.5s的計時器清掉。這個方法能夠解決數據錯亂的問題,可是切換的效果沒有了,或者是切換到一半的時候忽然就沒了,這樣體驗不是很好。

第二個方法是延後切換,即若是用戶點擊過快的時候,把這些操做排隊,等着一個個作切換的動畫。

咱們用一個數組表示隊列,若是當前已經在作滑動的動畫,則入隊暫不執行動畫,以下代碼所示:

methods: {
    nextQuestion() {
        this.currentIndex = (this.currentIndex + 1) 
            % this.questions.length;
        // 把currentIndex插到隊首
        this.slideQueue.unshift(this.currentIndex);
        // 若是當前沒有滑動,則執行滑動
        !this.sliding && this._slideToNext();
    },
}複製代碼

每次點擊按鈕都把待處理的currentIndex插到隊列裏面,若是當前已經在滑動了,則不馬上執行,不然執行滑動_slideToNext函數:

_slideToNext() {
    // 取出下一個要處理的元素
    let currentIndex = this.slideQueue.pop();
    // 下一個slide的類型
    let nextType =  currentIndex % 2 ? "odd" : "even";
    let $nextSlide = this.$refs[`${nextType}Task`].$el;
    $nextSlide.style.display = "block";
    setTimeout(() => {
        $nextSlide.style.transform = "translateX(0)";
        this.sliding = true;
        setTimeout(() => {
            this._updateData(currentIndex);
            // 若是當前還有未處理的元素,
            // 則繼續處理即繼續滑動
            if (this.slideQueue.length) {
                // 要等到兩個component的DOM更新了
                this.$nextTick(this._slideToNext);
            } else {
                this.sliding = false;
            }
        }, 500);
    }, 0);
},複製代碼

這個函數每次都先取出當前要處理的currentIndex,而後接下來的操做和第2點提到的同樣,只是在0.5s動畫結束後的異步回調裏面須要判斷一下,當前隊列是否還有未處理的元素,若是有的話,須要繼續執行_slideToNext,直到隊列空了。這個執行須要掛在nextTick裏面,由於須要等到兩個component的DOM更新了才能操做。

這樣理論上就沒問題了,但實際上仍是有問題,感覺以下:


咱們發現有些slide沒有過渡效果,並且不是非必現的,沒有規律。通過一番排查,發現若是把上面的nextTick改爲setTimeout狀況就會好一些,而且setTimeout的時間越長,就越不會出現失去過渡效果的狀況。可是這個不能從根本上解決問題,這裏的緣由應該是Vue的自動更新DOM和transition動畫不是很兼容,有多是Vue的異步機制問題,也有多是JS結合transition自己就有問題,但之前沒有遇到過,具體沒有深刻排查。無論怎麼樣,只能放棄使用CSS的transition作動畫。

若是有使用jQuery的話,可使用jQuery的animation,若是沒有的話,那麼可使用原生dom的animate函數,以下代碼所示:

_slideToNext(fast = false) {
    let currentIndex = this.slideQueue.pop();
    // 下一個slide的類型
    let nextType =  currentIndex % 2 ? "odd" : "even";
    // 獲取下一個slide的dom元素
    let $nextSlide = this.$refs[`${nextType}Task`].$el;
    $nextSlide.style.display = "block";
    this.sliding = true;
    // 使用原生animate函數
    $nextSlide.animate([
        // 關鍵幀
        {transform: "translateX(100%)"},
        {transform: "translateX(0)"}
    ], {
        duration: fast ? 200 : 500,
        iteration: 1,
        easing: "ease"
    // 返回一個Animate對象,它有一個onfinish的回調
    }).onfinish = () => {
        // 等動畫結束後更新數據
        this._updateData(currentIndex);
        if (this.slideQueue.length) {
            this.$nextTick(() => {
                this._slideToNext(true);
            });
        } else {
            this.sliding = false;
        }
    };
},複製代碼

使用animate函數達到了和transition一樣的效果,而且還有一個onfinish的動畫結束回調函數。上面代碼還作了一個優化,若是用戶點得很快的時候,縮短過渡動畫的時間,讓它切得更快一點,這樣看起來更天然一點。使用這樣的方式,就不會出現transition的問題了。最後的效果以下:


這個體驗感受已經比較流暢了。

原生animate不兼容IE/Edge/Safari,能夠裝一個polyfill的庫,如這個web-animation,或者使用其它一些第三方的動畫庫,或本身用setInterval寫一個。

若是你要加上一題的按鈕,支持返回上一題,那麼可能須要準備3個component,中間那個用於顯示,左右兩邊各跟着一個,準備隨時切過來。具體讀者能夠自行嘗試。

這種模式除了答題的場景,還有多封郵件預覽、PPT展現等均可以用到,它除了有一個過渡的效果外,還能提早預加載下一個slide須要的圖片、音頻、視頻等資源,而且不會像傳統的輪播插件那樣一會兒把全部的slide都渲染了。適用於slide比較多的狀況,不須要太複雜的切換動畫。


【號外】《高效前端》已上市,京東、亞馬遜、淘寶等均有售

相關文章
相關標籤/搜索