成人怡红院-成人怡红院视频在线观看-成人影视大全-成人影院203nnxyz-美女毛片在线看-美女免费黄

站長資訊網
最全最豐富的資訊網站

深入了解Vue中的雙端diff 算法

diff 算法是渲染器中最復雜的部分,本篇文章帶大家了解一下Vue中的雙端diff 算法,希望對大家有所幫助!

深入了解Vue中的雙端diff 算法

Vue 和 React 都是基于 vdom 的前端框架,組件渲染會返回 vdom,渲染器再把 vdom 通過增刪改的 api 同步到 dom。(學習視頻分享:vuejs視頻教程)

當再次渲染時,會產生新的 vdom,渲染器會對比兩棵 vdom 樹,對有差異的部分通過增刪改的 api 更新到 dom。

這里對比兩棵 vdom 樹,找到有差異的部分的算法,就叫做 diff 算法。

diff 算法是渲染器中最復雜的部分,也是面試的熱點問題。今天我們就通過 Vue 的 diff 算法來探究下 diff 算法吧。

diff 算法

我們知道,兩棵樹做 diff,復雜度是 O(n^3) 的,因為每個節點都要去和另一棵樹的全部節點對比一次,這就是 n 了,如果找到有變化的節點,執行插入、刪除、修改也是 n 的復雜度。所有的節點都是這樣,再乘以 n,所以是 O(n * n * n) 的復雜度。

深入了解Vue中的雙端diff 算法

這樣的復雜度對于前端框架來說是不可接受的,這意味著 1000 個節點,渲染一次就要處理 1000 * 1000 * 1000,一共 10 億次。

所以前端框架的 diff 約定了兩種處理原則:只做同層的對比,type 變了就不再對比子節點。

因為 dom 節點做跨層級移動的情況還是比較少的,一般情況下都是同一層級的 dom 的增刪改。

這樣只要遍歷一遍,對比一下 type 就行了,是 O(n) 的復雜度,而且 type 變了就不再對比子節點,能省下一大片節點的遍歷。另外,因為 vdom 中記錄了關聯的 dom 節點,執行 dom 的增刪改也不需要遍歷,是 O(1)的,整體的 diff 算法復雜度就是 O(n) 的復雜度。

深入了解Vue中的雙端diff 算法

1000 個節點渲染一次最多對比 1000 次,這樣的復雜度就是可接受的范圍了。

但是這樣的算法雖然復雜度低了,卻還是存在問題的。

比如一組節點,假設有 5 個,類型是 ABCDE,下次渲染出來的是 EABCD,這時候逐一對比,發現 type 不一樣,就會重新渲染這 5 個節點。

而且根據 type 不同就不再對比子節點的原則,如果這些節點有子節點,也會重新渲染。

dom 操作是比較慢的,這樣雖然 diff 的算法復雜度是低了,重新渲染的性能也不高。

所以,diff 算法除了考慮本身的時間復雜度之外,還要考慮一個因素:dom 操作的次數。

上面那個例子的 ABCDE 變為 EABCD,很明顯只需要移動一下 E 就行了,根本不用創建新元素。

但是怎么對比出是同個節點發生了移動呢?

判斷 type 么? 那不行,同 type 的節點可能很多,區分不出來的。

最好每個節點都是有唯一的標識。

所以當渲染一組節點的時候,前端框架會讓開發者指定 key,通過 key 來判斷是不是有點節點只是發生了移動,從而直接復用。

這樣,diff 算法處理一組節點的對比的時候,就要根據 key 來再做一次算法的優化。

我們會把基于 key 的兩組節點的 diff 算法叫做多節點 diff 算法,它是整個 vdom 的 diff 算法的一部分。

接下來我們來學習一下多節點 diff 算法:

簡單 diff

假設渲染 ABCD 一組節點,再次渲染是 DCAB,這時候怎么處理呢?

多節點 diff 算法的目的是為了盡量復用節點,通過移動節點代替創建。

所以新 vnode 數組的每個節點我們都要找下在舊 vnode 數組中有沒有對應 key 的,有的話就移動到新的位置,沒有的話再創建新的。

