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

Vue原理解析(十二):别让Transition-Animation成为短板Transition组件实现原理

时间:2023-03-31 14:42:13 vue.js

Animation一直是前端的纠结点,容易被忽视却又如此重要,能写得愉快又naturalVue的交互体验确实可以为项目增色不少。毕竟一开始就能感受到,所以还是有必要弄清楚Vue的transition组件的实现原理的。transition组件的动画实现分为两种,使用Css类名和JavaScripthooks,后面会依次介绍。Transition组件介绍这是一个抽象组件,也就是说组件渲染完成后,不会以任何Dom的形式出现,只是以槽的形式控制内部的子节点。它的作用是在合适的时候添加/删除Css类名或者执行JavaScripthooks来达到动画执行的目的。由于过渡到VNode是一个组件,所以在生成真正的Dom时,首先需要将其转换为一个VNode,然后通过它来将这个VNode转换为真正的Dom。那么我们先看看transition组件会变成什么样的VNode。exportconsttransitionProps={//transition组件接受的props属性出现:Boolean,//是否第一次渲染css:Boolean,//是否取消CSS动画模式:String,//选择in中的一个-outorout-intype:String,//displaydeclarationtomonitoranimationortransitionname:String,//defaultventerClass:String,//默认`${name}-enter`leaveClass:String,//默认`${name}-leave`enterToClass:String,//默认`${name}-enter-to`leaveToClass:String,//默认`${name}-leave-to`enterActiveClass:String,//默认`${name}-enter-active`leaveActiveClass:String,//默认`${name}-leave-active`appearClass:String,//输入appearActiveClass:String,//开始渲染appearToClass:String,//离开firstrenderduration:[Number,String,Object]//动画持续时间}exportdefault{name:'transition',props:transitionProps,abstract:true,//标记为抽象组件,不会参与vue内部父子组件构造关系render(h){//用render函数写的,终于知道为什么叫h了letchildren=this.$slots.default//获取默认槽中的节点if(!children){return}if(!children.length){return}if(children.length>1){...slotcanonlyhaveonechild}constmode=this.modeif(mode&&mode!=='in-out'&&mode!=='out-in'){...mode只能是in-out或者out-in}constchild=children[0]//子节点对应VNodeconstid=`__transition-${this._uid}-`child.key=child.key==null//给子节点的VNode添加key属性?child.isComment//注释节点?id+'comment':id+child.tag:isPrimitive(child.key)//原始值?(String(child.key).indexOf(id)===0?child.key:id+child.key):child.key(child.data||(child.data={})).transition=extractTransitionData(this)//核心!给子节点的transition属性赋props和hook函数,表示它是transition组件渲染的VNodereturnchild}}exportfunctionextractTransitionData(comp){//赋值函数constdata={}constoptions=comp.$optionsfor(constkeyinoptions.propsData){//transition组件接收到的propsdata[key]=comp[key]}constlisteners=options._parentListeners//transition组件上注册的hook方法for(constkeyinlisteners){data[key]=listeners[key]}returndata}通过上面的代码,我们知道transition组件主要做了两件事。首先在渲染子节点的VNode中添加key属性,然后在其data属性下添加一个transition属性,表示这是一个transition组件渲染的VNode,然后处理Css类名的实现原理在路径中创建真实Dom的过程中。我们先关注一下Css类名的实现方法的原理,既然已经得到了对应的VNode,那么现在就需要创建一个真正的Dom了。在路径过程中,将Dom上的style、css、attr等属性分成模块进行创建,这些模块都有自己的钩子函数,比如有created,update,insert函数,有的模块不一样,表示在某个时间段内做某事。transition也不例外,创建的hook会先被执行。我们知道transition组件分为进入状态和离开状态,先看进入状态:exportfunctionenter(vnode){//参数为组件槽中的VNodeconstel=vnode.elm//对应realnodeconstdata=resolveTransition(vnode.data.transition)//扩展属性//data包含传入的props和扩展的6个class属性if(isUndef(data)){//如果不是transition渲染的vnode,再见返回}。..}exportfunctionresolveTransition(def){//扩展属性constres={}extend(res,autoCssTransition(def.name||'v'))//类对象扩展为空对象resextend(res,def)//将def上的属性扩展到res对象上returnres}constautoCssTransition(name){//生成6个需要使用的类对象return{enterClass:`${name}-enter`,enterToClass:`${name}-enter-to`,enterActiveClass:`${name}-enter-active`,leaveClass:`${name}-leave`,leaveToClass:`${name}-leave-to`,leaveActiveClass:`${name}-leave-active`}})执行回车,先继续展开transition属性为6之后要用到的类名,我们往下看:exportfunctionenter(vnode){//参数为组件insertionVNodeinsidetheslot...const{//解构需要的参数enterClass,enterToClass,enterActiveClass,appearClass,appearActiveClass,appearToClass,css,type//...省略其他参数}=dataconstisAppear=!context._isMounted||!vnode.isRootInsert//_isMounted表示组件是否挂载//isRootInsert表示是否插入根节点if(isAppear&&!appear&&appear!==''){//如果没有配置appear属性,则为也是第一次渲染直接退出,没有动画return}conststartClass=isAppear&&appearClass//如果定义了appear并且有对应的appearClass?appearClass//执行定义的appearClass:enterClass//否则,执行enterClassconstactiveClass=isAppear&&appearActiveClass?appearActiveClass:enterActiveClassconsttoClass=isAppear&&appearToClass?appearToClass:enterToClass...}接下来就是取出props里面和extendedclass的值,以备后用再说appear的实现原理。如果没有挂载或者根节点插入,并且定义了appear属性,那么就使用appearClass来完全执行进入状态的功能,否则没有动画直接渲染。接下来是核心实现过程。exportfunctionenter(vnode){...constexpectsCSS=css!==false&&!isIE9//没有明确指示css动画不会被执行constcb=once(()=>{//定义将只执行一次cb函数,刚刚定义,不执行)//添加startClassaddTransitionClass(el,activeClass)//添加activeClassnextFrame(()=>{//对requestAnimationFrame的封装,下一帧浏览器渲染回调时执行removeTransitionClass(el,startClass)//移除startClassaddTransitionClass(el,toClass)//添加toClasswhenTransitionEnds(el,type,cb)//在浏览器过渡结束事件transitionend或animationend后执行cb,去掉toClass和activeClass})}}首先定义一个cb函数,被once函数包裹,它的功能是执行t他只在里面工作一次。当然这个cb只是定义了,并不会执行。接下来将startClass和activeClass同步添加到当前真实节点,也就是我们熟悉的v-enter和v-enter-active;然后在requestAnimationFrame中去掉startClass,添加toClass,也就是浏览器渲染的下一帧,添加toClass,也就是v-enter-to;最后执行whenTransitionEnds方法,该方法是监听浏览器的动画结束事件,即transitionend或animationend事件,表示v-enter-active中定义的动画或transition结束,执行cb上面定义结束后。移除此函数中的toClass和activeClass。不难发现enter态的主要作用是管理v-enter/v-enter-active/v-enter-to这三个类的增删改查,具体动画由用户定义.自然地,我们可以认为leave状态的作用就是管理其他三个类的增删改查。接下来只展示leave的核心代码:/removev-leave-active})addTransitionClass(el,leaveClass)//添加v-leaveaddTransitionClass(el,leaveActiveClass)//添加v-leave-activenextFrame(()=>{//浏览器的下一帧executesremoveTransitionClass(el,leaveClass)//移除v-leaveaddTransitionClass(el,leaveToClass)//添加v-leave-towhenTransitionEnds(el,type,cb)//在动画结束事件后执行cb函数})}那里源码中还有很多边界条件,比如transition包是一个抽象组件,执行enter的时候leave还没有执行,前面的enter没执行又执行enter等等。有兴趣的可以自行去看完整的源码实现。这里只分析核心实现原理。接下来,让我们看看JavaScript钩子是如何实现的。JavaScript钩子实现原理了解了Css类名方法的实现原理后,就不难理解JavaScript钩子的实现了。hook的实现也分为进入和离开两种状态,代码也在这两个函数中,只是忽略了前面介绍的css方法。下面我们从钩子实现的角度来看这两个状态函数。首先看enter:exportfunctionenter(vnode){if(isDef(el._leaveCb)){//如果进入enter时没有执行_leaveCb,则立即执行el._leaveCb.cancelled=true//执行标记of_leaveCbBitel._leaveCb()//cb._leaveCb执行后会变为null}//el._leaveCb是定义在离开状态的cb函数,也就是离开状态的回调函数//见cb的定义在下面输入您将知道如何致富。const{beforeEnter,enter,afterEnter,enterCancelled,duration...其他参数}=dataconstuserWantsControl=getHookArgumentsLength(enter)//传入enterhook//如果hook中enter函数的参数大于1,表示传入了done函数,表示用户要自己控制//这就是为什么在enter中动画结束后需要调用done函数的原因constcb=el._enterCb=once(()=>{//这里定义了el._enterCb函数,对应leave的是el._leaveCbif(cb.cancelled){//如果处于leave状态,enter状态的cb函数没有执行,执行enterCancelledhookenterCancelled&&enterCancelled(el)}else{afterEnter&&afterEnter(el)//否则正常执行afterEnterhook}el._enterCb=null//el._enterCb执行后为null...省略css逻辑相关})mergeVNodeHook(vnode,'insert',()=>{//将函数体插入到inserthook中,t路径中的模块Thehookexecutedaftercreated...enter&&enter(el,cb)//执行enterhook并传入cb,其中cb对应enterhook中的done函数})beforeEnter&&beforeEnter(el)nextFrame(()=>{if(!userWantsControl){//如果用户不想控制if(duration){//如果有指定的合法过渡时间参数setTimeout(cb,duration)//setTimeout后执行cb}else{whenTransitionEnds(el,type,cb)//在浏览器过渡结束后的事件之后执行}}})}上面的代码就是JavaScript钩子实现的原理,这里要注意它们的执行顺序:先执行beforeEnter钩子,因为这个是同步的,cb是刚刚定义的,insert是created之后执行的,nextFrame是浏览器的下一帧,异步执行插入到insert钩子的函数体中,也是同步的,但是created之后,执行里面的enter钩子。如果用户不想控制动画结束,执行nextFrame中的函数体。如果用户要控制,也就是调用done函数,直接直接执行cb函数。正常情况下会执行里面的afterEnterhook。leave状态下,只贴出核心代码,与enter对比。它们之间的区别不是很大:exportfunctionleave(vnode){const{beforeLeave,leave,afterLeave,duration...省略其他参数}=dataconstcb=once(()=>{afterLeave&&afterLeave(el)...})beforeLeave&&beforeLeave(el)nextFrame(()=>{if(!userWantsControl){//用户不想控制if(isValidDuration(duration)){setTimeout(cb,duration)}else{whenTransitionEnds(el,type,cb)}}})leave&&leave(el,cb)//这里用户想控制done的执行}leave状态的钩子的执行顺序是beforeLeave,leave,afterLeave。至此,transition内置组件的两种实现原理都分析完了。源代码中将考虑更多的边界条件。如果需要更全面的了解,还需要看源码。笔者看完这个过渡原理,有点失望。事实证明,它并没有使我成为动画大师。最重要的是css的动画知识,可见打好基础的重要性!最后以一个面试官可能会问的问题结束,因为我真的被问到了。面试官微笑着礼貌的问道:请解释一下transition组件的实现原理?回过头来:transition组件是一个不渲染任何Dom的抽象组件,主要是帮助我们更方便的编写动画。以槽的形式对单个内部子节点进行动画管理。在渲染阶段,会在子节点的虚拟Dom上挂载一个transition属性,表示它是一个被transition组件包裹的节点。在路径阶段,会执行过渡组件的内部钩子。钩子分为进入和离开两种状态。在包裹的子节点上使用v-if或v-show来切换状态。您可以使用Css或JavaScript挂钩。使用css时,类名会在enter/leave状态下进行增删改查。用户只需要编写类名对应的动画即可。如果使用JavaScripthooks,指定的函数也是依次执行的,这些函数也需要用户定义,组件只控制this的流程。下一篇:我正在努力写作中……点个赞或者关注一下,很容易找到~参考:Vue.js源码综合解析,分享一个作者写的组件库,说不定哪天会用过~↓一个你可能会用到的vue功能组件库,持续完善中...