vue核心之虛擬DOM(vdom)

原文鏈接:https://www.jianshu.com/p/af0b398602bc

本文是在網上閱讀而來,用於記錄文檔,便於學習。因此轉載路徑爲:https://www.jianshu.com/p/af0b398602bc

一、真實DOM和其解析流程?

瀏覽器渲染引擎工作流程都差不多,大致分爲5步,創建DOM樹——創建StyleRules——創建Render樹——佈局Layout——繪製Painting
第一步,用HTML分析器,分析HTML元素,構建一顆DOM樹(標記化和樹構建)。
第二步,用CSS分析器,分析CSS文件和元素上的inline樣式,生成頁面的樣式表。
第三步,將DOM樹和樣式表,關聯起來,構建一顆Render樹(這一過程又稱爲Attachment)。每個DOM節點都有attach方法,接受樣式信息,返回一個render對象(又名renderer)。這些render對象最終會被構建成一顆Render樹。
第四步,有了Render樹,瀏覽器開始佈局,爲每個Render樹上的節點確定一個在顯示屏上出現的精確座標。
第五步,Render樹和節點顯示座標都有了,就調用每個節點paint方法,把它們繪製出來。
DOM樹的構建是文檔加載完成開始的?構建DOM數是一個漸進過程,爲達到更好用戶體驗,渲染引擎會盡快將內容顯示在屏幕上。它不必等到整個HTML文檔解析完畢之後纔開始構建render數和佈局。
Render樹是DOM樹和CSSOM樹構建完畢纔開始構建的嗎?這三個過程在實際進行的時候又不是完全獨立,而是會有交叉。會造成一邊加載,一遍解析,一遍渲染的工作現象。
CSS的解析是從右往左逆向解析的(從DOM樹的下-上解析比上-下解析效率高),嵌套標籤越多,解析越慢。

webkit渲染引擎工作流程

二、JS操作真實DOM的代價!

用我們傳統的開發模式,原生JS或JQ操作DOM時,瀏覽器會從構建DOM樹開始從頭到尾執行一遍流程。在一次操作中,我需要更新10個DOM節點,瀏覽器收到第一個DOM請求後並不知道還有9次更新操作,因此會馬上執行流程,最終執行10次。例如,第一次計算完,緊接着下一個DOM更新請求,這個節點的座標值就變了,前一次計算爲無用功。計算DOM節點座標值等都是白白浪費的性能。即使計算機硬件一直在迭代更新,操作DOM的代價仍舊是昂貴的,頻繁操作還是會出現頁面卡頓,影響用戶體驗。

三、爲什麼需要虛擬DOM,它有什麼好處?

Web界面由DOM樹(樹的意思是數據結構)來構建,當其中一部分發生變化時,其實就是對應某個DOM節點發生了變化,

虛擬DOM就是爲了解決瀏覽器性能問題而被設計出來的。如前,若一次操作中有10次更新DOM的動作,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內容保存到本地一個JS對象中,最終將這個JS對象一次性attch到DOM樹上,再進行後續操作,避免大量無謂的計算量。所以,用JS對象模擬DOM節點的好處是,頁面的更新可以先全部反映在JS對象(虛擬DOM)上,操作內存中的JS對象的速度顯然要更快,等更新完成後,再將最終的JS對象映射成真實的DOM,交由瀏覽器去繪製。

四、實現虛擬DOM

例如一個真實的DOM節點。
真實DOM
我們用JS來模擬DOM節點實現虛擬DOM。
虛擬DOM
其中的Element方法具體怎麼實現的呢?
Element方法實現
第一個參數是節點名(如div),第二個參數是節點的屬性(如class),第三個參數是子節點(如ul的li)。除了這三個參數會被保存在對象上外,還保存了key和count。其相當於形成了虛擬DOM樹。
虛擬DOM樹
有了JS對象後,最終還需要將其映射成真實DOM
虛擬DOM對象映射成真實DOM

我們已經完成了創建虛擬DOM並將其映射成真實DOM,這樣所有的更新都可以先反應到虛擬DOM上,如何反應?需要用到Diff算法。

