虛擬DOM內部是如何工作的

感謝譯者xianfeng授權發佈。
原文鏈接:The Inner Workings Of Virtual DOM
譯者: xianfeng,阿里前端開發工程師。三年前端開發經驗,熱愛前端,喜歡鑽研新技術。博客地址:http://imjue.com
責編:陳秋歌,尋求報道或者投稿請發郵件至chenqg#csdn.net。
瞭解更多前沿技術資訊,獲取深度技術文章推薦,請關注CSDN研發頻道微博

Virtual DOM很神奇,同時也比較複雜,難以理解。React,preact和相似的JavaScript庫都使用了Virtual Dom。然而,我找不到任何好的文章或者文檔,可以詳細地又容易理解的方式來解釋它。因此我決定自己寫一篇。

注意:文章篇幅較長,文中有大量的圖片來幫助理解。文中使用的是preact的代碼,因爲它體積小,容易閱讀。但是它與React裏大部分的概率是保持一致的。希望閱讀完這篇文章後,你可以更好地理解React和Preact這樣的類庫,甚至爲它們作出貢獻。

在這篇文章中,我將列舉一個簡單的例子來解釋以下這些是如何工作的:

  • Babel和JSX;
  • 創建VNode-一個簡單的virtual DOM元素;
  • 處理組件和子組件;
  • 初始化渲染和創建一個DOM元素;
  • 重新渲染;
  • 移除DOM元素;
  • 替換DOM元素。

The App

這是一個簡單地可篩選的搜索應用,它包含了兩個組件FilteredList和List。List組件用來渲染一組items(默認:”California”和”New York”)。這個應用有一個搜索框,可以根據字母來過濾列表項。非常地直觀:

概覽圖

我們用JSX來寫組件,它會被babel轉換成純JavaScript,然後Preact的 h 函數會將這段JavaScript轉換成DOM樹,最後Preact的Virtual DOM算法會將virtual DOM轉換成真實的DOM樹,來構建我們的應用。

在深入Virtual DOM的生命週期之前,我們先理解一下JSX,因爲它爲庫提供了入口。

Babel And JSX

在React,Preact這樣的類庫中,沒有HTML標籤,取而代之的是,一切都是JavaScript。所以我們要在JavaScript中寫HTML標籤,但是在JavaScript中寫HTML簡直就是噩夢。

對於我們的應用來說,我們將會像下面這樣來寫HTML。

這就是JSX的由來。JSX本質上就是允許我們在JavaScript中書寫HTML!並且允許我們在HTML中通過使用花括號來使用JavaScript。

JSX幫助我們像下面這樣寫組件:

JSX轉換成JavaScript

JSX很酷,但它不是合法的JavaScript,並且最終我們需要的是真實的DOM。JSX只是幫助編寫一個真實DOM的替代品,除此之外,它別無用處。所以我們需要一種方法將它轉換成對應的JSON對象(也就是Virtual DOM),作爲轉化成真實DOM的輸入。我們需要一個函數來實現這個功能。

在Preact中h函數就是幹這件事情的,等同於React中的React.createElement

但是如何將JSX轉換成h函數的調用呢?Babel就是幹這件事情的。Babel遍歷每個JSX節點,並將它們轉換成h函數調用。

圖片描述

Babel JSX(React vs Preact)

默認情況下,Babel將JSX轉換成React.createElement調用。

圖片描述

但是我們可以很容易地將函數名修改成任何名稱,只需要在babelrc中配置一下即可。

Option 1:
//.babelrc
{   "plugins": [
      ["transform-react-jsx", { "pragma": "h" }]
     ]
}
Option 2:
//Add the below comment as the 1st line in every JSX file
/** @jsx h */

圖片描述

掛載到真實DOM

不僅僅是render中的代碼會被轉換成h函數,最初的掛載也會!

這就是代碼執行開始的地方。

//Mount to real DOM
render(<FilteredList/>, document.getElementById(‘app’));
//Converted to "h":
render(h(FilteredList), document.getElementById(‘app’));

h函數的輸出

h函數將jsx轉化後的內容轉換成Virtual DOM節點。一個Preact的Virtual DOM節點就是一個簡單的代表了單個包含屬性和子節點的DOM節點的JS對象,如下所示:

{
   "nodeName": "",
   "attributes": {},
   "children": []
}

比如,應用的input標籤對應的Virtual DOM如下:

{
   "nodeName": "input",
   "attributes": {
    "type": "text",
    "placeholder": "Search",
    "onChange": ""
   },
   "children": []
}

注意:h函數並不是創建整棵樹!它只是簡單地創建某個節點的JS對象。但是因爲render方法。

好了,讓我們看看Virtual DOM是如何工作的。

Preact中的Virtual DOM算法

在下面的流程圖中,展示了在Preact中,組件是如何被創建、更新和刪除的過程。同時也展示了像componentWillMount這樣的生命週期事件是什麼時候被調用的。

圖片描述

