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回掉函數。