別再說虛擬 DOM 快了,要被打臉的

如果你覺得虛擬 DOM 很快,那麼這篇文章可能就是你所缺少的

我經常聽到有人在羣裏,或者在社區裏說的一個很嚴重的錯誤,那就是說 React 的 Virtual Dom 是以快出名的,比原生 DOM 快多了,啥啥啥的,每次都一兩句話說不清楚,所以下次有誰再說 React 是以快出名的,你就把這篇文章丟給他,下面進入正題。

在過去的幾年裏,你一直在跟蹤 JavaScript 社區的發展,你至少聽說過 Virtual DOM(React,Vue.js 2,Riot.js,Angular 2等等)。他們承諾(或者更確切地說,他們的宣傳)更快的渲染界面,特別是更新,減少麻煩。你很快的上手了使用虛擬DOM的應用程序,這很好。幾個月後,您的應用程序現在變得越來越複雜,你可能從用戶交互到屏幕更新只需要一兩秒鐘的更新。你可能會想,這東西很神奇,應該會比 jQuery 快,但是實際上不是這個樣子的。

雖然我同意虛擬 DOM 爲我們提供了很多便利,但我將解釋爲什麼我認爲根據定義,更快的渲染和更快的更新是不正確的。要付出代價,其利益並不是大多數人想象或至少希望的。

要閱讀本文,您需要熟悉DOM。理想情況下,您至少可以使用 DOM API。如果你只使用 DOM API 構建東西,你可能不需要這篇文章,但我仍然希望你閱讀它並在評論中留下一點評語。

渲染和更新

讓我們來看看手動執行 DOM 節點的創建和更新的鳥瞰圖。這對於理解虛擬DOM如何工作以及它解決了哪些問題非常重要。

在談論 JavaScript Web 應用程序時,用戶界面的更改通過 DOM 操作發生。這個過程分爲兩個階段:

  1. JS 部分:定義 JavaScript 世界中的變化
  2. DOM 部分:使用 DOM API 函數和屬性執行更改

性能是根據整個過程的速度來衡量的,但瞭解每部分的速度也很重要,以便了解要優化的內容。

有兩種方法可以創建和更新DOM樹的各個部分。

①字符串方式創建

使用字符串既快速又簡單,但在更新方面並不是非常精細。對於字符串,JS部分是它如此之快的原因。您可以在幾毫秒內創建一段代表5000個節點的HTML。這是一個例子:

const userList = document.getElementById("user-list");

// JS 部分
const html = users.map(function (user) {
  return `
    <div id="${user.id}" class=”user”>
      <h2 class="header">${user.firstName} ${user.lastName}</h2>
      <p class="email"><a href=”mailto:${user.email}”>EMAIL</a></p>
      <p class="avg-grade">Average grade: ${user.avgGrade}</p>
      <p class="enrolled">Enrolled: ${user.enrolled}</p>
    </div>
  `
}).join("");

// DOM 部分
userList.innerHTML = html;

我提到使用這種方法時存在侷限性。請考慮以下示例:

const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.innerHTML = `<input class="search" type="text" value="bar">`;

雖然看起來上面的內容很簡單,但它實際上並不起作用。當我們運行上面的代碼時,原始<input>元素被替換而不是更新,例如,如果用戶有焦點的字段,他們將失去焦點。

②使用 DOM 對象

創建和更新 DOM 樹的另一種方法是使用 DOM 對象。就你必須編寫的代碼而言,這種方法非常冗長,而且總體來說它也慢得多。

讓我們使用這個方法重寫用戶列表示例:

const userList = document.getElementById(“user-list”);
// JS part 
const = document.createDocumentFragment(); 
users.forEach(function(user){ 
  const div = document.createElement(“div”); 
  div.id = user.id; 
  div.className =“user”; 
  const header = document.createElement(“h2”); 
  h2 .className =“header”; 
  h2.appendChild(
    document.createTextNode(`$ {user.firstName} $ {user.lastName}`)
  ); 
  // .... 
  frag.appendChild(div); 
});
// DOM部分
userList.innerHTML =“”; 
userList.appendChild(FRAG);

這看起來不太好,但它仍然是創建DOM節點的有效方法。它還有一個優點,即我們能夠將它與第三方庫(如D3)混合使用,以執行 HTML 字符串不易處理的事情。在真正的優勢,雖然是執行粒度更新現有的樹時:

const search = document.getElementById(“search”); 
search.innerHTML =`<input class ="search" type ="text"value ="foo">`; 
//將值更改爲“bar”?
search.querySelector("input")。value ="bar";