現在理解起來有些困難,所以我們一步一步來拆解流程圖中的每種情況。

情景1:初始化App

1.1 創建Virtual DOM

高亮的部分展示了根據給定的組件生成的Virtual DOM樹。注意一點這裏並沒有爲子組件創建Virtual DOM。

圖片描述

下面這幅圖展示了應用首次加載時發生的情況。這個庫最後爲FilteredList組件創建了帶有子節點和屬性的Virtual DOM。

圖片描述

注意:在這個過程中還調用了componentWillMountrender生命週期方法(在上圖中的綠色區塊)。

此時,我們有了一個Virtual DOM,div元素是父親節點,帶有一個input和一個list的子節點。

1.2 如果不是一個組件,則創建真實的DOM

在這一步中,它只是爲父親節點創建一個真實DOM,對於子節點,重複這個過程。

圖片描述

此時,我們在下圖中只有一個div展示出來。

圖片描述

1.3 對於子元素重複這個過程

在這一步中,循環所有的子節點。在我們的應用中,將會循環input和list。

圖片描述

1.4 處理孩子節點和添加到父親節點

在這一步中,我們將會處理葉子節點,由於input有個父節點div,那麼我們將會將input添加到div中作爲子節點。然後流程轉向創建List(第二個子節點是div)。

圖片描述

此時,我們的App長下面這樣。

圖片描述

注意:在input被創建之後,由於它沒有任何子節點,並不會立馬就去循環和創建List組件。相反地,它會首先把input標籤添加到父節點div中去,完事之後再返回處理List標籤。

1.5 處理子節點

現在控制流回到了步驟1.1,並且開始處理List組件。但是由於List是一個組件,所以它會遍歷執行自身的render方法,從而獲得一組VNodes,就像下面這樣:

圖片描述

List組件的循環完成時,它會返回List的VNode,就像下面這樣:

圖片描述

1.6 對於所有的子節點,重複步驟1.1到1.4

對於每個節點,它將會重複以上的每一步。一旦到達葉子節點,它將會被加入到父節點中去,並且重複這個過程。

圖片描述

下面的圖片展示了每個節點是如何添加上去的(深度優先遍歷)。

圖片描述

1.7 處理完成

此時已經完成了處理過程。然後對於所有的組件,會調用componentDidMount方法(從子組件開始,直到父組件)。

圖片描述

注意:當一切準備就緒,一個真實DOM的引用會被添加到每個組件的實例中。這個引用會在接下來的一些更新操作(創建、更新、刪除)被用來比較,避免重複創建相同的DOM節點。

情景2:刪除葉子節點

當輸入”cal”並按回車,這將會刪除第二個列表子元素,也就是一個葉子節點(New York),同時其他父元素都會保留。

圖片描述

讓我們看下這種情景下,流程是怎麼樣的。

2.1 創建VNodes

在初始化渲染之後,後面的每次改變都是一次”更新”。當創建VNodes時,更新週期與創建週期非常相似,並且再一次創建所有的VNodes。不過既然是更新(不是創建)組件,將會調用每個組件和子組件相應的componentWillReceiveProps,shouldComponentUpdatecomponentWillUpdate方法。

另外,更新週期並不會重新創建已經存在的DOM元素。

圖片描述

2.2 使用真實DOM引用,避免創建重複的節點

之前提到過,在初始化加載期間,每個組件都有一個指向真實DOM樹的引用。下面的圖展示了引用是如何尋找我們的應用的。

圖片描述

當VNodes被創建後,每個VNode的屬性都會與真實DOM的屬性相比較。如果真實DOM存在,循環將會轉移到下個節點

圖片描述

2.3 如果在真實DOM中有其它的節點,則刪除。

下面的圖展示了真實DOM和VNode之間的不同。

圖片描述

由於存在不同,真實DOM中的”New York”節點會被算法刪除掉,正如下面圖展示的那樣。這個算法也稱爲”componentDidUpdate”生命週期。

圖片描述

情景3-卸載整個組件

舉例:當輸入blabla時,由於不匹配”California”和”New York”,我們將不會渲染子組件List。這意味着,我們需要卸載整個組件。

圖片描述

圖片描述

刪除一個組件類似於刪除一個單獨的節點。除此之外,當我們刪除一個包含組件引用的節點,將會調用”componentWillUnmount”,然後遞歸刪除所有的DOM元素。在刪除了所有的真實DOM元素之後,”componentDidUnmount”將會被調用。

下面的圖片展示了真實DOM元素”ul”包含了指向”List”組件的引用。

圖片描述

下面的圖片在流程圖中高亮了deleting/unmounting一個組件是如何工作的。

圖片描述

最後

希望這篇文章能幫助你理解Virtual DOM是如何工作的(至少在Preact中)。

歡迎加入“CSDN前端開發者”羣,與更多專家、技術同行進行熱點、難點技術交流。請掃描以下二維碼申請入羣。

圖片描述

發佈了342 篇原創文章 · 獲贊 104 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章