Vue插件動效優化:從style綁定到scoped深坑

問題發現

最近準備對團隊裏公共的插件作一些小動效,優化用戶體驗。此次的先從最簡單的toast插件入手。
主要的文件有以下兩個:
index.jscss

import Toast from './Toast.vue';

const _TOAST = {
    show: false,
    component: null
};

export default {
    install(Vue) {
        // 添加實例方法
        Vue.prototype.$toast = (text, options = {duration: 2000}) => {
            if (_TOAST.show) {
                return;
            }
            if (!_TOAST.component) {
                let ToastComponent = Vue.extend(Toast);
                _TOAST.component = new ToastComponent();
                let element = _TOAST.component.$mount().$el;
                document.body.appendChild(element);
            }
            _TOAST.component.duration = options.duration || 2000;
            _TOAST.component.whiteSpace = options.whiteSpace || 'inherit';
            _TOAST.component.position = options.position || 'center';
            _TOAST.component.text = text;
            _TOAST.component.show = _TOAST.show = true;
            setTimeout(() => {
                _TOAST.component.show = _TOAST.show = false;
            }, options.duration);
        };
        Vue.prototype.$killToast = () => {
            if (_TOAST.component) {
                _TOAST.component.show = _TOAST.show = false;
            }
        };
    }
};

Toast.vuehtml

<template>
    <div v-show="show" class="toast" :style="styleObject">
        {{text}}
    </div>
</template>

<script>
    export default {
        name: 'Toast',
        data() {
            return {
                show: false,
                text: 'toast',
                // 默認顯示2s
                duration: 2000,
                // 默認換行
                whiteSpace: 'inherit',
                // 顯示的位置
                position: 'center'
            };
        },
        computed: {
            styleObject() {
                return {
                    webkitAnimation: 'show-toast ' + this.duration / 1000 + 's linear forwards',
                    animation: 'show-toast ' + this.duration / 1000 + 's linear forwards',
                    whiteSpace: this.whiteSpace,
                    // toast的位置
                    top: this.position === 'up' ? '15%' : this.position === 'bottom' ? '85%' : '50%'
                };
            }
        }
    };
</script>


<style scoped>
    @keyframes show-toast {
        0% {opacity: 0;}
        25% {opacity: 1; z-index: 9999}
        50% {opacity: 1; z-index: 9999}
        75% {opacity: 1; z-index: 9999}
        100% {opacity: 0; z-index: 0}
    }

    .toast {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        z-index: 999;
        background-color: #000;
        opacity: .7;
        color: #fff;
        box-sizing: border-box;
        min-height: 80px;
        padding: 20px 30px;
        line-height: 50px;
        min-width: 364px;
        max-width: 80%;
        border-radius: 15px;
        font-size: 28px;
        text-align: center;
        word-wrap: break-word;
    }
</style>

這都是最普通的插件寫法,使用的時候,improt toast form XXX 引入index.js,而且Vue.use一下,就能直接在組件中用this.$toast使用。vue

再來講一下動效的問題。上面的Toast.vue代碼中,在styleObject裏默認寫了一個動效show-toast,而且根據duration計算他的動效時長。
上面的代碼邏輯上沒有毛病,可是在實際運行時,看不出動效的效果。
難道是動效時長太快了?我用Chrome的Performance工具錄製了整個toast出現時每一幀的渲染狀況:
73555184a74669bc8058e37d7d44b594.jpg
能夠看到,toast是直接出現的,並無一個咱們想要的過渡動效web

那麼,問題出在哪裏了呢?
image瀏覽器

問題分析

猜測1:transition和display衝突?

由於v-show的本質是display,參考周俊鵬大神的《解決transition動畫與display衝突的幾種方法》,會不會是由於,瀏覽器的UI線程在處理UI操做時,將多個css屬性的set操做加入在同一個tick中處理,因此就形成了這樣一種狀況:
咱們在display=block的同時加入了一個animation屬性,這兩個操做被同時執行,因此獲得了一個瞬間顯示出來的效果。app

要驗證這樣的猜測其實很簡單,只須要把v-show改爲v-ifdom

<div v-if="show" class="toast" :style="styleObject">
      {{text}}
 </div>

惹不起咱們曲線救國總行吧,讓視圖重繪,從註釋直接渲染成一個dom,繞過display的問題,這樣問題是否是就解決了呢?函數

too young too simple。
動畫仍是依舊沒有出現。工具

猜測2:styleObject計算問題

咱們經過打斷點的方式,一步一步看插件渲染流程。咱們發現插件的render函數是這樣實現的:
image.png優化

