您的位置:首頁>正文

如何實現一個 Virtual DOM 及源碼分析

如何實現一個 Virtual DOM 及源碼分析

Virtual DOM演算法

web頁面有一個對應的DOM樹, 在傳統開發頁面時, 每次頁面需要被更新時, 都需要手動操作DOM來進行更新, 但是我們知道DOM操作對性能來說是非常不友好的, 會影響頁面的重排, 從而影響頁面的性能。 因此在React和VUE2.0+引入了虛擬DOM的概念, 他們的原理是:把真實的DOM樹轉換成javascript物件樹, 也就是虛擬DOM, 每次資料需要被更新的時候, 它會生成一個新的虛擬DOM, 並且和上次生成的虛擬DOM進行對比, 對發生變化的資料做批量更新。 ---(因為操作JS物件會更快, 更簡單, 比操作DOM來說)。

我們知道web頁面是由一個個HTML元素嵌套組合而成的,

當我們使用javascript來描述這些元素的時候, 這些元素可以簡單的被表示成純粹的JSON物件。

比如如下HTML代碼:

上面是真實的DOM樹結構, 我們可以使用javascript中的json物件來表示的話, 變成如下:

因此我們可以使用javascript物件表示DOM的資訊和結構, 當狀態變更的時候, 重新渲染這個javascript物件的結構, 然後可以使用新渲染的物件樹去和舊的樹去對比, 記錄兩顆樹的差異, 兩顆樹的差異就是我們需要對頁面真正的DOM操作, 然後把他們應用到真正的DOM樹上, 頁面就得到更新。 視圖的整個結構確實全渲染了, 但是最後操作DOM的時候,

只變更不同的地方。

因此我們可以總結一下 Virtual DOM演算法:

1. 用javascript物件結構來表示DOM樹的結構, 然後用這個樹構建一個真正的DOM樹, 插入到文檔中。

2. 當狀態變更的時候, 重新構造一顆新的物件樹, 然後使用新的物件樹與舊的物件樹進行對比, 記錄兩顆樹的差異。

3. 把記錄下來的差異用到步驟1所構建的真正的DOM樹上。 視圖就更新了。

演算法實現:

2-1 使用javascript物件類比DOM樹。

使用javascript來表示一個DOM節點, 有如上JSON的資料, 我們只需要記錄它的節點類型, 屬性和子節點即可。

element.js 代碼如下:

入口index.js代碼如下:

打開頁面即可看到效果。

2-2 比較兩顆虛擬DOM樹的差異及差異的地方進行dom操作

上面的div只會和同一層級的div對比, 第二層級的只會和第二層級的對比, 這樣的演算法的複雜度可以達到O(n).

但是在實際代碼中, 會對新舊兩顆樹進行一個深度優先的遍歷, 因此每個節點都會有一個標記。 如下圖所示:

在遍歷的過程中,每次遍歷到一個節點就把該節點和新的樹進行對比,如果有差異的話就記錄到一個物件裡面。

現在我們來看下我的目錄下 有哪些檔;然後分別對每個檔代碼進行解讀,看看做了哪些事情,舊的虛擬dom和新的虛擬dom是如何比較的,且是如何更新頁面的 如下目錄:

目錄結構如下:

首先是 index.js檔 頁面渲染完成後 變成如下html結構

假如發生改變後,變成如下結構

可以看到 新舊節點頁面資料的改變,h1標籤從屬性 顏色從紅色 變為藍色,p標籤的文本發生改變,ul新增了一項元素li。

基本的原理是:先渲染出頁面資料出來,生成第一個範本頁面,然後使用計時器會生成一個新的頁面資料出來,對新舊兩顆樹進行一個深度優先的遍歷,因此每個節點都會有一個標記。

然後調用diff方法對比物件新舊節點遍歷進行對比,找出兩者的不同的地方存入到一個物件裡面去,最後通過patch.js找出物件不同的地方,分別進行dom操作。

index.js代碼如下:

執行 var tree = renderTree()方法後,會調用element.js,

1. 依次遍歷子節點(從內到外調用)依次為 li, h1, p, ul, li和h1和p有一個文本子節點,因此遍歷完成後,count就等於1,

