当前位置: 首页 > Web前端 > vue.js

vue插件动画优化:从stylebinding到scoped的深坑

时间:2023-03-31 21:11:17 vue.js

@keyframesshow-toast{0%{opacity:0;}25%{opacity:1;z-index:9999}50%{不透明度:1;z-index:9999}75%{不透明度:1;z-index:9999}100%{不透明度:0;z-index:0}}.toast{位置:固定;顶部:50%;左:50%;转换:翻译(-50%,-50%);z-指数:999;背景色:#000;不透明度:.7;颜色:#fff;框大小:边框框;最小高度:80px;填充:20px30px;行高:50px;最小宽度:364px;最大宽度:80%;边界半径:15px;字体大小:28px;文本对齐:居中;word-wrap:break-word;问题发现最近打算给团队里的公共插件做一些小动画效果,优化用户体验。这次先从最简单的toast插件说起。主要的文件有如下两个:index.jsimportToastfrom'./Toast.vue';const_TOAST={show:false,component:null};exportdefault{install(Vue){//添加实例方法Vue.prototype.$toast=(text,options={duration:2000})=>{if(_TOAST.show){return;}if(!_TOAST.component){让ToastComponent=Vue.extend(Toast);_TOAST.component=newToastComponent();让元素=_TOAST.component.$mount().$el;document.body.appendChild(元素);}_TOAST.component.duration=options.duration||2000;_TOAST.component.whiteSpace=options.whiteSpace||'继承';_TOAST.component.position=options.position||'中心';_TOAST.component.text=文本;_TOAST.component.show=_TOAST.show=true;setTimeout(()=>{_TOAST.component.show=_TOAST.show=假e;},options.duration);};Vue.prototype.$killToast=()=>{if(_TOAST.component){_TOAST.component.show=_TOAST.show=false;}};}};Toast.vue@keyframesshow-toast{0%{opacity:0;}25%{opacity:1;z-index:9999}50%{不透明度:1;z-index:9999}75%{不透明度:1;z-index:9999}100%{不透明度:0;z-index:0}}.toast{位置:固定;顶部:50%;左:50%;转换:翻译(-50%,-50%);z-指数:999;背景色:#000;不透明度:.7;颜色:#fff;框大小:边框框;最小高度:80px;填充:20px30px;行高:50px;最小宽度:364px;最大宽度:80%;边界半径:15px;字体大小:28px;文本对齐:居中;word-wrap:break-word;这都是最常见的插件写法,使用时importtoastformXXX导入index.js,还有Vue.use,直接在组件中使用this.$toast即可。再说动态效果。上面的Toast.vue代码中,默认在styleObject中写了一个动态效果show-toast,它的时长是根据时长计算的。上面代码的逻辑没有错,但是在实际运行中,看不到动态效果的效果。会不会是动画时间太快了?我用Chrome的Performance工具记录了整个toast出现时每一帧的渲染:可以看到,toast是直接出现的,并没有我们想要的过渡效果。那么,问题是什么?问题分析猜想一:transition和display冲突?因为v-show的本质是展示,参考周俊鹏《解决transition动画与display冲突的几种方法》,可能是因为浏览器的UI线程在处理UI操作的时候,在同一个tick中添加了多个css属性设置操作,所以造成了这样的情况:我们添加同时display=block的一个动画属性,这两个操作是同时执行的,所以我们得到了一个即时显示的效果。要验证这样的猜想其实很简单,只要将v-show改成v-if即可:{{text}}

别乱我们曲线救国,重绘view,直接从comment渲染成dom,绕过显示问题,问题就解决了吗?太年轻,太单纯。仍然没有动画。猜想二:StyleObject的计算问题下面我们通过断点的方式一步步看一下插件的渲染过程。我们发现插件的render函数是这样实现的:class中的宽高等样式可以正常渲染,但是style中的动态效果不起作用,所以是不是因为当渲染,一个是staticClass,一个是绑定的_vm.styleObject,一个是静态的,一个是动态的。是因为static可以生效吗?为了验证猜想,我们直接暴力改样式为static{{text}}
这时候插件的渲染流程就变成了这样:并且style中的animation属性也渲染到了dom上。这个问题解决了吗?有时天真。仍然没有动画。猜想3:风格和类的区别我花了很长时间去处理,连动画都没做,连个正常的对比都没有。于是我们用最原始暴力的方法,直接在类中添加这个show-toast动画,然后把styleObject去掉,看看能不能正常渲染:.toast{position:fixed;顶部:50%;左:50%;转换:翻译(-50%,-50%);z-指数:999;背景色:#000;不透明度:.7;颜色:#fff;框大小:边框框;最小高度:80px;填充:20px30px;行高:50px;最小宽度:364px;最大宽度:80%;边界半径:15px;字体大小:28px;文本对齐:居中;;动画:show-toast2s线性向前;}这次动画终于出现了!这时候我们在看吐司出现时每一帧的渲染图:可以清楚的看到有透明度的渐变效果。那你猜为什么2中的暴力风格不生效,而这里的暴力类会呢?下面对比一下渲染出来的风格:暴力风格:暴力类:仔细对比两者,终于找到问题的症结所在:show-toastshow-toast-data-v-19ed0bfa为什么这两个动画的名字不一样呢?那是因为作用域。在vue文件中的style标签上,有一个特殊的属性:scoped。当style标签带有scoped属性时,其CSS样式只能应用于当前组件,即该样式只能应用于当前组件元素。通过该属性可以防止组件之间的样式相互污染。vue中scoped属性的作用主要是通过PostCSS转译来实现的。添加scoped之后,我们的dom在编译之前看起来是这样的:固定的;}是这样编译的>.toast[data-v-19ed0bfa]{位置:固定;}PostCSS为一个组件中的所有dom添加一个唯一的动态属性,然后在CSS选择器中添加一个对应的属性选择器来选择组件中的dom。这种方法使得样式只适用于包含该属性的dom——组件的内部dom。所以问题的症结在于,通过scoped,将我们在