transition組件可以給任何元素和組件添加進入/離開過渡,但只能給單個組件實行過渡效果(多個元素可以用transition-group組件,下一節再講),調用該內置組件時,可以傳入如下特性: name 用於自動生成CSS過渡類名 例如:name:'fade'將自動拓展為.fade-enter, ...
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/[email protected]/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.appearCla