但是遍歷ul的時候,因為有一個子節點li,因此 count += 1; 所以調用完成後,ul的count等於2. 因此會對每個element屬性添加count屬性。對於最外層的container容器就是對每個子節點的依次增加,h1子節點默認為1,迴圈完成後 +1;因此變為2, p節點默認為1,迴圈完成後 +1,因此也變為2,ul為2,迴圈完成後 +1,因此變為3,因此container節點的count=2+2+3 = 7;

element.js部分代碼如下:

oldTree資料最終變成如下:

計時器 執行 var newTree = renderTree()後,調用方法步驟還是和第一步一樣:

2. 依次遍歷子節點(從內到外調用)依次為 li, h1, p, ul, li和h1和p有一個文本子節點,因此遍歷完成後,count就等於1,因為有2個子元素li,count都為1,因此ul每次遍歷依次在原來的基礎上加1,因此遍歷完成第一個li時候,ul中的count為2,當遍歷完成第二個li的時候,ul的count就為4了。因此ul中的count為4. 對於最外層的container容器就是對每個子元素依次增加。

所以 container節點的count = 2 + 2 + 5 = 9;

newTree資料最終變成如下資料:

var patches = diff(oldTree, newTree);

調用diff方法可以比較新舊兩棵樹節點的資料,把兩顆樹的不同節點找出來。(注意,查看diff對比資料的方法,找到不同的節點,可以查看這篇文章diff演算法)如下調用代碼:

執行deepWalk如下代碼:

1. 判斷新節點是否為null,如果為null,說明節點被刪除掉。

2. 判斷新舊節點是否為字串,如果為字串說明是文本節點,並且新舊兩個文本節點不同的話,存入陣列裡面去,如下代碼:

currentPatch.push({type: patch.TEXT, content: newNode});

patch.TEXT 為 patch.js裡面的 TEXT = 3;content屬性為新節點。

3. 如果新舊tagName相同的話,並且新舊節點的key相同的話,繼續比較新舊節點的屬性,如下代碼:

diffProps方法的代碼如下:

diffProps代碼解析如下:

如上代碼是 判斷舊節點的屬性值是否在新節點中找到,如果找不到的話,count++; 把新節點的屬性值賦值給 propsPatches 存儲起來。

如上代碼是 判斷新節點的屬性是否能在舊節點中找到,如果找不到的話,count++; 把新節點的屬性值賦值給 propsPatches 存儲起來。

最後如果count 等於0的話,說明所有屬性都是相同的話,所以不需要做任何變化。否則的話,返回新增的屬性。

如果有 propsPatches 的話,執行如下代碼:

因此currentPatch陣列裡面也有對應的更新的屬性,props就是需要更新的屬性物件。

繼續代碼:

如上代碼判斷子節點是否相同,diffChildren代碼如下:

如上代碼:var diffs = listDiff(oldChildren, newChildren, 'key'); 新舊節點按照key來比較,目前key為undefined,所以diffs 為如下:

newChildren = diffs.children;

oldChildren資料如下:

接著就是遍歷 oldChildren, 第一次遍歷時 leftNode 為null,因此 currentNodeIndex = currentNodeIndex + 1 = 0 + 1 = 1; 不是第一次遍歷,那麼leftNode都為上一次遍歷的子節點,因此不是第一次遍歷的話,那麼 currentNodeIndex = currentNodeIndex + leftNode.count + 1;

然後遞迴呼叫 deepWalk(child, newChild, currentNodeIndex, patches); 方法,接著把child賦值給leftNode,leftNode = child;

所以一直遞迴遍歷,最終把不相同的節點 會存儲到 currentPatch 陣列內。最後執行

把對應的currentPatch 存儲到 patches物件內中的對應項,最後就返回 patches物件。

4. 返回到index.js 代碼內,把兩顆不相同的樹節點的提取出來後,需要調用patch.js方法傳進;把不相同的節點應用到真正的DOM上.

不相同的節點 patches資料如下:

如下代碼調用:

patch(root, patches);

執行patch方法,代碼如下:

deepWalk 代碼如下:

1. 首次調用patch的方法,root就是container的節點,因此調用deepWalk方法,因此 var currentPatches = patches[0] = undefined,

var len = node.childNodes ? node.childNodes.length : 0; 因此 len = 3; 很明顯該子節點的長度為3,因為子節點有 h1, p, 和ul元素;

