對於過度動畫如果要同時渲染整個列表時,可以使用transition-group組件。 transition-group組件的props和transition組件類似,不同點是transition-group組件的props是沒有mode屬性的,另外多了以下兩個props tag 標簽名 moveCl ...
對於過度動畫如果要同時渲染整個列表時,可以使用transition-group組件。
transition-group組件的props和transition組件類似,不同點是transition-group組件的props是沒有mode屬性的,另外多了以下兩個props
tag 標簽名
moveClass 新增/移除元素時的過渡 ;如果未指定則預設會拼湊出name+"-move"這個格式的,一般很少用到,比較複雜的動畫可以該介面實現
不同於transition組件,transition-group組件它會以一個真實元素呈現,預設為一個<span>,我們也可以通過tag特性更換為其他元素,每個總都需要提供唯一的key屬性值。以Vue官網的某例子為例,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="vue.js"></script> </head> <body> <style> .list-item{display: inline-block;margin-right: 10px;} .list-enter-active,.list-leave-active{transition: all 1s;} .list-enter,.list-leave-to{opacity: 0;transform: translateY(30px);} </style> <div id="d"> <button v-on:click='add'>add</button><button v-on:click='remove'>remove</button> <transition-group tag="p" name="list" > <span v-for="no in Nums" :key="no" class="list-item">{{no}}</span> </transition-group> </div> <script>
Vue.config.productionTip=false;
Vue.config.devtools=false; var app = new Vue({ el:'#d', methods:{ randomIndex:function(){return Math.floor(Math.random()*this.Nums.length)}, add:function(){this.Nums.splice(this.randomIndex(),0,this.nextVal++)}, remove:function(){this.Nums.splice(this.randomIndex(),1)} }, data:{Nums:[1,2,3,4,5,6,7,8,9],nextVal:10} }) </script> </body> </html>
渲染的DOM樹如下:
我們可以看到transition-group渲染為了一個p元素,這是因為我們通過tag特性指定為了p,顯示如下:
當我們點擊add或remove時就會新增/刪除一個數字,並會觸發動畫
動畫更新時Vue通過key來作為該元素的唯一標識,保存到內部的一個map變數里,如果沒有key是要報錯的,:例如我們把模板里的:key="no"去掉,改成<span v-for="no in Nums" class="list-item">{{no}}</span>,控制台會報錯的,如下:
源碼分析
從源碼角度來看,對於transition-group來說,它只是新增了一個管理子節點的功能,最終的動畫還是和transition組件一樣,來通過設置每個子節點的data.transition屬性為對應的動畫對象來實現的,transition-group組件的結構如下:
var props = extend({ //第8343行 transition-group組件的props定義 tag: String, moveClass: String }, transitionProps); delete props.mode; var TransitionGroup = { //transition-group組件的定義 props: props, render: function render (h) { //render函數 /**/ }, beforeUpdate: function beforeUpdate () { //beforeUpdate鉤子 /**/ }, updated: function updated () { //updated鉤子 /**/ }, methods: { hasMove: function hasMove (el, moveClass) { /**/ } } }
transition-group初始化時只會執行組件里的render函數,更新操作時會執行beforeUpdate和updated鉤子函數,hasMove是一個在updated里要用到的方法
初始化時會執行render函數,如下:
render: function render (h) { //transition的render函數,它也沒有template模板 var tag = this.tag || this.$vnode.data.tag || 'span'; //標簽名,預設為span var map = Object.create(null); var prevChildren = this.prevChildren = this.children; var rawChildren = this.$slots.default || []; //獲取調用transition-group時的所有子節點Vnode var children = this.children = []; //組件實例上新增一個children屬性 var transitionData = extractTransitionData(this); //調用extractTransitionData()函數提取在transition組件上定義的data,和transition組件是一樣的 for (var i = 0; i < rawChildren.length; i++) { //遍歷所有子節點 var c = rawChildren[i]; if (c.tag) { if (c.key != null && String(c.key).indexOf('__vlist') !== 0) { //如果c存在key children.push(c); //保存到children數組裡 map[c.key] = c //保存到map對象里,key作為鍵名 ;(c.data || (c.data = {})).transition = transitionData; //給所有子節點VNode.data.transition上增加transitionData } else { //如果子節點c不存在key,則報錯,也就是上面說的去掉模板里key後的報錯 var opts = c.componentOptions; var name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag; warn(("<transition-group> children must be keyed: <" + name + ">")); } } } if (prevChildren) { //更新時的分支 var kept = []; var removed = []; for (var i$1 = 0; i$1 < prevChildren.length; i$1++) { var c$1 = prevChildren[i$1]; c$1.data.transition = transitionData; c$1.data.pos = c$1.elm.getBoundingClientRect(); if (map[c$1.key]) { kept.push(c$1); } else { removed.push(c$1); } } this.kept = h(tag, null, kept); this.removed = removed; } return h(tag, null, children) //執行h函數,也就是_c函數去創建tag這個VNode },
初始化就這樣子結束了,map里保存了所有的子節點,key作為鍵名,值為對應的VNode:
這樣每個子節點VNode都含有transition屬性了,如果設置了初次渲染則頁面初始化時就會自動執行動畫了,如果新增/移除了元素,動畫怎麼實現的呢,這個就要靠beforeUpdate鉤子來實現了。
當頁面更新,例如我們點擊了remove按鈕會刪除一個元素並觸發更新,此時會重新執行render函數,與初始化有點不同,如下:
render: function render (h) { //transition的render函數,它也沒有template模板 var tag = this.tag || this.$vnode.data.tag || 'span'; var map = Object.create(null); var prevChildren = this.prevChildren = this.children; //獲取之前的children信息 var rawChildren = this.$slots.default || []; //獲取當前的所有子節點Vnode var children = this.children = []; var transitionData = extractTransitionData(this); for (var i = 0; i < rawChildren.length; i++) { var c = rawChildren[i]; if (c.tag) { if (c.key != null && String(c.key).indexOf('__vlist') !== 0) { children.push(c); map[c.key] = c ;(c.data || (c.data = {})).transition = transitionData; } else { var opts = c.componentOptions; var name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag; warn(("<transition-group> children must be keyed: <" + name + ">")); } } } if (prevChildren) { //如果prevChildren存在 var kept = []; var removed = []; for (var i$1 = 0; i$1 < prevChildren.length; i$1++) { //遍歷prevChildren var c$1 = prevChildren[i$1]; //這是每個之前的children c$1.data.transition = transitionData; c$1.data.pos = c$1.elm.getBoundingClientRect(); //獲取它的位置屬性,比如:{bottom: 68,height: 21,left: 8,right: 17.390625,top: 47,width: 9.390625,x: 8,y: 47} if (map[c$1.key]) { //如果map中存在C$1 kept.push(c$1); //則保存到kept里(本次渲染時保留的) } else { //如果map中不存在C$1 removed.push(c$1); //則保存到removed里(需要移除的) } } this.kept = h(tag, null, kept); //調用h創建一個VNode,保存到this.keps里 this.removed = removed; //需要刪除的VNode集合 } return h(tag, null, children) },
this.kept中保存了當前保留的所有子節點,而this.remove則保存了需要移除的子節點,例子里執行到這裡this.kept如下:
this.remove如下:
最後還是調用h返回以最新的children作為子節點的VNode,返回到組件的主線,觸發_update函數時如下:
Vue.prototype._update = function (vnode, hydrating) { //第2645行 var vm = this; if (vm._isMounted) { //如果已經掛載了 callHook(vm, 'beforeUpdate'); //則執行beforeUpdate生命周期函數 } var prevEl = vm.$el; var prevVnode = vm._vnode; //先將vm._vnode保存到prevVnode里 var prevActiveInstance = activeInstance; activeInstance = vm; vm._vnode = vnode; // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ); // no need for the ref nodes after initial patch // this prevents keeping a detached DOM tree in memory (#5851) vm.$options._parentElm = vm.$options._refElm = null; } else { //如果prevVnode不為空,即更新操作時 // updates vm.$el = vm.__patch__(prevVnode, vnode); //調用vm.__patch__進行更新操作 } /*略*/
因為當前運行是transition-group組件的過程,所以beforeUpdate就會執行,如下:
beforeUpdate: function beforeUpdate () { //transition-group組件的beforeUpdate鉤子函數 // force removing pass this.__patch__( this._vnode, this.kept, false, // hydrating true // removeOnly (!important, avoids unnecessary moves) ); //先執行一遍_patch__,這將刪除所有要刪除的節點,並觸發它們的離開轉換過渡,參數4設置為false,避免不必要的移動 this._vnode = this.kept; //再將this.kept賦值給_vnode },
this.__patch__會執行patchVnode,最後會執行updateChildren對每個子節點做出更新,除了kept之外的子節點VNode都會刪除掉。beforeUpdate最後會把this.kept保存到this._vnode。
最後回到transition-group組件的_update里,該函數會把vm._vnode保存到prevVnode局部變數,最後判斷如果prevVnode存在則執行__patch__做更新操作,由於每個子節點Vnode.data.transition都保存著動畫信息,所以都會自動執行動畫。
updated鉤子函數官網說是為了改變定位用的,是個新特性,搗鼓了一會兒沒找到合適的場景,以後用到了再來深入研究一下