兩棵樹如果完全比較時間複雜度是O(n^3),但參照《深入淺出React和Redux》一書中的介紹,React的Diff算法的時間複雜度是O(n)。要實現這麼低的時間複雜度,意味着只能平層的比較兩棵樹的節點,放棄了深度遍歷。這樣做,似乎犧牲掉了一定的精確性來換取速度,但考慮到現實中前端頁面通常也不會跨層移動DOM元素,這樣做是最優的。

深度優先遍歷,記錄差異

。。。。

Diff操作

在實際代碼中,會對新舊兩棵樹進行一個深度的遍歷,每個節點都會有一個標記。每遍歷到一個節點就把該節點和新的樹進行對比,如果有差異就記錄到一個對象中。

下面我們創建一棵新樹,用於和之前的樹進行比較,來看看Diff算法是怎麼操作的。
old Tree
new Tree
平層Diff,只有以下4種情況:
1、節點類型變了,例如下圖中的P變成了H3。我們將這個過程稱之爲REPLACE。直接將舊節點卸載並裝載新節點。舊節點包括下面的子節點都將被卸載,如果新節點和舊節點僅僅是類型不同,但下面的所有子節點都一樣時,這樣做效率不高。但爲了避免O(n^3)的時間複雜度,這樣是值得的。這也提醒了開發者,應該避免無謂的節點類型的變化,例如運行時將div變成p沒有意義。
2、節點類型一樣,僅僅屬性或屬性值變了。我們將這個過程稱之爲PROPS。此時不會觸發節點卸載和裝載,而是節點更新。
查找不同屬性方法

3、文本變了,文本對也是一個Text Node,也比較簡單,直接修改文字內容就行了,我們將這個過程稱之爲TEXT。

4、移動/增加/刪除 子節點,我們將這個過程稱之爲REORDER。看一個例子,在A、B、C、D、E五個節點的B和C中的BC兩個節點中間加入一個F節點。

例子
我們簡單粗暴的做法是遍歷每一個新虛擬DOM的節點,與舊虛擬DOM對比相應節點對比,在舊DOM中是否存在,不同就卸載原來的按上新的。這樣會對F後邊每一個節點進行操作。卸載C,裝載F,卸載D,裝載C,卸載E,裝載D,裝載E。效率太低。
粗暴做法
如果我們在JSX裏爲數組或枚舉型元素增加上key後,它能夠根據key,直接找到具體位置進行操作,效率比較高。常見的最小編輯距離問題,可以用Levenshtein Distance算法來實現,時間複雜度是O(M*N),但通常我們只要一些簡單的移動就能滿足需要,降低精確性,將時間複雜度降低到O(max(M,N))即可。
最終Diff出來的結果

映射成真實DOM

虛擬DOM有了,Diff也有了,現在就可以將Diff應用到真實DOM上了。深度遍歷DOM將Diff的內容更新進去。

根據Diff更新DOM

根據Diff更新DOM
我們會有兩個虛擬DOM(js對象,new/old進行比較diff),用戶交互我們操作數據變化new虛擬DOM,old虛擬DOM會映射成實際DOM(js對象生成的DOM文檔)通過DOM fragment操作給瀏覽器渲染。當修改new虛擬DOM,會把newDOM和oldDOM通過diff算法比較,得出diff結果數據表(用4種變換情況表示)。再把diff結果表通過DOM fragment更新到瀏覽器DOM中。

虛擬DOM的存在的意義?vdom 的真正意義是爲了實現跨平臺,服務端渲染,以及提供一個性能還算不錯 Dom 更新策略。vdom 讓整個 mvvm 框架靈活了起來

Diff算法只是爲了虛擬DOM比較替換效率更高,通過Diff算法得到diff算法結果數據表(需要進行哪些操作記錄表)。原本要操作的DOM在vue這邊還是要操作的,只不過用到了js的DOM fragment來操作dom(統一計算出所有變化後統一更新一次DOM)進行瀏覽器DOM一次性更新。其實DOM fragment我們不用平時發開也能用,但是這樣程序員寫業務代碼就用把DOM操作放到fragment裏,這就是框架的價值,程序員才能專注於寫業務代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章