最近看了 vue-design 項目,寫的很棒,文中 diff 的思路全是來自該項目,這裏只是作一個學習的記錄。做者(【掘金地址】hcysunyang)已是 vue3 的 contributor 了。值得學習的一位 前端人。再次感謝👏👏👏html
我是一名前端的小學生。行文中對某些設計原理理解有誤十分歡迎你們討論指正😁😁😁,謝謝啦!固然有好的建議也謝謝提出來
(玩笑)前端
當前前端框架都有 diff算法,做用主要是處理比對虛擬Dom(Vnode
),最大化複用舊節點,最後渲染爲真實 Dom,最大化下降節點建立、刪除的的開銷。vue
老樣子,原本寫一篇文章的。東西越寫越多就裂開了🤣
node
【第一篇】和麪試官聊聊Diff___React(本文)
【第二篇】和麪試官聊聊Diff___vue2
【第三篇】和麪試官聊聊Diff___Vue3react
好了,不說廢話了。咱們開始吧。git
vue 、react中都是組件構成,每一個組件又是標籤元素構成。github
我主要技術棧是Vue。稍微說一下vue
vue編譯會涉及到幾個過程 【參考剖析 Vue.js 內部運行機制,推薦看看】面試
parse
(解析) => optimize
(優化) => generate
(節點生成)算法
parse
(解析)class
、style
等。若是要學正則,也能夠看看,寫的真不錯的。optimize
(優化)optimize
的主要做用是標記 static
靜態節點,這是 Vue 在編譯過程當中的一處優化,後面當 update 更新界面時,會有一個 patch
的過程, diff 算法會直接跳過靜態節點,從而減小了比較的過程,優化了 patch
的性能。generate
(節點生成)generate
是將 AST 轉化成 render function 字符串的過程,獲得結果是 render 的字符串以及 staticRenderFns 字符串。這個diff算法是處在optimize
(優化)階段的一個操做。編程
另外,各個框架的節點比對都是同級比對,即同一層級的相應子節點比對。
本文注重的是patch過程,具體的細節和邊界就沒有考慮。
==另 外 注 意==
- 三篇文章 diff 的講解,爲了方便展現 節點複用, 用了
children
保存內容,實際上這是不合理的,由於children不一樣還會遞歸補丁(patch)- diff也不是vue optimize的所有,只是其中一部分,例如compile時肯定節點類型,不一樣類型 不一樣的
mount/patch
處理方式等等。
先說思路,
如今好比說由若干個新老節點(preNodes
/ nextNodes
)。
nextNodes
)與老節點(preNodes
)一一比對,nextNodes
中上一個節點的前面newNodes
)與新節點( nextNodes
)比對去除多餘節點最後的結果就是由 nextNodes
、preNodes
產生的 節點樹(newNodes
)。
好比有以下新舊節點:
// 舊節點 const preNodes = [ {key: "k-1", children: "<span>old1</span>"}, {key: "k-2", children: "<span>old2</span>"}, {key: "k-3", children: "<span>old3</span>"}, {key: "k-4", children: "<span>old4</span>"}, {key: "k-5", children: "<span>old5</span>"}, {key: "k-6", children: "<span>old6</span>"}, ] //新節點,想要最後呈現的節點 const nextNodes = [ {key: "k-11", children: "<span>11</span>"}, {key: "k-0", children: "<span>0</span>"}, {key: "k-5", children: "<span>5</span>"}, {key: "k-13", children: "<span>13</span>"}, {key: "k-1", children: "<span>1</span>"}, {key: "k-7", children: "<span>7</span>"}, {key: "k-16", children: "<span>16</span>"}, {key: "k-3", children: "<span>3</span>"}, {key: "k-15", children: "<span>15</span>"}, {key: "k-17", children: "<span>7</span>"}, {key: "k-4", children: "<span>4</span>"}, {key: "k-6", children: "<span>6</span>"} ]
如上,在 preNodes
裏若是有老節點能夠複用,便用老節點替代他。指望的結果應該是
能夠看到老節點都獲得了複用~
下面就具體講解獲得最後新節點的過程。
i
是nextNodes的索引,j
是preNodes的索引,每一個nextNode都要與全部preNode節點做比對。preNodes
的上一個索引節點橙色標記,虛線標記當前遍歷的節點,綠色爲新增節點,j
標記的是相等的節點(若是有)
初始狀態,新生成節點(newNodes)基於老節點
i=0, lastIndex=0 (默認值),k-11
在 preNodes
未找到, 爲新節點。插入至 0
後
i=1, lastIndex=0(默認值),k-0
在 preNodes
未找到, 爲新節點。插入至 i -1
節點後面
i=2, lastIndex=4(更新後),k-5
在preNodes找到索引(j=4 > lastIndex
,index更新)插入(刪除+新增),複用節點
i=3, lastIndex=4,k-13
在preNodes未找到, 爲新節點。插入至 i -1
節點 後
i=4, lastIndex=4,k-1
在 preNodes
找到索引(j=0 < lastIndex)插入(刪除+新增)至 i - 1
後
i=5, lastIndex=0,k-7
在 preNodes
未找到,爲新節點。插入至 i-1
節點後
i=6, lastIndex=0,k-16
在 preNodes
未找到,爲新節點。插入至 i -1
節點後
i=7, lastIndex=4,k-3
在 preNodes
找到索引(j=2 < lastIndex)插入(刪除+新增)至 i - 1
後
i=8, lastIndex=0,k-5
在 preNodes
未找到,爲新節點。插入至 i -1
節點後
i=9, lastIndex=0,k-17
在 preNodes
未找到,爲新節點。插入至 i-1
節點 後
i=10, lastIndex=4,k-4
在preNodes找到索引(j=3 < lastIndex
)插入(刪除+新增)至 i - 1
後
i=11, lastIndex=6(更新後),k-6
在 preNodes
找到索引(j=5 > lastIndex
,index更新)插入(刪除+新增),複用節點
遍歷完成,清除多餘節點。
最終結果
建議仔細理解。
本節是代碼的具體實現,很少作講解,若是有任何疑慮建議精度圖例講解。或者留言交流,十分歡迎~~~
// React_diff() function React_diff(){ console.log(nextNodes.map(item => item.key)); const newNodes = JSON.parse(JSON.stringify(preNodes)); let lastIndex = 0; for(let i=0; i< nextNodes.length; i++){ const nextNode = nextNodes[i]; let find = false; for(let j=0; j< preNodes.length; j++){ const preNode = preNodes[j]; if(preNode.key === nextNode.key){ find = true; if(j < lastIndex){ // 須要移動 /* insertBefore 效果時遇到一樣的刪除原來的再添加, 這裏由於是數組模擬,因此須要先添加再刪除 , 數組處理有點不一樣,插入和刪除索引 都是從老節點找的。 [1,2,3].splice(0,0,4) => [4,1,2,3] */ const index = i > 0 ? i-1 : 0; const insertPos = newNodes.findIndex(node => node.key === nextNodes[index].key); const deleteIndex = newNodes.findIndex(node => node.key === preNode.key); //添加因爲 splice 是在某索引前面加。因此insertPos+1 newNodes.splice(insertPos+1, 0, preNode) //刪除 newNodes.splice(deleteIndex, 1); }else { lastIndex = j; } } } if(!find) {// 插入新節點 const index = i > 0 ? i-1 : 0; const insertPos = newNodes.findIndex(node => node.key === nextNodes[index].key); newNodes.splice(insertPos + 1, 0, nextNode); } } for(let i = newNodes.length - 1; i>=0; i--){ let find = false; for(let j =0; j<nextNodes.length; j++){ if(nextNodes[j].key === newNodes[i].key){ find = true; continue; } } if(!find){ newNodes.splice(i, 1); } } console.log('react diff: ', newNodes); }
優化點能夠利用key與index的作個對應關係,少一層遍歷.
function React_diff(){ const newNodes = JSON.parse(JSON.stringify(preNodes)); let lastIndex = 0; //產生nextNodes 的keyInIndexmap const prekeyInIndex = {}; for(let i =0; i< preNodes.length; i++){ prekeyInIndex[preNodes[i].key] = i; } for(let i=0; i< nextNodes.length; i++){ const nextNode = nextNodes[i]; let find = false; const j = prekeyInIndex[nextNode.key]; if(typeof j !== 'undefined') { find = true; const preNode = preNodes[j]; console.log(j, lastIndex); if(j < lastIndex){ // 須要移動 /* insertBefore 效果時遇到一樣的刪除原來的再添加, 這裏由於是數組模擬,因此須要先添加再刪除 , 數組處理有點不一樣,插入和刪除索引 都是從老節點找的。 [1,2,3].splice(0,0,4) => [4,1,2,3] */ const index = i > 0 ? i-1 : 0; const insertPos = newNodes.findIndex(node => node.key === nextNodes[index].key); const deleteIndex = newNodes.findIndex(node => node.key === preNode.key); //添加因爲 splice 是在某索引前面加。因此insertPos+1 newNodes.splice(insertPos+1, 0, preNode) //刪除 newNodes.splice(deleteIndex, 1); }else { lastIndex = j; } } if(!find) {// 插入新節點 const index = i > 0 ? i-1 : 0; const insertPos = newNodes.findIndex(node => node.key === nextNodes[index].key); newNodes.splice(insertPos + 1, 0, nextNode); } } //產生nextNodes 的keyInIndexmap const nextkeyInIndex = {}; for(let i =0; i< nextNodes.length; i++){ nextkeyInIndex[nextNodes[i].key] = i; } for(let i = newNodes.length - 1; i>=0; i--){ const idx = nextkeyInIndex[newNodes[i].key]; if(typeof idx === 'undefined'){ newNodes.splice(i, 1); } } console.log('react diff: ', newNodes); }
本文中例子只是爲了更好理解 diff 思路, patch 過程與真實狀況還有些差別(下面爲與vue patch的一些差別。可作參考)
insertbefore
、 delete
、add
。這些方法均是單獨封裝不能採用相對應的 Dom Api,由於 vue 不止用在瀏覽器環境
Vue@3.2
⇲ 已經出來了,React@18
也快了,哎,框架學不完。仍是多看看不變的東西吧(js, 設計模式, 數據結構,算法...)哎哎哎,,同志,看完怎麼不點贊,別看別人就說你呢,你幾個意思?
站在別人肩膀能看的更遠。
【推薦】vue-design
【掘金小冊】剖析Vue.js內部運行機制
【CSDN】React、Vue2.x、Vue3.0的diff算法
以上。