根據 React 歷史來聊如何理解虛擬 DOM

文章首發自我的博客javascript

最近我發現不少面試題裏面都有「如何理解虛擬 DOM」這個題,我以爲這個題應該沒有想象中那麼好答,由於不少人沒有真正理解虛擬 DOM 它的價值所在,我這篇從虛擬 DOM 的誕生過程來引出它的價值以及歷史地位,幫助你深刻的理解它。php

什麼是虛擬DOM

本質上是 JavaScript 對象,這個對象就是更加輕量級的對 DOM 的描述。前端

對,就是這麼簡單!java

就是一個複雜一點的對象而已,沒什麼好說的,重點是爲何要有這個東西,以及有了這個描述有什麼好處纔是咱們今天要介紹的內容。react

爲何要有虛擬DOM

再談爲何要用虛擬 DOM 以前,先來聊一聊 React 是怎麼誕生的,畢竟在瞭解歷史背景,再去思考他的誕生,就知道是必然會出現的。git

再查了不少關於 React 的歷史相關的文章,這篇文章我感受比較值得令我信服:React 是怎樣煉成的面試

衆所周知,Facebook 是 PHP 大戶,因此 React 最開始的靈感就來至於 PHP。算法

字符串拼接時代 - 2004

在 2004 年這個時候,你們都還在用 PHP 的字符串拼接來開發網站:編程

$str = '<ul>';
foreach ($talks as $talk) {
  $str += '<li>' . $talk->name . '</li>';
}
$str += '</ul>';
複製代碼

這種方式代碼寫出來很差看不說,還容易形成 XSS 等安全問題。小程序

應對方法是對用戶的任何輸入都進行轉義(Escape)。可是若是對字符串進行屢次轉義,那麼反轉義的次數也必須是相同的,不然會沒法獲得原內容。若是又不當心把 HTML 標籤(Markup)給轉義了,那麼 HTML 標籤會直接顯示給用戶,從而致使不好的用戶體驗。

XHP 時代 - 2010

到了 2010 年,爲了更加高效的編碼,同時也避免轉義 HTML 標籤的錯誤,Facebook 開發了 XHP 。XHP 是對 PHP 的語法拓展,它容許開發者直接在 PHP 中使用 HTML 標籤,而再也不使用字符串。

$content = <ul />;
foreach ($talks as $talk) {
  $content->appendChild(<li>{$talk->name}</li>);
}
複製代碼

這樣的話,全部的 HTML 標籤都使用不一樣於 PHP 的語法,咱們能夠輕易的分辨哪些須要轉義哪些不須要轉義。

不久的後來,Facebook 的工程師又發現他們還能夠建立自定義標籤,並且經過組合自定義標籤有助於構建大型應用。

JSX - 2013

到了 2013 年,前端工程師 Jordan Walke 向他的經理提出了一個大膽的想法:把 XHP 的拓展功能遷移到 JS 中。首要任務是須要一個拓展來讓 JS 支持 XML 語法,該拓展稱爲 JSX。由於當時因爲 Node.js 在 Facebook 已經有不少實踐,因此很快就實現了 JSX。

能夠猜測一下爲何要遷移到 js 中,我猜測應該是先後端分離致使的。

const content = (
  <TalkList> { talks.map(talk => <Talk talk={talk} />)} </TalkList> ); 複製代碼

React

在這個時候,就有另一個很棘手的問題,那就是在進行更新的時候,須要去操做 DOM,傳統 DOM API 細節太多,操做複雜,因此就很容易出現 Bug,並且代碼難以維護。

而後就想到了 PHP 時代的更新機制,每當有數據改變時,只須要跳到一個由 PHP 全新渲染的新頁面便可。

從開發者的角度來看的話,這種方式開發應用是很是簡單的,由於它不須要擔憂變動,且界面上用戶數據改變時全部內容都是同步的。

爲此 React 提出了一個新的思想,即始終總體「刷新」頁面

當發生先後狀態變化時,React 會自動更新 UI,讓咱們從複雜的 UI 操做中解放出來,使咱們只需關於狀態以及最終 UI 長什麼樣。

下面看看局部刷新和總體刷新的區別。

圖片來自於極客時間王沛老師的《React進階與實戰》

局部刷新:

// 下面是僞代碼
var ul = find(ul) // 先找到 ul
ul.append(`<li>${message3}</li>`) //而後再將message3插到最後

// 想一想若是是不插到最後一個,而是插到中間的第n個
var ul = find(ul) // 先找到 ul
var preli = find(li(n-1)) // 再找到 n-1 的一個 li
preli.next(`<li>${message3}</li>`) // 再插入到 n-1 個的後面
複製代碼

