哈嘍,我是前端小躺平。今天要和你聊聊虛擬 DOM 相關內容,React 選它,真的是為了效能嗎?

在過去的十年裡,前端技術日新月異。從最早的純靜態頁面,到 jQuery 一統江湖,再到近幾年大火的 MVVM 框架——研發模式升級這件事情對於前端來說,好像成了某種常態。其實

研發模式不斷演進的背後,恰恰蘊含著前端人對 “DOM 操作” 這一核心動作的持續思考和改進

。而虛擬 DOM,正是先驅們在這個過程中孕育出的一顆明珠。

在 MVVM 框架這個領域分支,有一道至今仍然非常經典的面試題:“

為什麼我們需要虛擬 DOM?

”。

這個問題比較常見的回答思路是:“

DOM 操作是很慢的,而 JS 卻可以很快

,直接操作 DOM 可能會導致頻繁的迴流與重繪,JS 不存在這些問題。因此虛擬 DOM 比原生 DOM 更快”。

但真的是這樣嗎?

快速搞定虛擬 DOM 的兩個“大問題”

溫故而知新,在一切開始之前,我們先來複習一下虛擬 DOM是什麼。

虛擬 DOM(Virtual DOM)本質上是

JS 和 DOM 之間的一個對映快取

,它在形態上表現為一個能夠描述 DOM 結構及其屬性資訊的

JS 物件

。虛擬 DOM 在 React 中的形態如下圖所示:

React 選擇虛擬 DOM,真的是為了效能嗎?

就這個示例來說,你需要把握住以下兩點:

虛擬 DOM 是 JS 物件

虛擬 DOM 是對真實 DOM 的描述

這樣就基本解決了虛擬 DOM“是什麼”的問題,接下來我們看看 React 中的虛擬 DOM 大致是如何工作的。虛擬 DOM 在 React 元件的掛載階段和更新階段都會作為“關鍵人物”出鏡,其參與的工作流程如下:

掛載階段

,React 將結合 JSX 的描述,構建出虛擬 DOM 樹,然後透過 ReactDOM。render 實現虛擬 DOM 到真實 DOM 的對映(觸發渲染流水線);

更新階段

,頁面的變化在作用於真實 DOM 之前,會先作用於虛擬 DOM,虛擬 DOM 將在 JS 層藉助演算法先對比出具體有哪些真實 DOM 需要被改變,然後再將這些改變作用於真實 DOM。

OK,現在我們用最短的時間迅速搞定了“What”和“How”兩個大問題。或許過程有些粗糙,但這絲毫不影響你吃透本課時的核心內容,也就是虛擬 DOM 背後的“Why”。

“為什麼需要虛擬 DOM?”“虛擬 DOM 的優勢何在?”“虛擬 DOM 是否伴隨更好的效能?” ,要想回答好這無窮無盡的為什麼,你千萬

不要點對點

地去看待問題本身。虛擬 DOM 相對於過往的 DOM 操作解決方案來說,是一個新生事物。要想理解一個新生事物存在、發展的合理性,我們必須

將其放在一個足夠長的、合理的上下文中去討論

接下來我要做的事情,就是幫你把這個上下文完全地鋪開。當你清楚了虛擬 DOM 在歷史長河中的位置後,將能迅速地理解它到底幫助前端開發解決掉了什麼問題,彼時,所有的答案都會躍然紙上。

歷史長河中的 DOM 操作解決方案

現在,讓我們一起來回顧一下,那些沒有虛擬 DOM 的苦逼日子。

1. 原生 JS 支配下的“人肉 DOM” 時期

在前端這個工種的萌芽階段,前端頁面“展示”的屬性遠遠強於其“互動”的屬性,這就導致 JS 的定位只能是“輔助”:在很長一段時間裡,前端工程師們會花費大量的時間去實現靜態的 DOM,待一切結束後,再補充少量 JS,實現一些類似於拖拽、隱藏、幻燈片之類的“特效”。

在這個階段,作為前端開發者來說,雖然我們一無所有,但過得很快樂——簡單的業務需求決定了我們不需要去做太多或太複雜的 DOM 操作,原生 JS,足矣。

2.解放生產力的先導階段:jQuery 時期

時代的浪潮滾滾向前,人們很快就不再滿足於簡單到有些無聊的互動效果,開始追求更加豐富的使用者體驗,與之而來的就是大量 DOM 操作需求帶來的前端開發工作量的激增。在這個過程中,早期前端們漸漸地明白了一個道理:原生 JS 提供的 DOM API,實在是太太太太太難用了。

為了能夠實現高效的開發,jQuery 首先解決的就是“API 不好使”這個問題——它將 DOM API 封裝為了相對簡單和優雅的形式,同時一口氣做掉了跨瀏覽器的相容工作,並且提供了鏈式 API 呼叫、外掛擴充套件等一系列能力用於進一步解放生產力。最終達到的效果正是我們喜聞樂見的“寫得更少,做得更多”。