也就是這樣的:

const oldChildren = n1.children const newChildren = n2.children  let lastIndex = 0 // 遍歷新的 children for (let i = 0; i < newChildren.length; i++) {     const newVNode = newChildren[i]     let j = 0     let find = false     // 遍歷舊的 children     for (j; j < oldChildren.length; j++) {       const oldVNode = oldChildren[j]       // 如果找到了具有相同 key 值的兩個節點,則調用 patch 函數更新       if (newVNode.key === oldVNode.key) {         find = true         patch(oldVNode, newVNode, container)                  處理移動...                  break //跳出循環,處理下一個節點       }    }    // 沒有找到就是新增了    if (!find) {       const prevVNode = newChildren[i - 1]       let anchor = null       if (prevVNode) {         anchor = prevVNode.el.nextSibling       } else {         anchor = container.firstChild       }       patch(null, newVNode, container, anchor)    } }

這里的 patch 函數的作用是更新節點的屬性,重新設置事件監聽器。如果沒有對應的舊節點的話,就是插入節點,需要傳入一個它之后的節點作為錨點 anchor。

我們遍歷處理新的 vnode:

先從舊的 vnode 數組中查找對應的節點,如果找到了就代表可以復用,接下來只要移動就好了。

如果沒找到,那就執行插入,錨點是上一個節點的 nextSibling。

深入了解Vue中的雙端diff 算法

那如果找到了可復用的節點之后,那移動到哪里呢?

其實新的 vnode 數組中記錄的順序就是目標的順序。所以把對應的節點按照新 vnode 數組的順序來移動就好了。

const prevVNode = newChildren[i - 1] if (prevVNode) {     const anchor = prevVNode.el.nextSibling     insert(newVNode.el, container, anchor) }

要插入到 i 的位置,那就要取 i-1 位置的節點的 nextSibling 做為錨點來插入當前節點。

深入了解Vue中的雙端diff 算法

但是并不是所有的節點都需要移動,比如處理到第二個新的 vnode,發現它在舊的 vnode 數組中的下標為 4,說明本來就是在后面了,那就不需要移動了。反之,如果是 vnode 查找到的對應的舊的 vnode 在當前 index 之前才需要移動。

也就是這樣:

let j = 0 let find = false // 遍歷舊的 children for (j; j < oldChildren.length; j++) {     const oldVNode = oldChildren[j]     // 如果找到了具有相同 key 值的兩個節點,則調用 patch 函數更新之     if (newVNode.key === oldVNode.key) {         find = true         patch(oldVNode, newVNode, container)          if (j < lastIndex) { // 舊的 vnode 數組的下標在上一個 index 之前,需要移動           const prevVNode = newChildren[i - 1]           if (prevVNode) {             const anchor = prevVNode.el.nextSibling             insert(newVNode.el, container, anchor)           }         } else {// 不需要移動           // 更新 lastIndex           lastIndex = j         }         break     } }

查找新的 vnode 在舊的 vnode 數組中的下標,如果找到了的話,說明對應的 dom 就是可以復用的,先 patch 一下,然后移動。

移動的話判斷下下標是否在 lastIndex 之后,如果本來就在后面,那就不用移動,更新下 lastIndex 就行。

如果下標在 lastIndex 之前,說明需要移動,移動到的位置前面分析過了,就是就是新 vnode 數組 i-1 的后面。

這樣,我們就完成了 dom 節點的復用和移動。

新的 vnode 數組全部處理完后,舊的 vnode 數組可能還剩下一些不再需要的,那就刪除它們:

// 遍歷舊的節點 for (let i = 0; i < oldChildren.length; i++) {     const oldVNode = oldChildren[i]     // 拿著舊 VNode 去新 children 中尋找相同的節點     const has = newChildren.find(       vnode => vnode.key === oldVNode.key     )     if (!has) {       // 如果沒有找到相同的節點,則移除       unmount(oldVNode)     } }

這樣,我們就完成了兩組 vnode 的 diff 和對應 dom 的增刪改。

小結一下:

diff 算法的目的是根據 key 復用 dom 節點,通過移動節點而不是創建新節點來減少 dom 操作。