2. 然後進行for迴圈,獲取該父節點的子節點,因此第一個子節點為 h1 元素,walker.index++; 因此walker.index = 1; 再進行遞迴 deepWalk(child, walker, patches); 此時子節點為h1, walker.index為1, 因此獲取 currentPatches = patches[1]; 獲取值,再獲取 h1的子節點的長度,len = 1; 然後再for迴圈,獲取child為文本節點,此時 walker.index++; 所以此時walker.index 為2, 在調用deepwalk方法遞迴,因此再繼續獲取 currentPatches = patches[2]; 值為undefined,再獲取len = 0; 因為文本節點麼有子節點,所以for迴圈跳出,所以判斷currentPatches是否有值,因為此時 currentPatches 為undefined,所以遞迴結束,再返回到 h1元素上來,所以currentPatches = patches[1]; 所以有值,所以調用 applyPatches()方法來更新dom元素。

3. 繼續迴圈 i, 此時i = 1; 獲取子節點 child = p元素,walker.index++,此時walker.index = 3, 繼續調用 deepWalk方法,獲取 var currentPatches = patches[walker.index] = patches[3]的值,var len = 1; 因為p元素下有一個子節點(文本節點),再進for迴圈,此時 walker.index++; 因此walker.index = 4; child此時為文本節點,在調用 deepwalk方法的時候,再獲取var currentPatches = patches[walker.index] = patches[4]; 再執行len 代碼的時候 len = 0;因此跳出for迴圈,判斷 currentPatches是否有值,有值的話,更新對應的DOM元素。

4. 繼續迴圈i = 2; 獲取子節點 child = ul元素,walker.index++; 此時walker.index = 5; 在調用deepWalk方法遞迴,因此再獲取 var currentPatches = patches[walker.index] = patches[5]; 然後len = 1, 因為ul元素下有一個li元素,在繼續for迴圈遍歷,獲取子節點li,此時walker.index++; walker.index = 6; 再遞迴呼叫deepwalk方法,再獲取var currentPatches = patches[walker.index] = patches[6]; len = 1; 因為li的元素下有一個文本節點,再進行for迴圈,此時child為文本節點,walker.index++;此時walker.index = 7; 再執行 deepwalk方法,再獲取 var currentPatches = patches[walker.index] = patches[7]; 這時候 len = 0了,因此跳出for迴圈,判斷 當前的currentPatches是否有值,沒有,就跳出,然後再返回ul元素,獲取該自己li的時候,walker.index 等於5,因此var currentPatches = patches[walker.index] = patches[5]; 然後判斷 currentPatches是否有值,有值就進行更新DOM元素。

最後就是 applyPatches 方法更新dom元素了,如下代碼:

判斷類型,替換對應的屬性和節點。

最後就是對子節點進行排序的操作,代碼如下:

遍歷moves,判斷moves.type 是等於0還是等於1,等於0的話是刪除操作,等於1的話是新增操作。比如現在moves值變成如下:

node節點 就是 'ul'元素,var staticNodeList = utils.toArray(node.childNodes); 把ul的舊子節點li轉成Array形式,由於沒有屬性key,所以直接跳到下面遍歷代碼來,遍歷moves,獲取某一項的索引index,判斷move.type 等於0 還是等於1, 目前等於1,是新增一項,但是沒有key,因此調用move.item.render(); 渲染完後,對staticNodeList陣列裡面的舊節點的li項從第二項開始插入節點li,然後執行node.insertBefore(insertNode, node.childNodes[index] || null); node就是ul父節點,insertNode節點插入到 node.childNodes[1]的前面。因此把在第二項的前面插入第一項。

如下圖所示:

在遍歷的過程中,每次遍歷到一個節點就把該節點和新的樹進行對比,如果有差異的話就記錄到一個物件裡面。

現在我們來看下我的目錄下 有哪些檔;然後分別對每個檔代碼進行解讀,看看做了哪些事情,舊的虛擬dom和新的虛擬dom是如何比較的,且是如何更新頁面的 如下目錄:

目錄結構如下:

首先是 index.js檔 頁面渲染完成後 變成如下html結構

假如發生改變後,變成如下結構

可以看到 新舊節點頁面資料的改變,h1標籤從屬性 顏色從紅色 變為藍色,p標籤的文本發生改變,ul新增了一項元素li。