在class中的樣式,好比寬高等都能正常渲染,可是style中的動效就是不行,那麼會不會是由於render的時候,一個是staicClass,一個是綁定的_vm.styleObject一個是靜態,一個是動態。難道是由於靜態的才能生效?

爲了驗證猜測,咱們就直接暴力的把style改爲靜態的

<div v-if="show" class="toast" style="animation: show-toast 10s linear forwards">
      {{text}}
 </div>

這時候插件渲染流程就變成了這樣:
image.png

而dom上也渲染出了style裏的animation屬性。這樣問題是否是就解決了呢?

sometimes naive。
動畫仍是依舊沒有出現。

猜測3:style和class區別處理

折騰了半天,連個動畫都沒有搞出來,連個正常的對照都沒有。因此咱們用最原始最暴力的方法,直接在class裏面加上這個show-toast動畫,而後去掉styleObject ,看看他能不能正常渲染:

.toast {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        z-index: 999;
        background-color: #000;
        opacity: .7;
        color: #fff;
        box-sizing: border-box;
        min-height: 80px;
        padding: 20px 30px;
        line-height: 50px;
        min-width: 364px;
        max-width: 80%;
        border-radius: 15px;
        font-size: 28px;
        text-align: center;
        word-wrap: break-word;
        animation: show-toast 2s linear forwards;
    }

此次動畫終於出現了!這時候咱們在看看toast出現時每一幀的渲染狀況:
image.png
能夠明顯看出,有一個透明度的漸變效果。

那麼爲何猜測2裏暴力style不生效,這裏的暴力class就行呢?
咱們來比較一下渲染後的樣式:

暴力style:
image.png
暴力class:
image.png

仔細對比二者,終於發現了問題的癥結:
show-toast
show-toast-data-v-19ed0bfa

這兩個動畫的名稱爲何不同呢?那是由於scoped的緣由。
vue文件中的style標籤上,有一個特殊的屬性:scoped。當一個style標籤擁有scoped屬性時,它的CSS樣式就只能做用於當前的組件,也就是說,該樣式只能適用於當前組件元素。經過該屬性,可使得組件之間的樣式不互相污染。

vue中的scoped屬性的效果主要經過PostCSS轉譯實現,在加上scoped後,咱們的dom在編譯前是這樣

<template>
    <div v-show="show" class="toast">
        {{text}}
    </div>
</template>

<style scoped>
    .toast {
        position: fixed;
    }
</style>

編譯後是這樣

<template>
    <div data-v-19ed0bfa class="toast" style="display: none;">
        請勾選受權信息
    </div>
</template>

<style>
    .toast[data-v-19ed0bfa] {
        position: fixed;
    }
</style>

PostCSS給一個組件中的全部dom添加了一個獨一無二的動態屬性,而後,給CSS選擇器額外添加一個對應的屬性選擇器來選擇該組件中dom,這種作法使得樣式只做用於含有該屬性的dom——組件內部dom

因此問題的癥結就在於,經過scoped的做用,咱們寫在<style>裏的動效名show-toast被編譯成了show-toast-data-v-19ed0bfa
真正致使動效不生效的緣由,是由於咱們在styleObject裏寫的動效名是show-toast,而不是編譯後的show-toast-data-v-19ed0bfa,動效名對不上,因此並無執行裏面的動畫。

問題解決

緣由是找到了,可是問題尚未解決。
若是咱們直接暴力的在class裏寫動效,就像猜測3裏作的那樣,動效是能實現,可是咱們怎麼去動態更改動效時長呢?畢竟這個toast的插件是能夠經過設置duration來改變他的展現時長的。

首先,須要明確的是,scoped做用的是class中的名稱,而不是屬性
咱們把最開始的那個帶有動態styleObjectdom生成的樣式拿出來看看:
image.png

其實出問題的只是animation-name這一個屬性,其餘的屬性由於style的優先級要高於class因此都能正確覆蓋.toast裏的屬性。例如這裏的top
其次,仔細分析一下styleObject裏的東西,其實只有同樣是動態的,那就是動效時長這個屬性,因而咱們能夠繞開animation-name,直接去修改animation-duration

computed: {
            styleObject() {
                return {
                    animationDuration: this.duration / 1000 + 's',
                    whiteSpace: this.whiteSpace,
                    // toast的位置
                    top: this.position === 'up' ? '15%' : this.position === 'bottom' ? '85%' : '50%'
                };
            }
        }

而後再像上面同樣在class裏寫animation-name,這樣作後,樣式就變成了:
image.png
這樣,咱們再css裏寫的animation-duration就被styleObject裏的正確覆蓋了,這樣就能實現動態修改動效時間的需求。

相關文章
相關標籤/搜索