對于每個新的 vnode,在舊的 vnode 中根據 key 查找一下,如果沒查找到,那就新增 dom 節點,如果查找到了,那就可以復用。

復用的話要不要移動要判斷下下標,如果下標在 lastIndex 之后,就不需要移動,因為本來就在后面,反之就需要移動。

最后,把舊的 vnode 中在新 vnode 中沒有的節點從 dom 樹中刪除。

這就是一個完整的 diff 算法的實現。

深入了解Vue中的雙端diff 算法

這個 diff 算法我們是從一端逐個處理的,叫做簡單 diff 算法。

簡單 diff 算法其實性能不是最好的,比如舊的 vnode 數組是 ABCD,新的 vnode 數組是 DABC,按照簡單 diff 算法,A、B、C 都需要移動。

那怎么優化這個算法呢?

從一個方向順序處理會有這個問題,那從兩個方向同時對比呢?

這就是雙端 diff 算法:

雙端 diff

簡單 diff 算法能夠實現 dom 節點的復用,但有的時候會做一些沒必要的移動。雙端 diff 算法解決了這個問題,它是從兩端進行對比。

我們需要 4 個指針,分別指向新舊兩個 vnode 數組的頭尾:

深入了解Vue中的雙端diff 算法

頭和尾的指針向中間移動,直到 oldStartIdx <= oldEndIdx 并且 newStartIdx <= newEndIdx,說明就處理完了全部的節點。

每次對比下兩個頭指針指向的節點、兩個尾指針指向的節點,頭和尾指向的節點,是不是 key是一樣的,也就是可復用的。

如果是可復用的話就直接用,調用 patch 更新一下,如果是頭尾這種,還要移動下位置。

