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里的正确覆盖了,这样就能实现动态修改动效时间的需求。

相关文章
相关标签/搜索