這次我們結合快速方便的字符串 HTML 方法來創建初始 UI,然後我們使用 DOM 操作方法來更新 value 屬性。不像我們第一次這樣做,<input>現在沒有被替換,所以它不會像第一個例子那樣引起 UX 故障。

進入虛擬DOM

讓我們回到輸入示例的第一個版本:

const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.innerHTML = `<input class="search" type="text" value="bar">`;

如果我們參數化值部分,它將如下所示:

const search = document.getElementById("search");
const renderInput = function (value) {
  search.innerHTML = `<input class="search" type="text" value="${value}">`;
};
renderInput("foo");
// Change value to "bar"?
renderInput("bar");

好吧,新 renderInput() 功能肯定看起來很酷,但我們已經知道這不是好方法。

如果我們有一些騷操作可以讓我們繼續使用類似的東西,但同時弄清楚我們想要做什麼並做正確的事情呢?第二次 renderInput() 被調用,我們只更新 value 屬性,所以只更新該屬性而不是重新渲染整個屬性<input>

我們說過創建和更新 DOM 樹的整個過程分爲兩個階段。使用虛擬 DOM,DOM 階段應該儘可能高效,代價是在 JS 階段完成的額外工作。這項額外的工作會做 diff(不要以爲 js 計算就不花費代價),因此它的另一個名稱將是開銷。根據定義,虛擬 DOM 比精心設計的手動更新慢,但它爲我們提供了一個更方便的 API 來創建 UI。

虛擬DOM比精心設計的手動更新慢。

爲什麼有些開發人員認爲Virtual DOM更快

在虛擬DOM(尤其是React)的早期,傳播了一個神話,即虛擬 DOM 使 DOM 快速更新。正如我們在前面的章節中看到的那樣,這在技術上是不可行的。DOM 更新就是它們的原因,並且沒有任何魔法可以使它更快:它必須在瀏覽器的本機代碼中進行優化。

可以看到 React 主頁裏面沒有提到性能,而是開發人員的便利性。

React 的基本思維模式是每次有變動就整個重新渲染整個應用。如果沒有 Virtual DOM,簡單來想就是直接重置 innerHTML。很多人都沒有意識到,在一個大型列表所有數據都變了的情況下,重置 innerHTML 其實是一個還算合理的操作… 真正的問題是在 “全部重新渲染” 的思維模式下,即使只有一行數據變了,它也需要重置整個 innerHTML,這時候顯然就有大量的浪費。

您仍然可以看到比較各種虛擬 DOM 實現的基準測試,並且一些措辭會誤導新開發人員認爲虛擬 DOM 是當今事實上的標準,並且不值得對其他技術進行基準測試。然而,有一些基準可以將它與其他技術進行比較,例如 Aerotwist 的 React +性能文章,它描繪了虛擬 DOM 在宏觀方案中所處位置的更真實的畫面。

我們得到了什麼?這值得麼?

虛擬DOM最終是一種執行 DOM 更新的循環方式。但是,它打開了通向有趣架構的大門,例如將視圖視爲狀態函數,或者編寫和組合視圖組件。虛擬 DOM 帶來了很多好東西,儘管瘋狂的性能水平不是其中之一。您可以將其視爲 Python 或 PHP 中的編碼與 C 中的編碼之間的差異。我們以性能爲代價獲得更多的開發人員工具。換句話說,這是一種權衡。

另一方面,開發人員的時間丟失也是一些實現方面的事情。虛擬 DOM 試圖弄清楚它需要執行哪些更改的部分是由人類實現的,因此它並不總是萬無一失。有時你必須介入。在某些情況下,無法進行干預。對於絕對性能至關重要的事情,它甚至可能不是一種選擇。

衡量您的表現並根據硬數據來決定。

最重要的是,虛擬DOM只是您可以使用的工具之一。衡量您的表現並根據硬數據來決定。數據綁定仍然非常可行,我們已經看到您也可以手動完成所有操作。它絕對不是萬能的,因此沒有必要與虛擬DOM結合。

結論

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

這就是所謂的 diff 算法,雖然說 diff 算法號稱算法複雜度 O(n) 可以得到最小操作結果,但實際上 DOM 樹很大的時候,遍歷兩棵樹進行各種對比還是有性能損耗的,特別是我在頂層 setState 一個簡單的數據,你就要整棵樹 walk 一遍,而真實中我可以一句 jQuery 就搞定,所以就有了 shouldComponentUpdate 這種東西。

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

針對每一個點,都可以寫出比任何框架更快的手動優化,但是那有什麼意義呢?在構建一個實際應用的時候,你難道爲每一個地方都去做手動優化嗎?出於可維護性的考慮,這顯然不可能。框架給你的保證是,你在不需要手動優化的情況下,我依然可以給你提供過得去的性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章