transition组件可以给任何元素和组件添加进入/离开过渡,但只能给单个组件实行过渡效果(多个元素可以用transition-group组件,下一节再讲),调用该内置组件时,可以传入如下特性:
name 用于自动生成CSS过渡类名 例如:name:'fade'将自动拓展为.fade-enter,.fade-enter-active等
appear 是否在初始渲染时使用过渡 默认为false
css 是否使用 CSS 过渡类。 默认为 true。如果设置为 false,将只通过组件事件触发注册的 JavaScript 钩子。
mode 控制离开/进入的过渡时间序列 可设为"out-in"或"in-out";默认同时生效
type 指定过渡事件类型 可设为transition或animation,用于侦听过渡何时结束;可以不设置,Vue内部会自动检测出持续时间长的为过渡事件类型
duration 定制进入和移出的持续时间 以后用到再看
type表示transition对应的css过渡类里的动画样式既可以用transition也可以用animation来设置动画(可以同时使用),然后我们可以用指定,Vue内部会自动判断出来
除了以上特性,我们还可以设置如下特性,用于指定过渡的样式:
appear-class 初次渲染时的起始状态 ;如果不存在则等于enter-class属性 这三个属性得设置了appear为true才生效
appear-to-class 初次渲染时的结束状态 如果不存在则等于enter-to-class 属性
appear-active-class 初次渲染时的过渡 如果不存在则等于enter-active-class属性
enter-class 进入过渡时的起始状态
enter-to-class 进入过渡时的结束状态
enter-active-class 进入过渡时的过渡
leave-class 离开过渡时的起始状态
leave-to-class 离开过渡时的结束状态
leave-active-class 离开过渡时的过渡
对于后面六个class,内部会根据name拼凑出对应的class来,例如一个transition的name="fade",拼凑出来的class名默认分别为:fade-enter、fade-enter-to、fade-enter-active、fade-leave、fade-leave-to、fade-leave-active
除此之外还可以在transition中绑定自定义事件,所有的自定义事件如下
before-appear 初次渲染,过渡前的事件 未指定则等于before-enter事件
appear 初次渲染开始时的事件 未指定则等于enter事件
after-appear 初次渲染,过渡结束后的事件 未指定则等于enter-cancelled事件
appear-cancelled 初次渲染未完成又触发隐藏条件而重新渲染时的事件,未指定则等于enter-cancelled事件
before-enter 进入过渡前的事件
enter 进入过渡时的事件
after-enter 进入过渡结束后的事件
enter-cancelled 进入过渡未完成又触发隐藏条件而重新渲染时的事件
before-leave 离开过渡前的事件
leave 离开时的事件
after-leave 离开后的事件
leave-cancelled 进入过渡未完成又触发隐藏条件而重新渲染时的事件
transition相关的所有属性应该都列出来了(应该比官网还多吧,我是从源码里找到的),我们举一个例子,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> </head> <style> .fade-enter,.fade-leave-to{background: #f00;transform:translateY(20px);} /*.fade-enter和.fade-leave-to一般写在一起,当然也可以分开*/ .fade-enter-active,.fade-leave-to{transition:all 1s linear 500ms;} </style> <body> <div id="app"> <button @click="show=!show">按钮</button> <transition name="fade" :appear="true" @before-enter="beforeenter" @enter="enter" @after-enter="afterenter" @before-leave="beforeleave" @leave="leave" @after-leave="afterleave"> <p v-if="show">你好</p> </transition> </div> <script> Vue.config.productionTip=false; Vue.config.devtools=false; var app = new Vue({ el:"#app", data:{ show:true }, methods:{ beforeenter(){console.log('进入过渡前的事件')}, enter(){console.log('进入过渡开始的事件')}, afterenter(){console.log('进入过渡结束的事件')}, beforeleave(){console.log('离开过渡前的事件')}, leave(){console.log('离开过渡开始的事件')}, afterleave(){console.log('离开过渡结束的事件')} } }) </script> </body> </html>
我们调用transition组件时设置了appear特性为true,这样页面加载时动画就开始了,如下:
控制台输出如下:
文字从透明到渐显,同时位移也发生了变化,我们点击按钮时又会触发隐藏,继续点击,又会显示,这是因为我们在transition的子节点里使用了v-show指令。
对于transition组件来说,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
条件渲染 (使用 v-if)
条件展示 (使用 v-show)
动态组件
组件根节点
用原生DOM模拟transition组件
Vue内部是通过修改transition子节点的class名来实现动画效果的,我们用原生DOM实现一下这个效果,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <style> .trans{transition:all 2s linear;} .start{transform:translatex(100px);opacity: 0;} </style> <body> <div id="con"> <button name="show">显式</button> <button name="hide">隐藏</button> </div> <p id="p">Hello Vue!</p> <script> var p = document.getElementsByTagName('p')[0]; document.getElementById('con').addEventListener('click',function(event){ switch(event.target.name){ case "show": p.style.display="block"; p.classList.add('trans'); p.classList.remove('start') break; case "hide": p.classList.add('trans') p.classList.add('start') break; } }) </script> </body> </html>
渲染的页面如下:
我们点击隐藏按钮后,Hello Vue!就逐渐隐藏了,然后我们查看DOM,如下:
这个DOM元素还是存在的,只是opacity这个透明度的属性为0,Vue内部的transition隐藏后是一个注释节点,这是怎么实现的,我们能不能也实现出来,当然可以。
Vue内部通过window.getComputedStyle()这个API接口获取到了transition或animation的结束时间,然后通过绑定transitionend或animationend事件(对应不同的动画结束事件)执行一个回调函数,该回调函数会将DOM节点设置为一个注释节点(隐藏节点的情况下)
我们继续改一下代码,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <style> .trans{transition:all 2s linear;} .start{transform:translatex(100px);opacity: 0;} </style> <body> <div id="con"> <button name="show">显式</button> <button name="hide">隐藏</button> </div> <p id="p">Hello Vue!</p> <script> var p = document.getElementsByTagName('p')[0], tid = null, pDom = null, CommentDom = document.createComment(""); document.getElementById('con').addEventListener('click',function(event){ switch(event.target.name){ case "show": CommentDom.parentNode.replaceChild(p,CommentDom) setTimeout(function(){p.classList.remove('start')},10) ModifyClass(1) break; case "hide": p.classList.add('trans') p.classList.add('start') ModifyClass(0) break; } }) function ModifyClass(n){ //s=1:显式过程 s=0:隐藏过程 var styles = window.getComputedStyle(p); var transitionDelays = styles['transitionDelay'].split(', '); //transition的延迟时间 ;比如:["0.5s"] var transitionDurations = styles['transitionDuration'].split(', '); //transition的动画持续时间 ;比如:"1s" var transitionTimeout = getTimeout(transitionDelays, transitionDurations); //transition的获取动画结束的时间,单位ms,比如:1500 tid && clearTimeout(tid); tid=setTimeout(function(){ if(n){ //如果是显式 p.classList.remove('trans') p.removeAttribute('class'); }else{ //如果是隐藏 p.parentNode.replaceChild(CommentDom,p); } },transitionTimeout) } function getTimeout(delays, durations) { //从Vue源码里拷贝出来的代码的,获取动画完成的总时间,返回ms格式 while (delays.length < durations.length) { delays = delays.concat(delays); } return Math.max.apply(null, durations.map(function (d, i) { return toMs(d) + toMs(delays[i]) })) } function toMs(s) { return Number(s.slice(0, -1)) * 1000 } </script> </body> </html>
这样当动画结束后改DOM就真的隐藏了,变为了一个注释节点,如下:
当再次点击时,就会显式出来,如下:
完美,这里遇到个问题,就是当显式的时候直接设置class不会有动画,应该是和重绘有关的吧m所以用了一个setTImeout()来实现。
Vue也就是把这些原生DOM操作进行了封装,我们现在来看Vue的源码
源码分析
transition是Vue的内置组件,在执行initGlobalAPI()时extend保存到Vue.options.component(第5052行),我们可以打印看看,如下:
Transition组件的格式为:
var Transition = { //第8012行 transition组件的定义 name: 'transition', props: transitionProps, abstract: true, render: function render (h) { /**/ } }
也就是说transition组件定义了自己的render函数。
以上面的第一个例子为例,执行到transition组件时会执行到它的render函数,如下:
render: function render (h) { //第8217行 transition组件的render函数,并没有template模板,初始化或更新都会执行到这里 var this$1 = this; var children = this.$slots.default; if (!children) { return } // filter out text nodes (possible whitespaces) children = children.filter(function (c) { return c.tag || isAsyncPlaceholder(c); }); /* istanbul ignore if */ if (!children.length) { //获取子节点 return //如果没有子节点,则直接返回 } // warn multiple elements if ("development" !== 'production' && children.length > 1) { //如果过滤掉空白节点后,children还是不存在,则直接返回 warn( '<transition> can only be used on a single element. Use ' + '<transition-group> for lists.', this.$parent ); } var mode = this.mode; //获取模式 // warn invalid mode if ("development" !== 'production' && mode && mode !== 'in-out' && mode !== 'out-in' //检查mode是否规范只能是in-out或out-in ) { warn( 'invalid <transition> mode: ' + mode, this.$parent ); } var rawChild = children[0]; //获取所有子节点 // if this is a component root node and the component's // parent container node also has transition, skip. if (hasParentTransition(this.$vnode)) { //如果当前的transition是根组件,且调用该组件的时候外层又套了一个transition return rawChild //则直接返回rawChild } // apply transition data to child // use getRealChild() to ignore abstract components e.g. keep-alive var child = getRealChild(rawChild); /* istanbul ignore if */ if (!child) { return rawChild } if (this._leaving) { return placeholder(h, rawChild) } // ensure a key that is unique to the vnode type and to this transition // component instance. This key will be used to remove pending leaving nodes // during entering. var id = "__transition-" + (this._uid) + "-"; //拼凑key,比如:__transition-1 ;this._uid是transition组件实例的_uid,在_init初始化时定义的 child.key = child.key == null ? child.isComment ? id + 'comment' : id + child.tag : isPrimitive(child.key) ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key) : child.key; var data = (child.data || (child.data = {})).transition = extractTransitionData(this); //获取组件上的props和自定义事件,保存到child.data.transition里 var oldRawChild = this._vnode; var oldChild = getRealChild(oldRawChild); // mark v-show // so that the transition module can hand over the control to the directive if (child.data.directives && child.data.directives.some(function (d) { return d.name === 'show'; })) { //如果child带有一个v-show指令 child.data.show = true; //则给child.data新增一个show属性,值为true } if ( oldChild && oldChild.data && !isSameChild(child, oldChild) && !isAsyncPlaceholder(oldChild) && // #6687 component root is a comment node !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment) //这里是更新组件,且子组件改变之后的逻辑 ) { // replace old child transition data with fresh one // important for dynamic transitions! var oldData = oldChild.data.transition = extend({}, data); // handle transition mode if (mode === 'out-in') { // return placeholder node and queue update when leave finishes this._leaving = true; mergeVNodeHook(oldData, 'afterLeave', function () { this$1._leaving = false; this$1.$forceUpdate(); }); return placeholder(h, rawChild) } else if (mode === 'in-out') { if (isAsyncPlaceholder(child)) { return oldRawChild } var delayedLeave; var performLeave = function () { delayedLeave(); }; mergeVNodeHook(data, 'afterEnter', performLeave); mergeVNodeHook(data, 'enterCancelled', performLeave); mergeVNodeHook(oldData, 'delayLeave', function (leave) { delayedLeave = leave; }); } } return rawChild //返回DOM节点 }
extractTransitionData()可以获取transition组件上的特性等,如下:
function extractTransitionData (comp) { //第8176行 提取在transition组件上定义的data var data = {}; var options = comp.$options; //获取comp组件的$options字段 // props for (var key in options.propsData) { //获取propsData data[key] = comp[key]; //并保存到data里面 ,例如:{appear: true,name: "fade"} } // events. // extract listeners and pass them directly to the transition methods var listeners = options._parentListeners; //获取在transition组件上定义的自定义事件 for (var key$1 in listeners) { //遍历自定义事件 data[camelize(key$1)] = listeners[key$1]; //也保存到data上面 } return data }
例子里的transition组件执行到返回的值如下:
也就是说transition返回的是子节点VNode,它只是在子节点VNode的data属性上增加了transition组件相关的信息
对于v-show指令来说,初次绑定时会执行bind函数(可以看https://www.cnblogs.com/greatdesert/p/11157771.html),如下:
var show = { //第8082行 bind: function bind (el, ref, vnode) { //初次绑定时执行 var value = ref.value; vnode = locateNode(vnode); var transition$$1 = vnode.data && vnode.data.transition; //尝试获取transition,如果v-show绑定的标签外层套了一个transition则会把信息保存到该对象里 var originalDisplay = el.__vOriginalDisplay = el.style.display === 'none' ? '' : el.style.display; //保存最初的display属性 if (value && transition$$1) { //如果transition$$1存在的话 vnode.data.show = true; enter(vnode, function () { //执行enter函数,参数2是个函数,是动画结束的回掉函数 el.style.display = originalDisplay; }); } else { el.style.display = value ? originalDisplay : 'none'; } },
最后会执行enter函数,enter函数也就是动画的入口函数,比较长,如下:
function enter (vnode, toggleDisplay) { //第7599行 进入动画的回调函数 var el = vnode.elm; // call leave callback now if (isDef(el._leaveCb)) { //如果el._leaveCb存在,则执行它,离开过渡未执行完时如果重新触发了进入过渡,则执行到这里 el._leaveCb.cancelled = true; el._leaveCb(); } var data = resolveTransition(vnode.data.transition); //调用resolveTransition解析vnode.data.transition里的css属性 if (isUndef(data)) { return } /* istanbul ignore if */ if (isDef(el._enterCb) || el.nodeType !== 1) { return } var css = data.css; //是否使用 CSS 过渡类 var type = data.type; //过滤类型,可以是transition或animation 可以为空,Vue内部会自动检测 var enterClass = data.enterClass; //获取进入过渡是的起始、结束和过渡时的状态对应的class var enterToClass = data.enterToClass; var enterActiveClass = data.enterActiveClass; var appearClass = data.appearClass; //获取初次渲染时的过渡,分别是起始、结束和过渡时的状态对应的class var appearToClass = data.appearToClass; var appearActiveClass = data.appearActiveClass; var beforeEnter = data.beforeEnter; //进入过渡前的事件,以下都是相关事件 var enter = data.enter; var afterEnter = data.afterEnter; var enterCancelled = data.enterCancelled; var beforeAppear = data.beforeAppear; var appear = data.appear; var afterAppear = data.afterAppear; var appearCancelled = data.appearCancelled; var duration = data.duration; // activeInstance will always be the <transition> component managing this // transition. One edge case to check is when the <transition> is placed // as the root node of a child component. In that case we need to check // <transition>'s parent for appear check. var context = activeInstance; //当前transition组件的Vue实例vm var transitionNode = activeInstance.$vnode; //占位符VNode while (transitionNode && transitionNode.parent) { //如果transitoin组件是作为根节点的 transitionNode = transitionNode.parent; //则修正transitionNode为它的parent context = transitionNode.context; //修正context为对应的parent的context } var isAppear = !context._isMounted || !vnode.isRootInsert; //当前是否还未初始化 如果transition组件还没有挂载,则设置isAppear为true if (isAppear && !appear && appear !== '') { //如果appear为false(当前是初始化),且appear为false(即初始渲染时不使用过渡),或不存在 return //则直接返回,不做处理 } var startClass = isAppear && appearClass //进入过渡的起始状态 ? appearClass : enterClass; var activeClass = isAppear && appearActiveClass //进入过渡时的状态 ? appearActiveClass : enterActiveClass; var toClass = isAppear && appearToClass //进入过渡的结束状态 ? appearToClass : enterToClass; var beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter; var enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter; var afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter; var enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled; var explicitEnterDuration = toNumber( isObject(duration) ? duration.enter : duration ); if ("development" !== 'production' && explicitEnterDuration != null) { checkDuration(explicitEnterDuration, 'enter', vnode); } var expectsCSS = css !== false && !isIE9; //是否使用 CSS 过渡类 IE9是不支持的 var userWantsControl = getHookArgumentsLength(enterHook); var cb = el._enterCb = once(function () { //完成后的回调函数 if (expectsCSS) { removeTransitionClass(el, toClass); removeTransitionClass(el, activeClass); } if (cb.cancelled) { if (expectsCSS) { removeTransitionClass(el, startClass); } enterCancelledHook && enterCancelledHook(el); } else { afterEnterHook && afterEnterHook(el); } el._enterCb = null; }); if (!vnode.data.show) { // remove pending leave element on enter by injecting an insert hook mergeVNodeHook(vnode, 'insert', function () { var parent = el.parentNode; var pendingNode = parent && parent._pending && parent._pending[vnode.key]; if (pendingNode && pendingNode.tag === vnode.tag && pendingNode.elm._leaveCb ) { pendingNode.elm._leaveCb(); } enterHook && enterHook(el, cb); }); } // start enter transition beforeEnterHook && beforeEnterHook(el); //如果定义了beforeEnterHook钩子函数,则执行它,例子里的beforeenter会执行这里,输出:进入过渡前的事件 if (expectsCSS) { //如果expectsCSS为true addTransitionClass(el, startClass); //给el元素新增一个class,名为startClass addTransitionClass(el, activeClass); //给el元素新增一个class,名为activeClass nextFrame(function () { //下次浏览器重绘时 removeTransitionClass(el, startClass); //移除startClass这个class ;因为有设置了activeClass,所以此时就会开始执行动画了 if (!cb.cancelled) { //如果cb.cancelled为空 addTransitionClass(el, toClass); //添加toClass这个class if (!userWantsControl) { if (isValidDuration(explicitEnterDuration)) { //如果用户自定义了动画时间 setTimeout(cb, explicitEnterDuration); } else { whenTransitionEnds(el, type, cb); //否则执行默认的whenTransitionEnds()函数(等到动画结束后就会执行cb这个回调函数了) } } } }); } if (vnode.data.show) { toggleDisplay && toggleDisplay(); enterHook && enterHook(el, cb); } if (!expectsCSS && !userWantsControl) { cb(); } }
resolveTransition会根据transitioin里的name属性自动拼凑css名,如下:
function resolveTransition (def) { //第7419行 解析transition if (!def) { return } /* istanbul ignore else */ if (typeof def === 'object') { //如果def是一个对象 var res = {}; if (def.css !== false) { //如果css不等于false extend(res, autoCssTransition(def.name || 'v')); //获取class样式 } extend(res, def); return res } else if (typeof def === 'string') { return autoCssTransition(def) } } var autoCssTransition = cached(function (name) { return { enterClass: (name + "-enter"), enterToClass: (name + "-enter-to"), enterActiveClass: (name + "-enter-active"), leaveClass: (name + "-leave"), leaveToClass: (name + "-leave-to"), leaveActiveClass: (name + "-leave-active") } });
例子里执行到这里时返回的如下:
回到enter函数,最后会执行whenTransitionEnds函数,如下:
function whenTransitionEnds ( //第7500行 工具函数,当el元素的动画执行完毕后就去执行cb函数 el, expectedType, cb ) { var ref = getTransitionInfo(el, expectedType); //获取动画信息 var type = ref.type; //动画的类型,例如:transition var timeout = ref.timeout; //动画结束时间 var propCount = ref.propCount; //如果是transition类型的动画,是否有transform动画存在 if (!type) { return cb() } var event = type === TRANSITION ? transitionEndEvent : animationEndEvent; //如果是transition动画则设置event为transitionend(transition结束事件),否则设置为animationend(animate结束事件) var ended = 0; var end = function () { el.removeEventListener(event, onEnd); cb(); }; var onEnd = function (e) { //动画结束事件 if (e.target === el) { if (++ended >= propCount) { end(); //如果所有的动画都执行结束了,则执行end()函数 } } }; setTimeout(function () { if (ended < propCount) { end(); } }, timeout + 1); el.addEventListener(event, onEnd); //在el节点上绑定event事件,当动画结束后会执行onEnd函数 }
getTransitionInfo用于获取动画的信息,返回一个对象格式,如下:
function getTransitionInfo (el, expectedType) { //第7533行 获取el元素上上的transition信息 var styles = window.getComputedStyle(el); //获取el元素所有最终使用的CSS属性值 var transitionDelays = styles[transitionProp + 'Delay'].split(', '); //transition的延迟时间 ;比如:["0.5s"] var transitionDurations = styles[transitionProp + 'Duration'].split(', '); //动画持续时间 var transitionTimeout = getTimeout(transitionDelays, transitionDurations); //获取动画结束的时间 var animationDelays = styles[animationProp + 'Delay'].split(', '); var animationDurations = styles[animationProp + 'Duration'].split(', '); var animationTimeout = getTimeout(animationDelays, animationDurations); var type; var timeout = 0; var propCount = 0; /* istanbul ignore if */ if (expectedType === TRANSITION) { //如果expectedType等于TRANSITION(全局变量,等于字符串:'transition') if (transitionTimeout > 0) { type = TRANSITION; timeout = transitionTimeout; propCount = transitionDurations.length; } } else if (expectedType === ANIMATION) { //如果是animation动画 if (animationTimeout > 0) { type = ANIMATION; timeout = animationTimeout; propCount = animationDurations.length; } } else { timeout = Math.max(transitionTimeout, animationTimeout); //获取两个变量的较大值,保存到timeout里 type = timeout > 0 ? transitionTimeout > animationTimeout //修正类型 ? TRANSITION : ANIMATION : null; propCount = type ? type === TRANSITION //动画的个数 transition可以一次性指定多个动画的,用,分隔 ? transitionDurations.length : animationDurations.length : 0; } var hasTransform = type === TRANSITION && transformRE.test(styles[transitionProp + 'Property']); return { //最后返回一个动画相关的对象 type: type, timeout: timeout, propCount: propCount, hasTransform: hasTransform } }
例子里返回后的对象信息如下:
回到whenTransitionEnds函数,等到动画结束时就会执行参数3,也就是enter函数内定义的cb局部函数,该函数最终会移除toClass和activeClass,最后执行afterEnter回掉函数。