基本的原理是:先渲染出頁面資料出來,生成第一個範本頁面,然後使用計時器會生成一個新的頁面資料出來,對新舊兩顆樹進行一個深度優先的遍歷,因此每個節點都會有一個標記。

然後調用diff方法對比物件新舊節點遍歷進行對比,找出兩者的不同的地方存入到一個物件裡面去,最後通過patch.js找出物件不同的地方,分別進行dom操作。

index.js代碼如下:

執行 var tree = renderTree()方法後,會調用element.js,

1. 依次遍歷子節點(從內到外調用)依次為 li, h1, p, ul, li和h1和p有一個文本子節點,因此遍歷完成後,count就等於1,

但是遍歷ul的時候,因為有一個子節點li,因此 count += 1; 所以調用完成後,ul的count等於2. 因此會對每個element屬性添加count屬性。對於最外層的container容器就是對每個子節點的依次增加,h1子節點默認為1,迴圈完成後 +1;因此變為2, p節點默認為1,迴圈完成後 +1,因此也變為2,ul為2,迴圈完成後 +1,因此變為3,因此container節點的count=2+2+3 = 7;

element.js部分代碼如下:

oldTree資料最終變成如下:

計時器 執行 var newTree = renderTree()後,調用方法步驟還是和第一步一樣:

2. 依次遍歷子節點(從內到外調用)依次為 li, h1, p, ul, li和h1和p有一個文本子節點,因此遍歷完成後,count就等於1,因為有2個子元素li,count都為1,因此ul每次遍歷依次在原來的基礎上加1,因此遍歷完成第一個li時候,ul中的count為2,當遍歷完成第二個li的時候,ul的count就為4了。因此ul中的count為4. 對於最外層的container容器就是對每個子元素依次增加。

所以 container節點的count = 2 + 2 + 5 = 9;

newTree資料最終變成如下資料:

var patches = diff(oldTree, newTree);

調用diff方法可以比較新舊兩棵樹節點的資料,把兩顆樹的不同節點找出來。(注意,查看diff對比資料的方法,找到不同的節點,可以查看這篇文章diff演算法)如下調用代碼:

執行deepWalk如下代碼:

1. 判斷新節點是否為null,如果為null,說明節點被刪除掉。

2. 判斷新舊節點是否為字串,如果為字串說明是文本節點,並且新舊兩個文本節點不同的話,存入陣列裡面去,如下代碼:

currentPatch.push({type: patch.TEXT, content: newNode});

patch.TEXT 為 patch.js裡面的 TEXT = 3;content屬性為新節點。

3. 如果新舊tagName相同的話,並且新舊節點的key相同的話,繼續比較新舊節點的屬性,如下代碼:

diffProps方法的代碼如下:

diffProps代碼解析如下:

如上代碼是 判斷舊節點的屬性值是否在新節點中找到,如果找不到的話,count++; 把新節點的屬性值賦值給 propsPatches 存儲起來。

如上代碼是 判斷新節點的屬性是否能在舊節點中找到,如果找不到的話,count++; 把新節點的屬性值賦值給 propsPatches 存儲起來。

最後如果count 等於0的話,說明所有屬性都是相同的話,所以不需要做任何變化。否則的話,返回新增的屬性。

如果有 propsPatches 的話,執行如下代碼:

因此currentPatch陣列裡面也有對應的更新的屬性,props就是需要更新的屬性物件。

繼續代碼:

如上代碼判斷子節點是否相同,diffChildren代碼如下:

如上代碼:var diffs = listDiff(oldChildren, newChildren, 'key'); 新舊節點按照key來比較,目前key為undefined,所以diffs 為如下:

newChildren = diffs.children;

oldChildren資料如下:

接著就是遍歷 oldChildren, 第一次遍歷時 leftNode 為null,因此 currentNodeIndex = currentNodeIndex + 1 = 0 + 1 = 1; 不是第一次遍歷,那麼leftNode都為上一次遍歷的子節點,因此不是第一次遍歷的話,那麼 currentNodeIndex = currentNodeIndex + leftNode.count + 1;

然後遞迴呼叫 deepWalk(child, newChild, currentNodeIndex, patches); 方法,接著把child賦值給leftNode,leftNode = child;

所以一直遞迴遍歷,最終把不相同的節點 會存儲到 currentPatch 陣列內。最後執行

