zoukankan      html  css  js  c++  java
  • Vue插件动效优化:从style绑定到scoped深坑

    问题发现

    最近准备对团队里公共的插件做一些小动效,优化用户体验。这次的先从最简单的toast插件入手。
    主要的文件有如下两个:
    index.js

    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.vue

    <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- 364px;
            max- 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使用。

    再来说一下动效的问题。上面的Toast.vue代码中,在styleObject里默认写了一个动效show-toast,并且根据duration计算他的动效时长。
    上面的代码逻辑上没有毛病,但是在实际运行时,看不出动效的效果。
    难道是动效时长太快了?我用Chrome的Performance工具录制了整个toast出现时每一帧的渲染情况:

    可以看到,toast是直接出现的,并没有一个我们想要的过渡动效

    那么,问题出在哪里了呢?

    豌豆资源搜索网站https://55wd.com 广州vi设计公司http://www.maiqicn.com

    问题分析

    猜想1:transition和display冲突?

    因为v-show的本质是display,参考周俊鹏大神的《解决transition动画与display冲突的几种方法》,会不会是因为,浏览器的UI线程在处理UI操作时,将多个css属性的set操作加入在同一个tick中处理,所以就造成了这样一种情况:
    我们在display=block的同时加入了一个animation属性,这两个操作被同时执行,所以得到了一个瞬间显示出来的效果。

    要验证这样的猜想其实很简单,只需要把v-show改成v-if:

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

    惹不起我们曲线救国总行吧,让视图重绘,从注释直接渲染成一个dom,绕过display的问题,这样问题是不是就解决了呢?

    too young too simple。
    动画还是依旧没有出现。

    猜想2:styleObject计算问题

    我们通过打断点的方式,一步一步看插件渲染流程。我们发现插件的render函数是这样实现的:

    在class中的样式,比如宽高等都能正常渲染,但是style中的动效就是不行,那么会不会是因为render的时候,一个是staicClass,一个是绑定的_vm.styleObject,一个是静态,一个是动态。难道是因为静态的才能生效?

    为了验证猜想,我们就直接暴力的把style改成静态的

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

    这时候插件渲染流程就变成了这样:

    而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- 364px;
            max- 80%;
            border-radius: 15px;
            font-size: 28px;
            text-align: center;
            word-wrap: break-word;
            animation: show-toast 2s linear forwards;
        }

    这次动画终于出现了!这时候我们在看看toast出现时每一帧的渲染情况:

    可以明显看出,有一个透明度的渐变效果。

    那么为什么猜想2里暴力style不生效,这里的暴力class就行呢?
    我们来比较一下渲染后的样式:

    暴力style:

    暴力class:

    仔细对比两者,终于发现了问题的症结:
    show-toast
    show-toast->这两个动画的名称为什么不一样呢?那是因为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 >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->真正导致动效不生效的原因,是因为我们在styleObject里写的动效名是show-toast,而不是编译后的show-toast->问题解决

    原因是找到了,但是问题还没有解决。
    如果我们直接暴力的在class里写动效,就像猜想3里做的那样,动效是能实现,但是我们怎么去动态更改动效时长呢?毕竟这个toast的插件是可以通过设置duration来改变他的展示时长的。

    首先,需要明确的是,scoped作用的是class中的名称,而不是属性,
    我们把最开始的那个带有动态styleObject的dom生成的样式拿出来看看:

    其实出问题的只是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,这样做后,样式就变成了:

    这样,我们再css里写的animation-duration就被styleObject里的正确覆盖了,这样就能实现动态修改动效时间的需求。

  • 相关阅读:
    边界扫描的测试原理及九大指令
    边界扫描(boundary scan)
    setup&hold
    使用InstallShield打包windriver驱动-转
    一个完整的Installshield安装程序实例-转
    高通Trustzone and QSEE介绍
    Linux ALSA声卡驱动之五:移动设备中的ALSA(ASoC)
    Linux驱动中completion接口浅析(wait_for_complete例子,很好)
    dmesg 的时间戳处理
    linux popen函数
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/13712366.html
Copyright © 2011-2022 走看看