英文原文連接javascript
Virtual DOM很神奇,同時也比較複雜,難以理解。react,preact和類似的js庫都使用了virtual dom。然而,我找不到任何好的文章或者文檔,能夠詳細地又容易理解的方式來解釋它。所以我決定本身寫一篇。java
注意:文章篇幅較長,文中有大量的圖片來幫助理解。文中使用的是preact的代碼,由於它體積小,容易閱讀。可是它與React裏大部分的機率是保持一致的。但願閱讀完這篇文章後,你能夠更好地理解React和Preact這樣的類庫,甚至爲它們做出貢獻。node
在這篇文章中,我將列舉一個簡單的例子來解釋如下這些是如何工做的:react
Babel和JSX算法
建立VNode-一個簡單的virtual DOM元素babel
處理組件和子組件app
初始化渲染和建立一個DOM元素dom
從新渲染函數
移除DOM元素spa
替換DOM元素
這是一個簡單地可篩選的搜索應用,它包含了兩個組件FilteredList
和List
。List
組件用來渲染一組items(默認:"California"和"New York")。這個應用有一個搜索框,能夠根據字母來過濾列表項。很是地直觀:
咱們用jsx來寫組件,它會被babel轉換成純js,而後Preact的h
函數會將這段js轉換成DOM樹,最後Preact的Virtual DOM算法會將virtual DOM轉換成真實的DOM樹,來構建咱們的應用。
在深刻Virtual DOM的生命週期以前,咱們先理解一下jsx,由於它爲庫提供了入口。
在React,Preact這樣的類庫中,沒有HTML標籤,取而代之的是,一切都是javascript。因此咱們要在js中寫HTML標籤,可是在js中寫HTML簡直就是噩夢?
對於咱們的應用來講,咱們將會像下面這樣來寫HTML
這就是jsx的由來。jsx本質上就是容許咱們在javascript中書寫HTML!而且容許咱們在HTML中經過使用花括號來使用js。
jsx幫助咱們像下面這樣寫組件
jsx很酷,但它不是合法的js,而且最終咱們須要的是真實的DOM。JSX只是幫助編寫一個真實DOM的替代品,除此以外,它別無用處。因此咱們須要一種方法將它轉換成對應的JSON對象(也就是Virtual DOM),做爲轉化成真實DOM的輸入。咱們須要一個函數來實現這個功能。
在Preact中h
函數就是幹這件事情的,等同於React中的React.createElement
。
可是如何將jsx轉換成h
函數的調用呢?Babel就是幹這件事情的。Babel遍歷每一個jsx節點,並將它們轉換成h
函數調用。
默認狀況下,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 */
不只僅是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中,組件是如何被建立、更新和刪除的過程。同時也展現了像componentWillMount
這樣的生命週期事件是何時被調用的。
如今理解起來有些困難,因此咱們一步一步來拆解流程圖中的每種狀況。
高亮的部分展現了根據給定的組件生成的Virtual DOM樹。注意一點這裏並無爲子組件建立Virtual DOM
下面這幅圖展現了應用首次加載時發生的狀況。這個庫最後爲FilteredList組件建立了帶有子節點和屬性
的Virtual DOM
注意:在這個過程當中還調用了componentWillMount
和render
生命週期方法(在上圖中的綠色區塊)
此時,咱們有了一個Virtual DOM,div元素是父親節點,帶有一個input和一個list的子節點
在這一步中,它只是爲父親節點建立一個真實DOM,對於子節點,重複這個過程
此時,咱們在下圖中只有一個div展現出來
在這一步中,循環全部的子節點。在咱們的應用中,將會循環input和list
在這一步中,咱們將會處理葉子節點,因爲input有個父節點div,那麼咱們將會將input添加到div中做爲
子節點。而後流程轉向建立List
(第二個子節點是div)
此時,咱們的app長下面這樣
注意:在input
被建立以後,因爲它沒有任何子節點,並不會立馬就去循環和建立List組件。相反地,它會首先
把input
標籤添加到父節點div中去,完事以後再返回處理List標籤
如今控制流回到了步驟1.1,而且開始處理List
組件。可是因爲List
是一個組件,因此它會遍歷執行自身的render方法,從而得到一組VNodes,就像下面這樣:
當List
組件的循環完成時,它會返回List
的VNode,就像下面這樣:
對於每一個節點,它將會重複以上的每一步。一旦到達葉子節點,它將會被加入到父節點中去,而且重複這個過程。
下面的圖片展現了每一個節點是如何添加上去的(深度優先遍歷)
此時已經完成了處理過程。而後對於全部的組件,會調用componentDidMount
方法(從子組件開始,直到父組件)
注意:當一切準備就緒,一個真實DOM的引用會被添加到每一個組件的實例中。這個引用會在接下來的一些更新操做(建立、更新、刪除)被用來比較,避免重複建立相同的DOM節點
當輸入"cal"並按回車,這將會刪除第二個列表子元素,也就是一個葉子節點(New York),同時其餘父元素都會保留。
讓咱們看下這種情景下,流程是怎麼樣的
在初始化渲染以後,後面的每次改變都是一次"更新"。當建立VNodes時,更新週期與建立週期很是類似,而且再一次建立全部的VNodes。不過既然是更新(不是建立)組件,將會調用每一個組件和子組件相應的componentWillReceiveProps
,shouldComponentUpdate
和componentWillUpdate
方法。
另外,更新週期並不會從新建立已經存在的DOM元素。
以前提到過,在初始化加載期間,每一個組件都有一個指向真實DOM樹的引用。下面的圖展現了引用是如何尋找咱們的應用的。
當VNodes被建立後,每一個VNode的屬性都會與真實DOM的屬性相比較。若是真實DOM存在,循環將會轉移到下個節點
下面的圖展現了真實DOM和VNode之間的不一樣
因爲存在不一樣,真實DOM中的"New York"節點會被算法刪除掉,正以下面圖展現的那樣。這個算法也稱爲"componentDidUpdate"生命週期。
舉例:當輸入blabla
時,因爲不匹配"California"和"New York",咱們將不會渲染子組件List
。這意味着,咱們須要卸載整個組件
刪除一個組件相似於刪除一個單獨的節點。除此以外,當咱們刪除一個包含組件引用的節點,將會調用"componentWillUnmount",而後遞歸刪除全部的DOM元素。在刪除了全部的真實DOM元素以後,"componentDidUnmount"將會被調用。
下面的圖片展現了真實DOM元素"ul"包含了指向"List"組件的引用。
下面的圖片在流程圖中高亮了deleting/unmounting一個組件是如何工做的
但願這篇文章能幫助你理解Virtual DOM是如何工做的(至少在Preact中)