当前位置: 首页 > Web前端 > HTML5

Vue的transition-group和VirtualDomDiff算法

时间:2023-04-05 18:30:06 HTML5

一开始,这次的题目好像有点奇怪:把两个不相关的名词放在一起。众所周知,transition-group是Vue内置的组件之一,主要用于列表的动画,但是它和VirtualDomDiff算法有什么特殊的联系吗?答案显然是肯定的,那么接下来就是代码分解了。主要是最近Vue的VirtualDomDiff算法有点模糊,然后打开电脑学习了下;但很快注意到代码://removeOnly是一个特殊标志,仅供使用//以确保删除的元素在离开转换期间保持在正确的相对位置//constcanMove=!removeOnly什么是removeOnly?怎么感觉以前对这个变量没有印象?看注释:removeOnly只用在transition-group组件上,目的是为了保证被移除的元素在离开的动画过程中能够保持正确的相对位置(请原谅我的渣翻译);好吧,我在阅读源代码时忽略了一些细节。但这引起了我极大的好奇。对于过渡组组件,我必须使用Diff算法做一些事情。为什么这个组件需要这样做?先深入一点,如果没有这个removeOnly的干扰,也就是当canMove为真时,正常的Diff算法会是怎样的:){oldStartVnode=oldCh[++oldStartIdx]//Vnode已经向左移动}elseif(isUndef(oldEndVnode)){oldEndVnode=oldCh[--oldEndIdx]}elseif(sameVnode(oldStartVnode,newStartVnode)){patchVnode(oldStartVnode),newStartVnode,insertedVnodeQueue,newCh,newStartIdx)oldStartVnode=oldCh[++oldStartIdx]newStartVnode=newCh[++newStartIdx]}elseif(sameVnode(oldEndVnode,newEndVnode)){patchVnode(oldEndVnode,newEndVnode,insertedVnodeQueue,newCh,newEndIdx)oldEnd-Vnode=old[-oldEndIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newEndVnode)){//Vnode向右移动patchVnode(oldStartVnode,newEndVnode,insertedVnodeQueue,newCh,newEndIdx)canMove&&放大器;nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))oldStartVnode=oldCh[++oldStartIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldEndVnode,newStartVnode)){//Vnode向左移动patchVnode(oldEndVnode,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)canMove&&nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)oldEndVnode=oldCh[--oldEndIdx]newStartVnode=newCh[++newStartIdx]}else{if(isUndef(oldKeyToIdx))oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx)idxInOld=isDef(newStartVnode.key)?oldKeyToIdx[newStartVnode.key]:findIdxInOld(newStartVnode,oldCh,oldStartIdx,oldEndIdx)if(isUndef(idxInOld)){//新元素createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}else{vnodeToMove=oldCh[idxInOld]if(sameVnode(vnodeToMove,newStartVnode)){patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)oldCh[idxInOld]=undefinedcanMove&&nodeOps.insertBefore(parentElm,vnodeToolStar.elm)Vnodeelm.elm,}else{//相同的键但不同的元素。当作新元素createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}}newStartVnode=newCh[++newStartIdx]}}先是oldStartVnode再是newStartVnode进行比较,当然如果是同类型的,进入补丁流程;然后尝试比较oldEndVnode和newEndVnode,继续跳过;很明显,前面两个判断是没有canMove的,因为这里patch后不需要移动元素。是头对头,尾对尾,只是背不一样;继续对比oldStartVnode和newEndVnode,开始出现canMove,这里旧的head节点从head移动到tail,打补丁后oldStartElem也需要移动到oldEndElem之后;同样,如果跳过前面的判断,继续比较oldEndVnode和newStartVnode,也会出现同样的移动,只不过这次会将oldEndElm移到oldStartElm的前面;建立在Vnode节点上设置一个oldKeyToIdx的map(显然并不是所有的Vnode都会有key,所以这个map上可能不会有所有的oldVnode,甚至可能是空的),然后如果key定义在newStartVnode上,那么在一个map中就可以了尝试找到对应的oldVnode位置(当然,如果不存在,可以理所当然的认为这是一个新元素);而如果newStartVnode没有定义key,它会暴力遍历所有老的Vnode节点,看能不能找到同类型的可以打补丁的VNode;说明定义key还是很重要的。现在Vue模板在使用for循环列表时会要求定义键。你可以想象如果我们直接使用下标作为键会发生什么。;根据sameVnode方法:functionsameVnode(a,b){return(a.key===b.key&&((a.tag===b.tag&&a.isComment===b.isComment&&isDef(a.data)===isDef(b.data)&&sameInputType(a,b))||(isTrue(a.isAsyncPlaceholder)&&a.asyncFactory===b.asyncFactory&&isUndef(b.asyncFactory.error))))}首先会判断key是否一致,然后是tag类型和input类型等。所以当使用下标作为key的时候,很明显key会很容易判断一致,然后这取决于标签类型等。如果继续从map中找到对应的旧Vnode,则继续将这个Vnode对应的Dom节点移到旧的oldStartElem前面。综上所述,Diff算法的移动是在旧的Vnode上进行的,新的Vnode只更新elm的属性。在Diff算法的最后,可以想象这样一种情况,元素会移动到头部和尾部的两边,剩下的就是后面要移除的元素,需要执行离开动画,但是这个效果肯定很不好,因为此时的列表被打乱了。我们期待的动画显然是元素从原来的位置开始执行动画,所以这就是removeOnly存在的意义。transition-grouptransition-group的神奇之处在于如何使用removeOnly;直接跳转到transition-group的源码,直接是注释://Providestransitionsupportforlistitems.//supportsmovetransitionsusingtheFLIPtechnique.//因为vdom的childrenupdatealgorithm是“不稳定的”——即//它不保证被移除元素的相对定位,//我们强制transition-group将其子元素更新为两次pass://在第一遍中,我们移除所有需要移除的节点,//触发它们离开过渡;在第二遍中,我们将//插入/移动到最终所需的状态。这样在第二遍删除//节点将保留在它们应该在的位置。大致思路是:该组件是为列表提供动画支持,组件提供的动画使用了FLIP技术;因为Diff算法不能保证被移除元素的相对位置(正如我们上面总结的),我们让transition-group的更新必须经过两个阶段,第一阶段:我们先移除所有要移除的元素,以便触发他们离开的动画;在第二阶段:我们将元素移动到正确的位置。知道了大概的逻辑,那么transition-group是怎么实现的呢?首先,transition-group继承了transition组件相关的props,所以他们两个真是亲兄弟。constprops=extend({tag:String,moveClass:String},transitionProps)然后首先关注的是beforeMount方法beforeMount(){constupdate=this._updatethis._update=(vnode,hydrating)=>{constrestoreActiveInstance=setActiveInstance(this)//强制移除passthis.__patch__(this._vnode,this.kept,false,//hydratingtrue//removeOnly(!important,avoidsunnecessarymoves))this._vnode=this.keptrestoreActiveInstance()update.call(this,vnode,hydrating)}}transition-group对_update方法做了特殊处理,先强制执行patch,再执行原来的update方法,这里是注释中提到的两阶段处理现在;然后看this.kept,transition-group什么时候缓存了VNode树,再跟踪代码发现render方法也做了特殊处理:render(h:Function){consttag:string=this.tag||this.$vnode.data.tag||'span'constmap:Object=Object.create(null)constprevChildren:Array=this.prevChildren=this.childrenconstrawChildren:Array=this.$slots.默认||[]constchildren:Array=this.children=[]consttransitionData:Object=extractTransitionData(this)for(leti=0;ichildrenmustbekeyed:<${name}>`)}}}if(prevChildren){constkeeped:Array=[]constremoved:Array=[]for(leti=0;i=this.prevChildrenconstmoveClass:string=this.moveClass||((this.name||'v')+'-move')if(!children.length||!this.hasMove(children[0].elm,moveClass)){return}//我们将工作分为三个循环以避免在每次迭代中混合DOM读取和写入//-这有助于防止布局抖动。children.forEach(callPendingCbs)children.forEach(recordPosition)children.forEach(applyTranslation)//强制回流将所有内容都放在适当的位置//分配给它以避免在tree-shaking中被移除//$flow-disable-linethis._reflow=document.body.offsetHeightchildren.forEach((c:VNode)=>{if(c.data.moved){constel:any=c.elmconsts:any=el.styleaddTransitionClass(el,moveClass)s.transform=s.WebkitTransform=s.transitionDuration=''el.addEventListener(transitionEndEvent,el._moveCb=functioncb(e){if(e&&e.target!==el){return}if(!e||/transform$/.test(e.propertyName)){el.removeEventListener(transitionEndEvent,cb)el._moveCb=nullremoveTransitionClass(el,moveClass)}})}})},这里的处理会先检查添加move类后是否有transform属性,如果有则表示有移动动画;thenThen流程:调用pending回调,主要是去掉动画事件监听记录节点的最新相对位置,比较节点的新旧位置是否有变化。如果有变化,对节点应用transform并将节点移动到旧位置;然后强制回流更新dom节点的位置信息;所以我们看到的列表表面上可能没有变化,但实际上我们将节点移动到了原来的位置;最后我们给位置发生变化的节点添加移动类,触发移动动画;这就是transition-group拥有的黑魔法,真的帮我们做了很多幕后工作,温故知新。在写的过程中,其实发现之前的理解还有很多歧义,说明我平时看代码还是不够细心,没有请教深入理解就没有做过。