Vue的transition-group與Virtual Dom Diff算法的使用
開始
這次的題目看上去好像有點(diǎn)奇怪:把兩個(gè)沒有什么關(guān)聯(lián)的名詞放在了一起,正如大家所知道的,transition-group就是Vue的內(nèi)置組件之一主要用在列表的動(dòng)畫上,但是會(huì)跟Virtual Dom Diff算法有什么特別的聯(lián)系嗎?答案明顯是有的,所以接下來就是代碼分解。
緣起
主要是最近對(duì)Vue的Virtual Dom Diff算法有點(diǎn)模糊了,然后順手就打開了電腦準(zhǔn)備溫故知新;但是很快就留意到代碼:
// removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly
removeOnly是什么鬼,怎么感覺以前對(duì)這個(gè)變量沒啥印象的樣子,再看注釋:removeOnly只用在transition-group組件上,目的是為了保證移除的元素在離開的動(dòng)畫過程中能夠保持正確的相對(duì)位置(請(qǐng)?jiān)徫业脑g);好吧,是我當(dāng)時(shí)閱讀源碼的時(shí)候忽略了一些細(xì)節(jié)。但是這里引起我極大的好奇心,為了transition-group組件竟然要在Diff算法動(dòng)手腳,這個(gè)組件有什么必要性一定要這么做尼。
深入
首先假如沒有這個(gè)removeOnly的干擾,也就是canMove為true的時(shí)候,正常的Diff算法會(huì)是怎樣的流程:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left 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)) { // New element 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] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } }
- 首先會(huì)是oldStartVnode跟newStartVnode做對(duì)比,當(dāng)然如果它們類型一致就會(huì)進(jìn)入patch流程;
- 接著又嘗試oldEndVnode與newEndVnode做對(duì)比,繼續(xù)跳過;
- 明顯前面兩個(gè)判斷都沒有canMove的身影,因?yàn)檫@里patch后是不用移動(dòng)元素的,都是頭跟頭,尾跟尾,但是后面就不一樣了;再繼續(xù)oldStartVnode與newEndVnode對(duì)比,canMove開始出現(xiàn)了,這里舊的頭節(jié)點(diǎn)從頭部移動(dòng)到尾部了,進(jìn)行patch后,oldStartElem也需要移動(dòng)到oldEndElem后面;
- 同樣的如果跳過上一個(gè)判斷,繼續(xù)oldEndVnode與newStartVnode做對(duì)比,也會(huì)發(fā)生同樣的移動(dòng),只是這次是把oldEndElm移動(dòng)到oldStartElm前面去;
- 如果再跳過上面的判斷,就需要在舊的Vnode節(jié)點(diǎn)上建立一個(gè)oldKeyToIdx的map了(很明顯并不是所有的Vnode都會(huì)有key,所以這個(gè)map上并不一定有所有舊Vnode,甚至很有可能是空的),然后如果newStartVnode上定義了key的話在個(gè)map里面嘗試去找出對(duì)應(yīng)的oldVnode位置(當(dāng)然不存在的話,就可以理所當(dāng)然的認(rèn)為這是新的元素了);又如果newStartVnode沒有定義key,它就會(huì)暴力去遍歷所有的舊Vnode節(jié)點(diǎn)看看能否找出一個(gè)類型一致的可以進(jìn)行patch的VNode;說明定義key還是很重要的,現(xiàn)在Vue的模板上都會(huì)要求for循環(huán)列表的時(shí)候要定義key,可以想象如果我們直接使用下標(biāo)作為key的話會(huì)怎樣尼;根據(jù)sameVnode方法:
function sameVnode (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) ) ) ) }
首先會(huì)判斷key是否一致,然后是tag類型還有input類型等等。
所以下標(biāo)作為key的時(shí)候,很明顯key會(huì)很容易就會(huì)判斷為一致了,其次就是要看tag類型等等。
繼續(xù)如果從map里面找到了對(duì)應(yīng)的舊Vnode,又會(huì)繼續(xù)把這個(gè)Vnode對(duì)應(yīng)的Dom節(jié)點(diǎn)移動(dòng)到舊的oldStartElem前面。
綜上,Diff算法的移動(dòng)都是在舊Vnode上進(jìn)行的,而新Vnode僅僅只是更新了elm這個(gè)屬性。
在個(gè)Diff算法的最后,可以想象一種情況,元素都會(huì)往頭尾兩邊移動(dòng),剩下都是待會(huì)要剔除的元素了,需要執(zhí)行離開動(dòng)畫,但是這個(gè)效果肯定很糟糕,因?yàn)檫@個(gè)時(shí)候的列表是打亂了的,我們所期望的動(dòng)畫明顯是元素從原有的位置執(zhí)行離開動(dòng)畫了,那么也就是removeOnly存在的意義了。
transition-group的魔法
transition-group是如何利用removeOnly的尼;直接跳到transition-group的源碼上,直接就是一段注釋:
// Provides transition support for list items. // supports move transitions using the FLIP technique. // Because the vdom's children update algorithm is "unstable" - i.e. // it doesn't guarantee the relative positioning of removed elements, // we force transition-group to update its children into two passes: // in the first pass, we remove all nodes that need to be removed, // triggering their leaving transition; in the second pass, we insert/move // into the final desired state. This way in the second pass removed // nodes will remain where they should be.
大意就是:
這個(gè)組件是為了給列表提供動(dòng)畫支持的,而組件提供的動(dòng)畫運(yùn)用了FLIP技術(shù);
因?yàn)镈iff算法是不能保證移除元素的相對(duì)位置的(正如我們上面總結(jié)的),我們讓transition-group的更新必須經(jīng)過了兩個(gè)階段,第一個(gè)階段:我們先把所有要移除的元素移除以便觸發(fā)它們的離開動(dòng)畫;在第二個(gè)階段:我們才把元素移動(dòng)到正確的位置上。
知道了大致的邏輯了,那么transition-group具體是怎么實(shí)現(xiàn)的尼?
首先transition-group繼承了transiton組件相關(guān)的props,所以它們兩個(gè)真是鐵打的親兄弟。
const props = extend({ tag: String, moveClass: String }, transitionProps)
然后第一個(gè)重點(diǎn)來了beforeMount方法
beforeMount () { const update = this._update this._update = (vnode, hydrating) => { const restoreActiveInstance = setActiveInstance(this) // force removing pass this.__patch__( this._vnode, this.kept, false, // hydrating true // removeOnly (!important, avoids unnecessary moves) ) this._vnode = this.kept restoreActiveInstance() update.call(this, vnode, hydrating) } }
transition-group對(duì)_update方法做了特殊處理,先強(qiáng)行進(jìn)行一次patch,然后才執(zhí)行原本的update方法,這里也就是剛才注釋說的兩個(gè)階段的處理;
接著看this.kept,transition-group是在什么時(shí)候?qū)Node tree做的緩存的尼,再跟蹤代碼發(fā)現(xiàn)render方法也做了特殊處理:
render (h: Function) { const tag: string = this.tag || this.$vnode.data.tag || 'span' const map: Object = Object.create(null) const prevChildren: Array<VNode> = this.prevChildren = this.children const rawChildren: Array<VNode> = this.$slots.default || [] const children: Array<VNode> = this.children = [] const transitionData: Object = extractTransitionData(this) for (let i = 0; i < rawChildren.length; i++) { const c: VNode = 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 if (process.env.NODE_ENV !== 'production') { const opts: ?VNodeComponentOptions = c.componentOptions const name: string = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag warn(`<transition-group> children must be keyed: <${name}>`) } } } if (prevChildren) { const kept: Array<VNode> = [] const removed: Array<VNode> = [] for (let i = 0; i < prevChildren.length; i++) { const c: VNode = prevChildren[i] c.data.transition = transitionData c.data.pos = c.elm.getBoundingClientRect() if (map[c.key]) { kept.push(c) } else { removed.push(c) } } this.kept = h(tag, null, kept) this.removed = removed } return h(tag, null, children) },
這里的處理是首先用遍歷transition-group包含的VNode列表,把VNode都收集到children數(shù)組還有map上面去,并且把transition相關(guān)的屬性注入到VNode上,以便VNode移除的時(shí)候觸發(fā)對(duì)應(yīng)的動(dòng)畫。
然后就是如果prevChildren存在的時(shí)候,也就是render第二次觸發(fā)的時(shí)候遍歷舊的children列表,首先會(huì)把最新的transition屬性更新到舊的VNode上,然后就是很關(guān)鍵的去獲取VNode對(duì)應(yīng)的DOM節(jié)點(diǎn)的位置(很重要?。?,并且記錄;然后再根據(jù)map判斷哪些VNode是需要保持的(新舊列表相同的VNode),哪些是需要移除的,最后就是把this.kept指向需要保持的VNode列表;所以this.kept在第一階段的pacth過程中,才能準(zhǔn)確把要移除的VNode先移除,并且不會(huì)插入新的VNode,也不會(huì)移動(dòng)DOM節(jié)點(diǎn);在執(zhí)行后面的update方法才會(huì)做后面兩步。
接著看updated方法,如何去利用FLIP實(shí)現(xiàn)移動(dòng)動(dòng)畫的尼:
updated () { const children: Array<VNode> = this.prevChildren const moveClass: string = this.moveClass || ((this.name || 'v') + '-move') if (!children.length || !this.hasMove(children[0].elm, moveClass)) { return } // we divide the work into three loops to avoid mixing DOM reads and writes // in each iteration - which helps prevent layout thrashing. children.forEach(callPendingCbs) children.forEach(recordPosition) children.forEach(applyTranslation) // force reflow to put everything in position // assign to this to avoid being removed in tree-shaking // $flow-disable-line this._reflow = document.body.offsetHeight children.forEach((c: VNode) => { if (c.data.moved) { const el: any = c.elm const s: any = el.style addTransitionClass(el, moveClass) s.transform = s.WebkitTransform = s.transitionDuration = '' el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) { if (e && e.target !== el) { return } if (!e || /transform$/.test(e.propertyName)) { el.removeEventListener(transitionEndEvent, cb) el._moveCb = null removeTransitionClass(el, moveClass) } }) } }) },
這里的處理首先會(huì)檢查把move class加上之后是否有transform屬性,如果有就說明有移動(dòng)的動(dòng)畫;再接著處理:
- 調(diào)起pendding回調(diào),主要是移除動(dòng)畫事件的監(jiān)聽
- 記錄節(jié)點(diǎn)最新的相對(duì)位置
- 比較節(jié)點(diǎn)新舊位置,是否有變化,如果有變化就在節(jié)點(diǎn)上應(yīng)用transform,把節(jié)點(diǎn)移動(dòng)到舊的位置上;然后強(qiáng)制reflow,更新dom節(jié)點(diǎn)位置信息;所以我們看到的列表可能表面是沒有變化的,其實(shí)是我們把節(jié)點(diǎn)又移動(dòng)到原來的位置上了;
- 最后我們把位置有變化的節(jié)點(diǎn),加上move class,觸發(fā)移動(dòng)動(dòng)畫;
這就是transition-group所擁有的黑魔法,確實(shí)幫我們?cè)诒澈笞隽瞬簧俚氖虑椤?/p>
最后
溫故而知新,在寫的過程中其實(shí)發(fā)現(xiàn)了以前的理解還是有很多模糊的地方,說明自己平時(shí)閱讀代碼仍然不夠細(xì)心,沒有做到不求甚解,以后必須多多注意,最后的最后,如有錯(cuò)漏,希望大家能夠指正。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vue調(diào)取電腦攝像頭實(shí)現(xiàn)拍照功能
這篇文章主要為大家詳細(xì)介紹了vue調(diào)取電腦攝像頭實(shí)現(xiàn)拍照功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09關(guān)于vue中watch檢測(cè)到不到對(duì)象屬性的變化的解決方法
本篇文章主要介紹了關(guān)于vue中watch檢測(cè)到不到對(duì)象屬性的變化的解決方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02Vue設(shè)置瀏覽器小圖標(biāo)(ICON)的詳細(xì)步驟
vue中網(wǎng)頁圖標(biāo)默認(rèn)使用的是vue自帶的一個(gè)icon的圖標(biāo),也是vue的logo,下面這篇文章主要給大家介紹了關(guān)于Vue設(shè)置瀏覽器小圖標(biāo)(ICON)的詳細(xì)步驟,需要的朋友可以參考下2023-01-01部署vue+Springboot前后端分離項(xiàng)目的步驟實(shí)現(xiàn)
這篇文章主要介紹了部署vue+Springboot前后端分離項(xiàng)目的步驟實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05vue中監(jiān)聽input框獲取焦點(diǎn)及失去焦點(diǎn)的問題
這篇文章主要介紹了vue中監(jiān)聽input框獲取焦點(diǎn),失去焦點(diǎn)的問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07解決ant Design Search無法輸入內(nèi)容的問題
這篇文章主要介紹了解決ant Design Search無法輸入內(nèi)容的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10解決vue 格式化銀行卡(信用卡)每4位一個(gè)符號(hào)隔斷的問題
這篇文章主要介紹了vue 格式化銀行卡(信用卡)每4位一個(gè)符號(hào)隔斷的問題,本文給大家分享了解決方法,需要的朋友可以參考下2018-09-09