把對應的currentPatch 存儲到 patches物件內中的對應項,最後就返回 patches物件。

4. 返回到index.js 代碼內,把兩顆不相同的樹節點的提取出來後,需要調用patch.js方法傳進;把不相同的節點應用到真正的DOM上.

不相同的節點 patches資料如下:

如下代碼調用:

patch(root, patches);

執行patch方法,代碼如下:

deepWalk 代碼如下:

1. 首次調用patch的方法,root就是container的節點,因此調用deepWalk方法,因此 var currentPatches = patches[0] = undefined,

var len = node.childNodes ? node.childNodes.length : 0; 因此 len = 3; 很明顯該子節點的長度為3,因為子節點有 h1, p, 和ul元素;

2. 然後進行for迴圈,獲取該父節點的子節點,因此第一個子節點為 h1 元素,walker.index++; 因此walker.index = 1; 再進行遞迴 deepWalk(child, walker, patches); 此時子節點為h1, walker.index為1, 因此獲取 currentPatches = patches[1]; 獲取值,再獲取 h1的子節點的長度,len = 1; 然後再for迴圈,獲取child為文本節點,此時 walker.index++; 所以此時walker.index 為2, 在調用deepwalk方法遞迴,因此再繼續獲取 currentPatches = patches[2]; 值為undefined,再獲取len = 0; 因為文本節點麼有子節點,所以for迴圈跳出,所以判斷currentPatches是否有值,因為此時 currentPatches 為undefined,所以遞迴結束,再返回到 h1元素上來,所以currentPatches = patches[1]; 所以有值,所以調用 applyPatches()方法來更新dom元素。

3. 繼續迴圈 i, 此時i = 1; 獲取子節點 child = p元素,walker.index++,此時walker.index = 3, 繼續調用 deepWalk方法,獲取 var currentPatches = patches[walker.index] = patches[3]的值,var len = 1; 因為p元素下有一個子節點(文本節點),再進for迴圈,此時 walker.index++; 因此walker.index = 4; child此時為文本節點,在調用 deepwalk方法的時候,再獲取var currentPatches = patches[walker.index] = patches[4]; 再執行len 代碼的時候 len = 0;因此跳出for迴圈,判斷 currentPatches是否有值,有值的話,更新對應的DOM元素。

4. 繼續迴圈i = 2; 獲取子節點 child = ul元素,walker.index++; 此時walker.index = 5; 在調用deepWalk方法遞迴,因此再獲取 var currentPatches = patches[walker.index] = patches[5]; 然後len = 1, 因為ul元素下有一個li元素,在繼續for迴圈遍歷,獲取子節點li,此時walker.index++; walker.index = 6; 再遞迴呼叫deepwalk方法,再獲取var currentPatches = patches[walker.index] = patches[6]; len = 1; 因為li的元素下有一個文本節點,再進行for迴圈,此時child為文本節點,walker.index++;此時walker.index = 7; 再執行 deepwalk方法,再獲取 var currentPatches = patches[walker.index] = patches[7]; 這時候 len = 0了,因此跳出for迴圈,判斷 當前的currentPatches是否有值,沒有,就跳出,然後再返回ul元素,獲取該自己li的時候,walker.index 等於5,因此var currentPatches = patches[walker.index] = patches[5]; 然後判斷 currentPatches是否有值,有值就進行更新DOM元素。

最後就是 applyPatches 方法更新dom元素了,如下代碼:

判斷類型,替換對應的屬性和節點。

最後就是對子節點進行排序的操作,代碼如下:

遍歷moves,判斷moves.type 是等於0還是等於1,等於0的話是刪除操作,等於1的話是新增操作。比如現在moves值變成如下:

node節點 就是 'ul'元素,var staticNodeList = utils.toArray(node.childNodes); 把ul的舊子節點li轉成Array形式,由於沒有屬性key,所以直接跳到下面遍歷代碼來,遍歷moves,獲取某一項的索引index,判斷move.type 等於0 還是等於1, 目前等於1,是新增一項,但是沒有key,因此調用move.item.render(); 渲染完後,對staticNodeList陣列裡面的舊節點的li項從第二項開始插入節點li,然後執行node.insertBefore(insertNode, node.childNodes[index] || null); node就是ul父節點,insertNode節點插入到 node.childNodes[1]的前面。因此把在第二項的前面插入第一項。

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示