也就是這樣的:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {   if (oldStartVNode.key === newStartVNode.key) { // 頭頭     patch(oldStartVNode, newStartVNode, container)     oldStartVNode = oldChildren[++oldStartIdx]     newStartVNode = newChildren[++newStartIdx]   } else if (oldEndVNode.key === newEndVNode.key) {//尾尾     patch(oldEndVNode, newEndVNode, container)     oldEndVNode = oldChildren[--oldEndIdx]     newEndVNode = newChildren[--newEndIdx]   } else if (oldStartVNode.key === newEndVNode.key) {//頭尾,需要移動     patch(oldStartVNode, newEndVNode, container)     insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)      oldStartVNode = oldChildren[++oldStartIdx]     newEndVNode = newChildren[--newEndIdx]   } else if (oldEndVNode.key === newStartVNode.key) {//尾頭,需要移動     patch(oldEndVNode, newStartVNode, container)     insert(oldEndVNode.el, container, oldStartVNode.el)      oldEndVNode = oldChildren[--oldEndIdx]     newStartVNode = newChildren[++newStartIdx]   } else {          // 頭尾沒有找到可復用的節點   } }

頭頭和尾尾的對比比較簡單,頭尾和尾頭的對比還要移動下節點。

比如舊 vnode 的頭節點是新的 vnode 的尾節點,那就要把它移動到舊的 vnode 的尾節點的位置。

也就是:

insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

插入節點的錨點節點是 oldEndVNode 對應的 dom 節點的 nextSibling。

如果舊 vnode 的尾節點是新 vnode 的頭結點,那就要把它移動到舊 vnode 的頭結點的位置。

也就是:

insert(oldEndVNode.el, container, oldStartVNode.el)

插入節點的錨點節點是 oldStartVNode 對應的 dom 節點(因為要插在它之前)。

從雙端進行對比,能盡可能的減少節點移動的次數。

當然,還要處理下如果雙端都沒有可復用節點的情況:

如果雙端都沒有可復用節點,那就在舊節點數組中找,找到了就把它移動過來,并且原位置置為 undefined。沒找到的話就插入一個新的節點。

也就是這樣:

const idxInOld = oldChildren.findIndex(   node => node.key === newStartVNode.key ) if (idxInOld > 0) {   const vnodeToMove = oldChildren[idxInOld]   patch(vnodeToMove, newStartVNode, container)   insert(vnodeToMove.el, container, oldStartVNode.el)   oldChildren[idxInOld] = undefined } else {   patch(null, newStartVNode, container, oldStartVNode.el) }

因為有了一些 undefined 的節點,所以要加上空節點的處理邏輯:

if (!oldStartVNode) {     oldStartVNode = oldChildren[++oldStartIdx] } else if (!oldEndVNode) {     oldEndVNode = newChildren[--oldEndIdx] }

這樣就完成了節點的復用和移動的邏輯。

那確實沒有可復用的節點的那些節點呢?

經過前面的移動之后,剩下的節點都被移動到了中間,如果新 vnode 有剩余,那就批量的新增,如果舊 vnode 有剩余那就批量的刪除。

因為前面一個循環的判斷條件是 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,這樣如果 old vnode 多了,最后 newStartIdx 會小于 newEndIdx。如果 new vnode 多了,最后 oldStartIdx 會小于 oldEndIdx。

所以判斷條件是這樣的:

if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {   // 添加新節點   for (let i = newStartIdx; i <= newEndIdx; i++) {     patch(null, newChildren[i], container, oldStartVNode.el)   } } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {   // 移除操作   for (let i = oldStartIdx; i <= oldEndIdx; i++) {     unmount(oldChildren[i])   } }

這樣就是一個完整的 diff 算法了,包括查找可復用節點和移動節點、新增和刪除節點。

而且因為從兩側查找節點,會比簡單 diff 算法性能更好一些。

比如 ABCD 到 DABC,簡單 diff 算法需要移動 ABC 三個節點,而雙端 diff 算法只需要移動 D 一個節點。

小結一下:

雙端 diff 是頭尾指針向中間移動的同時,對比頭頭、尾尾、頭尾、尾頭是否可以復用,如果可以的話就移動對應的 dom 節點。

如果頭尾沒找到可復用節點就遍歷 vnode 數組來查找,然后移動對應下標的節點到頭部。

最后還剩下舊的 vnode 就批量刪除,剩下新的 vnode 就批量新增。

深入了解Vue中的雙端diff 算法

雙端 diff 算法是 Vue2 采用的 diff 算法,性能還不錯。

后來,Vue3 又對 diff 算法進行了一次升級,叫做快速 diff 算法。這個后面再講。

總結

React 和 Vue 都是基于 vdom 的前端框架,組件產生 vdom,渲染器再把 vdom 通過增刪改的 dom api 更新到 dom。

當再次渲染出 vdom 時,就要新舊兩棵 vdom 樹做 diff,只更新變化的 dom 節點。

兩棵樹的 diff 是 O(n^3) 的,時間復雜度太高,因此前端框架規定了只做同層 diff,還有 type 不一樣就認為節點不一樣,不再對比子節點。這樣時間復雜度一下子就降到了 O(n)。

但是對于多個子字節點的 diff 不能粗暴的刪除和新增,要盡量復用已有的節點,也就是通過移動代替新增。

所以多節點的時候,要指定 key,然后 diff 算法根據 key 來查找和復用節點。

簡單 diff 算法是依次根據 key 查找舊節點的,移動的話通過 lastIndex 判斷,大于它就不用動,小于它才需要移動。剩下的節點再批量刪除和新增。

但是簡單 diff 算法局限性還是比較大的,有些情況下性能并不好,所以 vue2 用的是雙端 diff 算法。

雙端 diff 算法是頭尾指針向中間移動,分別判斷頭尾節點是否可以復用,如果沒有找到可復用的節點再去遍歷查找對應節點的下標,然后移動。全部處理完之后也要對剩下的節點進行批量的新增和刪除。

其實 diff 算法最重要的就是找到可復用的節點,然后移動到正確的位置。只不過不同的算法查找順序不一樣。

vue2 是用的雙端 diff 的算法,而 vue3 則通過最長遞增子序列的算法做了進一步的優化,關于優化后的 diff 算法,我們之后再聊。

【相關視頻教程推薦:web前端】

贊(0)
分享到: 更多 (0)
?
網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號
贵阳40多岁熟女高潮呻吟| 四川少妇BBW搡BBBB槡BB| 国产精品久久久久久妇女| 日韩无码视频二区| 国产乱XXXX搡XXXXX搡| 亚洲精品AV中文字幕在线| 麻豆国产一卡二卡三卡| 最新69国产成人精品视频免费| 日本熟妇WWW色视频在线播放| 国产成人亚洲精品无码车A| 制服 丝袜 有码 无码 中文| 无码人妻一区二区三区精品视频年| 年轻老师的滋味5| 极品婬荡少妇XXXX欧美| 日本最新高清一区二区三| 高h乱好爽要尿了潮喷了| 国精产品W灬源码1688伊| 天天曰天天躁天天摸孕妇| 欧美国产SE综合| 久久久精品波多野结衣| 国产欧洲野花A级| 宝宝又大了1V1| 中文精品一卡2卡3卡4卡| 亚洲精品无码久久一线| 无码人妻精品中文字幕免费东京热| 欧美亚洲国产精品久久高清| 看全色黄大色大片免费无码| 哈昂~哈昂够了太多太深| 国产成人无码A区在线| がーるずらっしゅ在线中文| 在线观看国产成人AⅤ天堂| 亚洲少妇XXXXX| 亚洲国产日韩欧美一区二区三区 | 一边下奶一边吃面膜视频讲解图片| 午夜麻豆国产精品无码| 欧美 亚洲 日本 成人| 国产亚洲午夜高清国产拍精品| 18禁免费无码无遮挡不卡网站| 亚洲丁香五月天缴情综合| 日韩久久无码免费毛片软件| 国产精品538一区二区在线| 97超级碰碰碰久久久久| 丰满大屁股熟女偷拍内射| 国产乱子伦农村叉叉叉| 久久99精品国产自在现线小黄鸭 | VODAFONEWIFI性另类| 中年国产丰满熟女乱子正在播放| 亚洲成A人片在线观看无码专区| 秋霞在线观看视频| 精品久久人人妻人人做精品| 国产AV人人夜夜澡人人爽小说| 中文字幕无码专区人妻系列| 88国产精品欧美一区二区三区 | 五月丁香色综合久久4438| 一杆长枪直入两扇门| 国产94在线 | 亚洲| 久久免费看少妇高潮V片特黄| 少妇无码一区二区二三区| 野花日本免费完整版高清版 | 精品国产精品久久一区免费式| 欧美巨鞭大战丰满少妇| 亚洲AⅤ精品无码一区二区| 696969大但人文艺术主题| 国产一区二区三区在线观看免费| 欧美精品人人做人人爱视频| 亚洲AV无码成人精品区狼人影院| 把腿张开自慰给我看| 久久久久精品日韩久久久| 完全着衣の爆乳お姉さんが| 70歳の熟女セックス| 精品国产VA久久久久久久冰| 国产精久久一区二区三区| 天堂いっしょにしよ在线| 亚洲欧美日韩中文字幕在线一区| 大波妺AV网站影院| 蜜臀AV无码精品人妻色欲| 亚洲AV无码传区国产乱码O | 欧美精品人人做人人爱视频 | 熟女系列丰满熟妇AV| 2021日韩无码| 妺妺窝人体色777777换脸| 亚洲AⅤ优女AV综合久久久| 波多野结衣一区二区免费视频| 久久理伦片琪琪电影院| 又粗又大又硬毛片免费看| 国产精品爽爽VA在线观看网站| 人妻少妇精品视频无码专区| 永久免费看啪啪的网站| 把腿张开我要CAO死你在线观看| 久久久久精品久久九九| 亚洲AV乱码一区二区三区| 东北妇女精品BBWBBW| 欧美成人精品第一区二区三区| 亚洲蜜桃无码视頻精品网| BBBBBB嫩BBBBBB| 久久WWW免费人成_网站| 午夜精品久久久久久99热| 成人AV鲁丝片一区二区免费| 欧美成人高清AⅤ免费观看| 一本色道久久综合狠狠躁| 国产又色又刺激高潮免费视频试看| 色欲av蜜臀一区二区三区vr| A一区二区三区乱码在线 | 欧| 老师你乖乖的可以让你少吃点苦头 | 亚洲精品无码永久电影在线| 国产香蕉97碰碰视频VA碰碰看| 体育生爽擼雞巴CHINESE| 被两个男人按住胸吃奶好爽| 欧美性爱一区二区三区四区| 中文人妻熟妇乱又伦精品| 久久久久久九九99精品| 亚洲国产中文在线二区三区免| 国产热A欧美热A在线视频| 无码AV岛国片在线播放| 粉嫩AV一区二区三区免费观看| 日本少妇人妻XXXXⅩ18欧美| 男配每天都在体内成结节| 43417大但人文艺术| 麻花传媒0076在线观看| 一个添下面两个吃奶把腿扒开| 精品人妻暴躁一区二区三区| 亚洲国产AV无码专区亚洲AVL | 久久人妻少妇嫩草AV无码专区| 精品精品国产高清A级毛片| 久久午夜夜伦鲁鲁片免费无码影院 | 欧美 日韩 高清 国产AⅤ一区| 影音先锋最新AV资源网站| 久久精品成人无码观看免费| 亚洲色成人网站WWW永久四虎| 久久69老妇伦国产熟女高清 | 久久寂寞少妇成人内射| 亚洲熟妇色自偷自拍另类| 精品亚洲韩国一区二区三区| 亚洲色偷偷偷网站色偷一区人人澡| 精品少妇AY一区二区三区| 亚洲欧美综合精品AⅤ一区二区 | 玉米地诱子偷伦初尝云雨孽欲| 狼人无码精华AV午夜精品| 中美日韩精品激情无码AV| 年轻的小婊孑4中文字幕电影| 99精品视频在线观看免费| 欧洲精品码一区二区三区| 产后漂亮奶水人妻| 少妇高潮无套内谢麻豆传 | 视频一区二区三区在线观看蜜桃 | 欧美熟妇SEXFREE| 八戒八戒在线高清观看视频4| 日本三线和韩国三线品牌对比| 丰满熟女一区二区三区蜜桃臀| 无码中文字幕人妻在线一区| 和人妻隔着帘子按摩中字| 亚洲中文字幕无码一久久区| 免费A级毛片无码视频| JIZZJIZZ日本护士水好多| 搡老女人熟妇老太HD| 国产裸体歌舞一区二区| 亚洲乱妇老熟女爽到高潮的片| 军人全身脱精光自慰| JAGNEXSMAX在日本| 熟女俱乐部五十路二区AV| 国产又猛又黄又爽| 一本一道AV无码中文字幕﹣百度| 女人18毛片A级毛片视频| 成人欧美激情亚洲日韩蜜臀| 午夜无码A级毛片免费视频| 精品无码久久久久久久久| 52秋霞东北熟女叫床| 日日狠狠久久偷偷色综合96蜜桃 | 亚洲婷婷五月综合狠狠| 年轻漂亮岳每4乱理2 | 精品香蕉久久久午夜福利| 42岁女子经历20天断崖式衰老| 日本高清中文字幕在线观穿线视频| 国产精品久久久久久无毒不卡| 亚洲精品性爱av| 妺妺窝人体色WWW写真| 丰满熟妇大肉唇张开| 亚洲精品无码永久中文字幕| 女人双腿搬开让男人桶| 国产边做饭边被躁在线小说| 亚洲男女一区二区三区| 啪啪啪1000免费观看| 国产欧美另类精品久久久| 亚洲AV无码精品蜜桃| 人妻无码一区二区三区久| 自拍偷自拍亚洲精品被多人伦好爽| 狠狠色噜噜狠狠狠7777奇米| 中国少妇BBWBBW| 日本巨大的奶头在线观看| 国内精品人妻无码久久久影院导航| 一区二区三区人妻无码| 日日摸日日碰人妻无码老牲| 极品人妻系列少妇系列| CHINESE高潮收缩ORGASM| 无码一区二区三区视频| 久久婷婷国产剧情内射白浆 | 欧美成人v片一区二区三区激情| 国产成人AV一区二区三区| 野花韩国视频免费高清3| 色综合99久久久无码国产精品|