理解 Virtual DOM
原文出處:https://github.com/y8n/blog/issues/5
前言
使用過React的同學對於Virtual DOM並不陌生,作為React的重要核心概念,Virtual DOM憑藉其高效的diff演算法,讓我們不用關心應用的效能問題,毫無顧忌地修改各種資料狀態。在實際的開發中,我們並不需要關注Virtual DOM在一個框架中是如何執行的,但是理解Virtual DOM的實現原理卻是非常有必要的,同時也有助於我們更加深入React。
一、前端應用狀態管理
在日益複雜的前端應用中,狀態管理是一個經常被提及的話題,從早期的刀耕火種時代到jQuery,再到現在流行的MVVM時代,狀態管理的形式發生了翻天覆地的變化,我們再也不用維護茫茫多的事件回撥、監聽來更新檢視,轉而使用使用雙向資料繫結,只需要維護相應的資料狀態,就可以自動更新檢視,極大提高開發效率。
但是,雙向資料繫結也並不是唯一的辦法,還有一個非常粗暴有效的方式:一旦資料發生變化,重新繪製整個檢視,也就是重新設定一下innerHTML。這樣的做法確實簡單、粗暴、有效,但是如果只是因為區域性一個小的資料發生變化而更新整個檢視,價效比未免太低了,而且,像事件,獲取焦點的輸入框等,都需要重新處理。所以,對於小的應用或者說區域性的小檢視,這樣處理完全是可以的,但是面對複雜的大型應用,這樣的做法不可取。
說到這裡,你會說這跟Virtual DOM有什麼關係呢?其實Virtual DOM就是這麼做的,只是在高效的diff演算法計算下,避免對整棵DOM樹進行變更,而是進行鍼對性的檢視變更,將效率做到最優化。
二、什麼是Virtual DOM
Virtual DOM的概念有很多解釋,從我的理解來看,主要是三個方面,分別是:一個物件,兩個前提,三個步驟。
一個物件指的是Virtual DOM是一個基本的JavaScript物件,也是整個Virtual DOM樹的基本。
兩個前提分別是JavaScript很快和直接操作DOM很慢,這是Virtual DOM得以實現的兩個基本前提。得益於V8引擎的出現,讓JavaScript可以高效地執行,在效能上有了極大的提高。直接操作DOM的低效和JavaScript的高效相對比,為Virtual DOM的產生提供了大前提。
三個步驟指的是Virtual DOM的三個重要步驟,分別是:生成Virtual DOM樹、對比兩棵樹的差異、更新檢視。這三個步驟的具體實現也是本文將簡述的一大重點。
三、Virtual DOM三板斧
下面就將介紹Virtual DOM的三個步驟具體的含義以及實現思路。
1、生成Virtual DOM樹
DOM是前端工程師最常接觸的內容之一,一個DOM節點包含了很多的內容,但是一個抽象出一個DOM節點卻只需要三部分:節點型別,節點屬性、子節點。所以圍繞這三個部分,我們可以使用JavaScript簡單地實現一棵DOM樹,然後給節點實現渲染方法,就可以實現虛擬節點到真是DOM的轉化。
2、對比兩棵樹的差異
比較兩棵DOM樹的差異是Virtual DOM演算法最核心的部分,這也是我們常說的的 Virtual DOM的diff演算法。在比較的過程中,我們只比較同級的節點,非同級的節點不在我們的比較範圍內,這樣既可以滿足我們的需求,又可以簡化演算法實現。
比較“樹”的差異,首先是要對樹進行遍歷,常用的有兩種遍歷演算法,分別是深度優先遍歷和廣度優先遍歷,一般的diff演算法中都採用的是深度優先遍歷。對新舊兩棵樹進行一次深度優先的遍歷,這樣每個節點都會有一個唯一的標記。在遍歷的時候,每遍歷到一個節點就把該節點和新的樹的同一個位置的節點進行對比,如果有差異的話就記錄到一個物件裡面。
例如,上面的div和新的div有差異,當前的標記是0,那麼:patches[0] = [{difference}, {difference}, ...]
同理p
是patches[1]
,ul
是patches[3]
,以此類推。這樣當遍歷完整棵樹的時候,就可以獲得一個完整的差異物件。
在這個差異物件中記錄了有改變的節點,每一個發生改變的內容也不盡相同,但也是有跡可循,常見的差異包括四種,分別是:
- 替換節點
- 增加/刪除子節點
- 修改節點屬性
- 改變文字內容
所以在記錄差異的時候要根據不同的差異型別,記錄不同的內容。
3、更新檢視
在第二步得到整棵樹的差異之後,就可以根據這些差異的不同型別,對DOM進行鍼對性的更新。與四種差異型別相對應的,是更新檢視時具體的更新方法,分別是:
replaceChild()
appendChild()
/removeChild()
setAttribute()
/removeAttribute()
textContent
四、動手實現Virtual DOM
對原理有了一定的認識之後,自然是動手實現一番了,GitHub上有很多對Virtual DOM的實現,比如https://github.com/livoras/simple-virtual-dom/、https://github.com/Matt-Esch/virtual-dom等,我也對其進行了一個基本的實現,比較簡陋,傳送門。
五、進一步思考
Virtual DOM的原理和實現的說明已經結束了,但是對於Virtual DOM的思考遠沒有結束,Virtual DOM 對前端開發的影響難道就只是一堆演算法嗎?
1、效能對比
首先,先來看一下效能,在諸多的Virtual DOM實現中,都會強調演算法的高效,那麼在實際的使用中,Virtual DOM的效能到底如何呢?
上圖是對一個簡單的DOM樹進行不同方式的操作,由左邊的結構更新為右邊的結構,通過原生操作、jQuery、Virtual DOM和React四種方式,在Chrome的timeline中得到的效能對比,在這個圖中,我們並沒有看出Virtual DOM或者React的優勢,通過對比我們發現,原生的操作要比其他三種方式快,而其他三種方式就相差無幾了。當然,這樣一個簡單測試並沒有說明什麼,測試的DOM結構簡單,和我們平時面對的業務場景不是一個量級,代表不了什麼,但是起碼我們可以看到,這種情況下好像Virtual DOM並沒有我們想象的效能優勢。
在接下來的測試中我們增加測試量。上圖分別是使用原生操作、Virtual DOM和React三種方式進行兩類測試:插入10000個節點100次和修改3000個節點的屬性100次。分別取這100次的耗時最大值、最小值和平均值。從圖中我們可以看到明顯的差異,Virtual DOM和React的差異可以理解,畢竟我們自己實現的Virtual DOM沒有那麼龐大,只是針對虛擬DOM而實現的,比React快一點可以理解,但是原生的操作比Virtual DOM和React都要快得多,這又是怎麼一回事,好像和我們預想的不一樣,回到最初,我們提到,Virtual DOM的產生前提之一就是直接操作DOM很慢,現在看來直接操作不但不慢,反而快了很多,這不得不讓我產生了懷疑,是我對Virtual DOM的理解有誤還是對DOM的理解有誤呢?
2、再次審視Virtual DOM
框架存在的意義是什麼?是提高效能?提高開發效率?亦或是其他用途,每個人對框架的理解不同,答案也不盡相同。但是不得不承認,存在框架的情況下,專案的可維護性有了極大的提高,而對於其他方面就要做出犧牲,比如效能。在上面的效能測試中,其實完全走入了一個誤區,在測試中我們用到的原生的操作其實是“人為”地對操作進行優化之後的結果,而如果拋開人為優化的前提,最終的結果可能就不是這樣了。**Virtual DOM的優勢不在於單次的操作,而是在大量、頻繁的資料更新下,能夠對檢視進行合理、高效的更新。**這一點是原生操作遠遠無法替代的。
到此為止,再次審視Virtual DOM,可以簡單得出如下結論:
- Virtual DOM 在犧牲部分效能的前提下,增加了可維護性,這也是很多框架的通性
- 實現了對DOM的集中化操作,在資料改變時先對虛擬DOM進行修改,再反映到真實的DOM中,用最小的代價來更新DOM,提高效率
- 打開了函式式UI程式設計的大門
- 可以渲染到DOM以外的端,比如ReactNative
六、結語
本文對Virtual DOM有一個簡單的介紹,包括實現的部分也很簡單,甚至對列表的diff演算法也偷工減料,跟多高階的特性也沒有涉及,比如事件繫結、生命週期、JSX語法等,如果加上這些內容,就是一個小型版的React了。
本文旨在讓大家瞭解並認識Virtual DOM的基本概念、組成和實現,同時對Virtual DOM更深層的意義有所瞭解,這樣在以後用到相關的框架的時候也不會兩眼一抹黑了,起碼在效能優化上有點認識,比如列表要帶上key
這樣基本的優化操作。