jQuery 使 DOM 操作變得簡單、快速,並且始終確保其形式穩定、可用性穩定。雖然現在看來並不完美,但在當年能夠一統江湖,確實當之無愧。

3.民智初啟:早期模板引擎方案

jQuery 幫助我們能夠以更舒服的姿勢操作 DOM,但它並不能從根本上解決 DOM 操作量過大情況下前端側的壓力。

它就好比是一個

手持吸塵器

,雖然可以幫助我們更加方便快速地清潔某一處的灰塵,但是要想清潔多個位置的灰塵,你仍然需要拿著它四處奔走。這樣雖說不必再彎腰擦地板,但還是避免不了跑斷腿的結局。

既然“手持吸塵器”滿足不了日益膨脹的 DOM 操作需求,那我們想要的到底是什麼呢?是一個只需要接收命令,就能夠自己跑來跑去、把活幹得漂漂亮亮的“掃地機器人”。

而模板引擎方案,正是“掃地機器人”的雛形。

注:由於模板引擎更傾向於點對點解決煩瑣 DOM 操作的問題,它在能力和定位上既不能夠、也不打算替換掉 jQuery,兩者是和諧共存的。因此這裡不存在“模板引擎時期”,只有“模板引擎方案”。

怎麼理解模板這個概念呢?我們來看一個例子。比如說我現在手裡有一套員工資料,資料內容如下:

const staff = [

{

name: ‘修言’,

career: ‘前端’

},

{

name: ‘翠翠’,

career: ‘編輯’

},

{

name: ‘花花’,

career: ‘運營’

}

現在我想要在前端用表格展示這一堆資料,我就可以遵循模板的語法,把它塞進模板(template)裡去。下面就是一個典型的模板語法使用示例:

{% staff。forEach(function(person){ %}

{% }); %}

{% student。name %}{% student。age %}

可以看出,模板語法其實就是把 JS 和 HTML 結合在一起的一種規則,而模板引擎做的事情也非常容易理解。

把 staff 這個資料來源讀進去,塞到預置好的 HTML 模板裡,然後把兩者融合在一起,吐出一段目標字串給你。這段字串的內容,其實就是一份標準的、可用於渲染的 HTML 程式碼,它將對應一個 DOM 元素。最後,將這個 DOM 元素掛載到頁面中去,整個模板的渲染流程也就走完了。

這個過程可以用虛擬碼來表示,如下所示:

// 資料和模板融合出 HTML 程式碼

var targetDOM = template({data: students})

// 新增到頁面中去

document。body。appendChild(targetDOM)

當然,實際的過程會比我們描述的要複雜一些。這裡我補充一下模板引擎的實現思路,供感興趣的同學參考。模板引擎一般需要做下面幾件事情:

讀取 HTML 模板並解析它,分離出其中的 JS 資訊;

將解析出的內容拼接成字串,動態生成 JS 程式碼;

執行動態生成的 JS 程式碼,吐出“目標 HTML”;

將“目標 HTML”賦值給 innerHTML,觸發渲染流水線,完成真實 DOM 的渲染。

使用模板引擎方案來渲染資料是非常爽的:每次資料發生變化時,我們都不用關心到底是哪裡的資料變了,也不用手動去點對點完成 DOM 的修改。只

需要關注的僅僅是資料和資料變化本身

,DOM 層面的改變模板引擎會幫我們做掉。

如此看來,模板引擎像極了一個只需要接收命令,就能夠把活幹得漂漂亮亮的“掃地機器人”!可惜的是,模板引擎出現的契機雖然是為了使使用者介面與業務資料相分離,但實際的應用場景基本侷限在“實現高效的字串拼接”這一個點上,因此不能指望它去做太複雜的事情。尤其令人無法接受的是,

它在效能上的表現並不盡如人意

:由於不夠“智慧”,它更新 DOM 的方式是將已經渲染出 DOM 整體登出後再整體重渲染,並且不存在更新緩衝這一說。在 DOM 操作頻繁的場景下,模板引擎可能會直接導致頁面卡死。

注:請注意小標題中“早期”這個限定詞——本課時所討論的“模板引擎”概念,指的是虛擬 DOM 思想推而廣之以前,相對原始的一類模板引擎,這類模板引擎曾經主導了一個時代。但時下來看,越來越多的模板引擎正在引入虛擬 DOM,模板引擎最終也將走向現代化。

雖然指望模板引擎實現生產力解放有些天方夜譚,但模板引擎在思想上無疑具備高度的先進性:允許程式設計師只關心資料而不必關心 DOM 細節的這一操作,和 React 的“資料驅動檢視”思想如出一轍,實在是高!

那該怎麼辦呢?

jQuery 救不了加班寫 DOM 操作的前端,模板引擎也救不了,那該怎麼辦呢?

這時候有一批仁人志士,興許是從模板引擎的設計思想上得到了啟發,他們明確了要走“資料驅動檢視”這條基本道路,於是便沿著這個思路往下摸索:模板引擎的資料驅動檢視方案,核心問題在於對真實 DOM 的修改過於“大刀闊斧”,導致了 DOM 操作的範圍過大、頻率過高,進而可能會導致糟糕的效能。然後這幫人就想啊:既然操作真實 DOM 對效能損耗這麼大,那我操作假的 DOM 不就行了?

沿著這個思路再往下走,就有了我們都愛的虛擬 DOM。

注:出於嚴謹,還是要解釋下。真實歷史中的虛擬 DOM 創作過程,到底有沒有向模板引擎去學習,這個暫時無從考證。但是按照前端發展的過程來看,模板引擎和虛擬 DOM 確實在思想上存在遞進關係,很多場景下,面試官也可能會問及兩者的關係。因此在此處,我採取了這樣一種表述方式,希望能夠幫助你更好地把握住問題的關鍵所在。

虛擬 DOM 是如何解決問題的

讀到這裡,你可能對虛擬 DOM 已經有些感覺了。這裡我來幫你總結下,同樣是將使用者介面與資料相分離,模板引擎是這樣做的:

React 選擇虛擬 DOM,真的是為了效能嗎?

而在虛擬 DOM 的加持下,事情變成了這樣:

React 選擇虛擬 DOM,真的是為了效能嗎?

注意圖中的“模板”二字加了引號,這是因為虛擬 DOM 在實現上並不總是藉助模板。比如 React 就使用了 JSX,前面咱們著重講過,JSX 本質不是模板,而是一種使用體驗和模板相似的 JS 語法糖。

區別就在於多出了一層虛擬 DOM 作為緩衝層。這個緩衝層帶來的利好是:當 DOM 操作(渲染更新)比較頻繁時,它會先將前後兩次的虛擬 DOM 樹進行對比,定位出具體需要更新的部分,生成一個“補丁集”,最後只把“補丁”打在需要更新的那部分真實 DOM 上,實現精準的“

差量更新

”。這個過程對應的虛擬 DOM 工作流如下圖所示:

React 選擇虛擬 DOM,真的是為了效能嗎?

注:圖中的 diff 和 patch 其實都是函式名,這些函式取材於一個獨立的虛擬 DOM 庫。之所以寫明瞭具體流程對應的函式名,是因為我發現面試的時候,很多面試官習慣於用函式名指代過程,但不少人不清楚這個對應關係(尤其是 patch),會非常影響作答。這裡提前幫你把這個坑給規避掉。

還需要說明的一點是, 虛擬 DOM 和 Redux 一樣,不依附於任何具體的框架。學習虛擬 DOM,實際上可以完全不借助 React;但學習 React,就必須瞭解虛擬 DOM。如果你對虛擬 DOM 的具體實現過程感興趣,可以在這個 GitHub 倉庫裡檢視其原始碼細節。

回到主線劇情上來,差量更新可以確保虛擬 DOM 既能夠提供高效的開發體驗(開發者只需要關心資料),又能夠保持過得去的效能(只更新發生了變化的那部分 DOM),實在是妙啊!

React 選用虛擬 DOM,真的是為了更好的效能嗎?

讀到這裡,相信你至少已經 get 到了這樣一個點:在整個 DOM 操作的演化過程中,主要矛盾並不在於效能,而在於開發者寫得爽不爽,在於

研發體驗/研發效率

。虛擬 DOM 不是別的,正是前端開發們為了追求更好的研發體驗和研發效率而創造出來的高階產物。

虛擬 DOM 並不一定會帶來更好的效能,React 官方也從來沒有把虛擬 DOM 作為效能層面的賣點對外輸出過。

虛擬 DOM 的優越之處在於,它能夠在提供更爽、更高效的研發模式(也就是函式式的 UI 程式設計方式)的同時,仍然保持一個還不錯的效能

效能問題屬於前端領域複雜度比較高的問題。當我們量化效能的時候,往往並不能只追求一個單一的資料,而是需要結合具體的參照物、渲染的階段、資料的吞吐量等各種要素來作分情況的討論。

拿前面講過的模板渲染來舉例,我們可以對比一下它和虛擬 DOM 在效能開銷上的差異。兩者的渲染工作流對比如下圖所示:

React 選擇虛擬 DOM,真的是為了效能嗎?

React 選擇虛擬 DOM,真的是為了效能嗎?

從圖中可以看出,模板渲染的步驟1,和虛擬 DOM 渲染的步驟1、2都屬於 JS 範疇的行為,這兩者是具備可比性的,我們放在一起來看:動態生成 HTML 字串的過程本質是對字串的拼接,對效能的消耗是有限的;而虛擬 DOM 的構建和 diff 過程邏輯則相對複雜,它不可避免地涉及遞迴、遍歷等耗時操作。

因此在 JS 行為這個層面,模板渲染勝出

模板渲染的步驟3,和虛擬 DOM 的步驟3 都屬於 DOM 範疇的行為,兩者具備可比性,因此我們仍然可以愉快地對比下去:模板渲染是全量更新,而虛擬 DOM 是差量更新。

乍一看好像差量更新一定比全量更新高效,但你需要考慮這樣一種情況:資料內容變化非常大(或者說整個發生了改變),促使差量更新計算出來的結果和全量更新極為接近(或者說完全一樣)。

在這種情況下,DOM 更新的工作量基本一致,而虛擬 DOM 卻伴隨著開銷更大的 JS 計算,此時會出現的一種現象就是模板渲染和虛擬 DOM 在整體效能上難分伯仲:若兩者最終計算出的 DOM 更新內容完全一致,那麼虛擬 DOM 大機率不敵模板渲染;但只要兩者在最終 DOM 操作量上拉開那麼一點點的差距,虛擬 DOM 就將具備戰勝模板渲染的底氣。

因為虛擬 DOM 的劣勢主要在於 JS 計算的耗時,而 DOM 操作的能耗和 JS 計算的能耗根本不在一個量級

,極少量的 DOM 操作耗費的效能足以支撐大量的 JS 計算。

當然,上面討論的這種情況相對來說比較極端。在實際的開發中,更加高頻的場景是這樣的:我每次 setState 的時候只修改少量的資料,比如一個物件中的某幾個屬性,再比如一個數組中的某幾個元素。在這樣的場景下,模板渲染和虛擬 DOM 之間 DOM 操作量級的差距就完全拉開了,虛擬 DOM 將在效能上具備絕對的優勢。

注意,此處的結論是“在 XXX 場景下,虛擬 DOM 相對於 XXX 具備效能優勢”,它是有嚴格限定條件的。有人不到黃河心不死,可能又要問“那虛擬 DOM 對比 jQuery 呢?”“那虛擬 DOM 對比原生 DOM 呢?”。

我想說的是,效能問題不能一概而論,而且咱都講到這個份上了,就不要再鑽效能這個牛角尖了。jQuery、原生 DOM 在思維模式上來說和虛擬 DOM 截然不同,強行比較意義不大。

前面又是分析又是舉例地說了這麼多,其實我最終希望你明白的事情只有一件:

虛擬 DOM 的價值不在效能,而在別處

。因此想要從效能角度來把握虛擬 DOM 的優勢,無異於南轅北轍。偏偏在面試場景下,10 個人裡面有 9 個都走這條歧路,最後9個人裡面自然沒有一個能自圓其說,實在讓人惋惜。

那麼虛擬 DOM 的價值到底是什麼呢?

最後我想和你聊聊虛擬 DOM 的價值,這又是一個宏大的、容易說錯話的命題。當我們談及某個事物的價值時,其實就像是在稱讚一個美女,不同的人自然有著不同看待美女的視角。此處我無意於給你一個天衣無縫的標準答案(這樣的答案想必也不存在),而是希望能夠站在“虛擬 DOM 解決了哪些關鍵問題”這個視角,和你分享一些業內關於虛擬 DOM 的共識。

虛擬 DOM 解決的關鍵問題有以下兩個。

研發體驗/研發效率的問題:這一點前面已經反覆強調過,DOM 操作模式的每一次革新,背後都是前端對效率和體驗的進一步追求。虛擬 DOM 的出現,為資料驅動檢視這一思想提供了高度可用的載體,使得前端開發能夠基於函式式 UI 的程式設計方式實現高效的宣告式程式設計。

跨平臺的問題:虛擬 DOM 是對真實渲染內容的一層抽象。若沒有這一層抽象,那麼檢視層將和渲染平臺緊密耦合在一起,為了描述同樣的檢視內容,你可能要分別在 Web 端和 Native 端寫完全不同的兩套甚至多套程式碼。但現在中間多了一層描述性的虛擬 DOM,它描述的東西可以是真實 DOM,也可以是iOS 介面、安卓介面、小程式……同一套虛擬 DOM,可以對接不同平臺的渲染邏輯,從而實現“一次編碼,多端執行”,如下圖所示。其實說到底,跨平臺也是研發提效的一種手段,它在思想上和1是高度呼應的。

React 選擇虛擬 DOM,真的是為了效能嗎?

以上,虛擬 DOM 還有非常多的亮點值得我們去挖掘。

前端小躺平,期待和你共同進步。