總體刷新:

UI = f(messages) // 總體刷新 3 條消息,只須要調用 f 函數

// 這個是在初始渲染的時候就定義好的,更新的時候不用去管
function f(messages) {
	return <ul> {messages.map(message => <li>{ message }</li>)} </ul>
}
複製代碼

這個時候,我只須要關係個人狀態(數據是什麼),以及 UI 長什麼樣(佈局),再也不須要關係操做細節。

這種方式雖然簡單粗暴,可是很明顯的缺點,就是很慢。

另外還有一個問題就是這樣沒法包含節點的狀態。好比它會失去當前聚焦的元素和光標,以及文本選擇和頁面滾動位置,這些都是頁面的當前狀態。

Diff

爲了解決上面說的問題,對於沒有改變的 DOM 節點,讓它保持原樣不動,僅僅建立並替換變動過的 DOM 節點。這種方式實現了 DOM 節點複用(Reuse)。

至此,只要可以識別出哪些節點改變了,那麼就能夠實現對 DOM 的更新。因而問題就轉化爲如何比對兩個 DOM 的差別

說道對比差別,可能很容易想到版本控制(git)。

DOM 是樹形結構,因此 diff 算法必須是針對樹形結構的。目前已知的完整樹形結構 diff 算法複雜度爲 O(n^3) 。

完整的 Tree diff 實現算法。

可是時間複雜度 O(n^3) 過高了,因此Facebook工程師考慮到組件的特殊狀況,而後將複雜度下降到了 O(n)。

附:詳細的 diff 理解:難以想象的 react diff 。

Virtual DOM

前面說到,React 其實實現了對 DOM 節點的版本控制。

作過 JS 應用優化的人可能都知道,DOM 是複雜的,對它的操做(尤爲是查詢和建立)是很是慢很是耗費資源的。看下面的例子,僅建立一個空白的 div,其實例屬性就達到 231 個。

// Chrome v63
const div = document.createElement('div');
let m = 0;
for (let k in div) {
  m++;
}
console.log(m); // 231
複製代碼

對於 DOM 這麼多屬性,其實大部分屬性對於作 Diff 是沒有任何用處的,因此若是用更輕量級的 JS 對象來代替複雜的 DOM 節點,而後把對 DOM 的 diff 操做轉移到 JS 對象,就能夠避免大量對 DOM 的查詢操做。這個更輕量級的 JS 對象就稱爲 Virtual DOM 。

那麼如今的過程就是這樣:

  1. 維護一個使用 JS 對象表示的 Virtual DOM,與真實 DOM 一一對應
  2. 對先後兩個 Virtual DOM 作 diff ,生成變動(Mutation)
  3. 把變動應用於真實 DOM,生成最新的真實 DOM

能夠看出,由於要把變動應用到真實 DOM 上,因此仍是避免不了要直接操做 DOM ,可是 React 的 diff 算法會把 DOM 改動次數降到最低。

剩下的歷史就不談了,已經引出這篇文章的重點:虛擬 DOM。詳細的歷史可見:React 是怎樣煉成的,文中歷史部份內容不少摘抄與此。

總結

傳統前端的編程方式是命令式的,直接操縱DOM,告訴瀏覽器該怎麼幹。這樣的問題就是,大量的代碼被用於操做 DOM 元素,且代碼可讀性差,可維護性低。

React 的出現,將命令式變成了聲明式,摒棄了直接操做 DOM 的細節,只關注數據的變更,DOM 操做由框架來完成,從而大幅度提高了代碼的可讀性和可維護性。

在初期咱們能夠看到,數據的變更致使整個頁面的刷新,這種效率很低,由於多是局部的數據變化,可是要刷新整個頁面,形成了沒必要要的開銷。

因此就有了 Diff 過程,將數據變更先後的 DOM 結構先進行比較,找出二者的不一樣處,而後再對不一樣之處進行更新渲染。

可是因爲整個 DOM 結構又太大,因此採用了更輕量級的對 DOM 的描述—虛擬 DOM。

不過須要注意的是,虛擬 DOM 和 Diff 算法的出現是爲了解決由命令式編程轉變爲聲明式編程、數據驅動後所帶來的性能問題的。換句話說,直接操做 DOM 的性能並不會低於虛擬 DOM 和 Diff 算法,甚至還會優於。

這麼說的緣由是由於 Diff 算法的比較過程,比較是爲了找出不一樣從而有的放矢的更新頁面。可是比較也是要消耗性能的。而直接操做 DOM 就是有的放矢,咱們知道該更新什麼不應更新什麼,因此不須要有比較的過程。因此直接操做 DOM 效率可能更高。

