虛擬DOM內部是如何工做的

英文原文連接javascript

Virtual DOM很神奇,同時也比較複雜,難以理解。react,preact和類似的js庫都使用了virtual dom。然而,我找不到任何好的文章或者文檔,能夠詳細地又容易理解的方式來解釋它。所以我決定本身寫一篇。java

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

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

  1. Babel和JSX算法

  2. 建立VNode-一個簡單的virtual DOM元素babel

  3. 處理組件和子組件app

  4. 初始化渲染和建立一個DOM元素dom

  5. 從新渲染函數

  6. 移除DOM元素spa

  7. 替換DOM元素

The app

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

img

概覽圖

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

在深刻Virtual DOM的生命週期以前,咱們先理解一下jsx,由於它爲庫提供了入口。

Babel And JSX

在React,Preact這樣的類庫中,沒有HTML標籤,取而代之的是,一切都是javascript。因此咱們要在js中寫HTML標籤,可是在js中寫HTML簡直就是噩夢?

對於咱們的應用來講,咱們將會像下面這樣來寫HTML

img
img

這就是jsx的由來。jsx本質上就是容許咱們在javascript中書寫HTML!而且容許咱們在HTML中經過使用花括號來使用js。
jsx幫助咱們像下面這樣寫組件
img
img

jsx轉換成js

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

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

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

Babel JSX(React vs Preact)

默認狀況下,Babel將jsx轉換成React.createElement調用
img
可是咱們能夠很容易地將函數名修改爲任何名稱,只須要在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 */

img

掛載到真實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這樣的生命週期事件是何時被調用的。
img

如今理解起來有些困難,因此咱們一步一步來拆解流程圖中的每種狀況。

情景1:初始化app

1.1 建立Virtual DOM

高亮的部分展現了根據給定的組件生成的Virtual DOM樹。注意一點這裏並無爲子組件建立Virtual DOM
img
下面這幅圖展現了應用首次加載時發生的狀況。這個庫最後爲FilteredList組件建立了帶有子節點和屬性
的Virtual DOM
img
注意:在這個過程當中還調用了componentWillMountrender生命週期方法(在上圖中的綠色區塊)

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

1.2 若是不是一個組件,則建立真實的DOM

在這一步中,它只是爲父親節點建立一個真實DOM,對於子節點,重複這個過程
img
此時,咱們在下圖中只有一個div展現出來
img

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

在這一步中,循環全部的子節點。在咱們的應用中,將會循環input和list
img

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

在這一步中,咱們將會處理葉子節點,因爲input有個父節點div,那麼咱們將會將input添加到div中做爲
子節點。而後流程轉向建立List(第二個子節點是div)
img
此時,咱們的app長下面這樣
img

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

1.5 處理子節點

如今控制流回到了步驟1.1,而且開始處理List組件。可是因爲List是一個組件,因此它會遍歷執行自身的render方法,從而得到一組VNodes,就像下面這樣:
img
List組件的循環完成時,它會返回List的VNode,就像下面這樣:
img

1.6 對於全部的子節點,重複步驟1.1到1.4

對於每一個節點,它將會重複以上的每一步。一旦到達葉子節點,它將會被加入到父節點中去,而且重複這個過程。
img
下面的圖片展現了每一個節點是如何添加上去的(深度優先遍歷)
img

1.7 處理完成

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

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

情景2:刪除葉子節點

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

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

2.1 建立VNodes

在初始化渲染以後,後面的每次改變都是一次"更新"。當建立VNodes時,更新週期與建立週期很是類似,而且再一次建立全部的VNodes。不過既然是更新(不是建立)組件,將會調用每一個組件和子組件相應的componentWillReceiveProps,shouldComponentUpdatecomponentWillUpdate方法。

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

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

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

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

2.3 若是在真實DOM中有其它的節點,則刪除

下面的圖展現了真實DOM和VNode之間的不一樣

img
因爲存在不一樣,真實DOM中的"New York"節點會被算法刪除掉,正以下面圖展現的那樣。這個算法也稱爲"componentDidUpdate"生命週期。
img

情景3-卸載整個組件

舉例:當輸入blabla時,因爲不匹配"California"和"New York",咱們將不會渲染子組件List。這意味着,咱們須要卸載整個組件
img
img
刪除一個組件相似於刪除一個單獨的節點。除此以外,當咱們刪除一個包含組件引用的節點,將會調用"componentWillUnmount",而後遞歸刪除全部的DOM元素。在刪除了全部的真實DOM元素以後,"componentDidUnmount"將會被調用。
下面的圖片展現了真實DOM元素"ul"包含了指向"List"組件的引用。
img
下面的圖片在流程圖中高亮了deleting/unmounting一個組件是如何工做的
img

最後

但願這篇文章能幫助你理解Virtual DOM是如何工做的(至少在Preact中)

相關文章
相關標籤/搜索