React 厲害的地方並非說它比 DOM 快,而是說無論你數據怎麼變化,我均可以以最小的代價來進行更新 DOM。 方法就是我在內存裏面用新的數據刷新一個虛擬 DOM 樹,而後新舊 DOM 進行比較,找出差別,再更新到 DOM 樹上。

框架的意義在於爲你掩蓋底層的 DOM 操做,讓你用更聲明式的方式來描述你的目的,從而讓你的代碼更容易維護。沒有任何框架能夠比純手動的優化 DOM 操做更快,由於框架的 DOM 操做層須要應對任何上層 API 可能產生的操做,它的實現必須是普適的。

若是你想了解更多的虛擬 DOM 與性能的關係,請看下面公衆號裏面的兩篇文章和那個知乎話題,會讓你對虛擬 DOM 又更深層次的理解。

另外再提一個點,不少人會把 Diff 、數據更新、提高性能等概念綁定起來,可是你想一想這個問題:React 因爲只觸發更新,而不能知道精確變化的數據,因此須要 diff 來找出差別而後 patch 差別隊列。Vue 採用數據劫持的手段能夠精準拿到變化的數據,爲何還要用虛擬DOM?

虛擬DOM 的做用

要想回答上面那個問題,真的不要僅僅覺得虛擬 DOM 或者 React 是來解決性能問題的,好處可還有不少呢。下面我總結了一些虛擬 DOM 好做用。

  • Virtual DOM 在犧牲(犧牲很關鍵)部分性能的前提下,增長了可維護性,這也是不少框架的通性。
  • 實現了對 DOM 的集中化操做,在數據改變時先對虛擬 DOM 進行修改,再反映到真實的 DOM中,用最小的代價來更新DOM,提升效率(提高效率要想一想是跟哪一個階段比提高了效率,別隻記住了這一條)。
  • 打開了函數式 UI 編程的大門。
  • 能夠渲染到 DOM 之外的端,使得框架跨平臺,好比 ReactNative,React VR 等。
  • 能夠更好的實現 SSR,同構渲染等。這條實際上是跟上面一條差很少的。
  • 組件的高度抽象化。

既然虛擬 DOM 有這麼多做用,那麼上面的問題,Vue 採用虛擬 DOM 的緣由是什麼呢?

Vue 2.0 引入 vdom 的主要緣由是 vdom 把渲染過程抽象化了,從而使得組件的抽象能力也獲得提高,而且能夠適配 DOM 之外的渲染目標。 來自尤大文章:Vue 的理念問題

虛擬 DOM 的缺點

  • 首次渲染大量 DOM 時,因爲多了一層虛擬 DOM 的計算,會比 innerHTML 插入慢。
  • 虛擬 DOM 須要在內存中的維護一份 DOM 的副本(更上面一條其實也差很少,上面一條是從速度上,這條是空間上)。
  • 若是虛擬 DOM 大量更改,這是合適的。可是單一的,頻繁的更新的話,虛擬 DOM 將會花費更多的時間處理計算的工做。因此,若是你有一個DOM 節點相對較少頁面,用虛擬 DOM,它實際上有可能會更慢。但對於大多數單頁面應用,這應該都會更快。

總結

本文在介紹虛擬 DOM 並無像其餘文章同樣去解釋它的實現以及相關的 Diff 算法,關於 Diff 算法能夠看這篇 虛擬 DOM 究竟是什麼?文中介紹了不少庫的 diff 算法,可見其實 React 的 diff 算法並不算太快。

而是經過歷史來得出他的價值體現,從歷史怎麼看大牛們是怎麼一步一步的去解決問題,從歷史中看爲何別人能作出這麼偉大的東西,而咱們不能?

每一個偉大的產品都會有很是多的背景支持,都是一步一步發展而來的。

另外洗清了一個錯誤觀念:不少人認爲虛擬 DOM 最大的優點是 diff 算法,減小 JavaScript 操做真實 DOM 的帶來的性能消耗。

雖然這一個虛擬 DOM 帶來的一個優點,但並非所有。虛擬 DOM 最大的優點在於抽象了本來的渲染過程,實現了跨平臺的能力,而不只僅侷限於瀏覽器的 DOM,能夠是安卓和 IOS 的原生組件,能夠是近期很火熱的小程序,也能夠是各類 GUI。

最後但願你們多思考,跟隨者浪潮站在浪潮之巔。

參考連接

後記

我是桃翁,一個愛思考的前端er,想了解關於更多的前端相關的,請關注個人公號:「前端桃園」,若是想加入交流羣關注公衆號後回覆「微信」拉你進羣

相關文章
相關標籤/搜索