為什么我們需要UI框架?
響應式編程提出兩個最重要的觀點是:系統應該是事件驅動并且響應狀態的變化。
DOM的UI組件有自己的內部狀態,更新瀏覽器頁面并不是在發生變化后簡單的重新生成DOM。如果Gmail這樣做的話,出現一條新消息或者刪掉你寫的郵件,就會導致整個瀏覽器窗口刷新。
正是因為DOM的無狀態性,我們才需要類似key/value observation(Ember中使用了它),或者臟值檢查(Angular)。UI框架監聽數據模型的變化,并在DOM中更新對應的部分?;蛘弑O聽DOM的變化,更新對應的數據模型。這就是所謂的雙向數據綁定。它通常可以處理非常復雜的UI邏輯。
什么讓React與眾不同?
令React以及它的Virtual DOM如此與眾不同的是:它比其他實現JavaScript響應數據的方法都更簡單。你只需寫JavaScript去更新React組件,React會幫你更新DOM。數據綁定并沒有和應用纏在一起。
React使用單向數據綁定來簡化這一過程。每次當你在一個React組件的input文本框中輸入時,它并不會直接改變這個組件的狀態,而是更新數據模型。這會使得UI被更新,你輸入的文本就會出現在文本框里。
DOM很慢?
所有關于Virtual DOM的文章或演講都在說JavaScript引擎很快,讀寫瀏覽器的DOM很慢。這種說法完全不對。DOM是很快的。添加刪除DOM節點并不比在JavaScript對象上設置屬性慢很多,只是簡單的運算罷了。
然而,真正慢的是當DOM改變時瀏覽器的layout。DOM的每次改變,瀏覽器都需要重新計算CSS,重排重繪整個頁面。這是很耗時的。
那些寫瀏覽器的人一直在努力縮短重繪的時間,其中最大的工作是最小化、分批處理DOM改變。
Virtual DOM如何工作?
就像真實的DOM,Virtual DOM也是一顆節點樹。節點樹上元素是對象,attribute和內容是對象的屬性。React的render方法從組件上創建一顆節點樹,在action觸發數據模型發生mutation后響應式的更新節點樹。
每次在數據改變之后,就會在UI更新之前創建一個新的Virtual DOM。在React中,更新瀏覽器的DOM分三個步驟:
1. 只要數據發生改變,就會重新生成一個完整的Virtual DOM。
2. 重新計算比較出新的和之前的Virtual DOM的差異。
3. 更新真實DOM中真正發生改變的部分,就像是給DOM打了個補丁。
Virtual DOM慢嗎?
我們可以想到每次改變都重新渲染整個Virtual DOM是很浪費的,卻沒有注意到任意時刻React都在內存中保存了兩個Virtual DOM。但是,其實渲染Virtual DOM總是會比渲染真實DOM快,這跟你使用的瀏覽器無關。
問題是用戶并不能看到Virtual DOM。就好比你在其他國家有10000個墨西哥煎玉米卷,你遲早需要把玉米卷運回來,那會很貴并且緩慢。
玉米卷問題的關鍵在于:一次把所有玉米卷運回來更快,還是計算出你需要的數量和你實際擁有的數量,然后僅僅運輸兩者最小值更快?當你只想要4個玉米卷的時候,當然只運輸4個更劃算。
下一個問題是:你如何預定玉米卷?你可以說,“給我運送4個玉米卷”,或者說,“這是我的玉米卷狀態清單,你計算出實際結果吧”。第二個方法就是Virtual DOM工作的方式。你寫代碼來讓UI知道你想讓他如何展示,Virtual DOM計算出當前UI和它只需要更新的部分的差異。
React像變戲法般的將attribute加到元素上,然后在DOM Diff后決定需要更新的部分,并在文檔上單獨的修改這些元素。Virtual DOM加入了額外的步驟,但是它創造了一種優雅的方式去對頁面做最小的更新。
我們來看一些數字
我不打算做標準的測試。其他人做的各種各樣的測試已經證明了React的Virtual DOM更快。Virtual DOM在開發者對瀏覽器的性能優化之上加了一層腳本。這個額外的一層抽象使React相比其他更新DOM的方法,需要更多的CPU密集計算。
舉個使用原生JS操作DOM的“Hello,world!”的例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello JavaScript!</title>
</head>
<body>
<div id="example"></div>
<script>
document.getElementById("example").innerHTML = "<h1>Hello, world!</h1>";
</script>
</body>
</html>
你在React中也會做同樣的事。我們需要通過React、React DOM和babel,將看起來像是XML的代碼在render()方法中轉換成普通的JavaScript對象。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="build/react.js"></script>
<script src="build/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
</script>
</body>
</html>
原生操作DOM總是會更快。我們來看一下證明過程。
這是加載和渲染直接DOM操作的“Hello,World”頁面的timeline(chrome)
這是加載和渲染React“Hello,World”頁面的timeline(chrome)
React花了大量時間在scripting上,React比直接操作DOM慢的多。但是,它和jQuery比怎么樣?
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello jQuery!</title>
<script type="text/javascript" src="scripts/vendor/jquery-1.12.3.min.js"></script>
</head>
<body>
<div id="example"></div>
<script>
$(document).ready(function(){
$("#example").html("<h1>Hello, world!</h1>"); }
);
</script>
</body>
</html>
jQuery的總時間比原生JS慢了50ms,但都比React快3倍。顯然,原生JS和jQuery要快得多。一般來說,使用框架都比不使用框架慢。實際操作DOM前在內存中創建一個表示DOM的結構比直接操作DOM要慢。下面,我們討論一下究竟該如何讓Virtual DOM更快。
如何使用Virtual DOM
"Hello,World"例子對React不公正,因為他們僅僅包含了一個頁面的初始渲染。React長于管理頁面的更新。
數據模型的每一次改變都會觸發Virtual DOM的重新生成,這就是React和其他框架的不同之處。其他框架會檢測文檔的變化,只更新必要的部分。Virtual DOM通常占用更少的內存,因為它不需要在內存中常駐觀察者。
但是,每次改動發生后都比較兩個完整的Virtual DOM是低效的。復雜的UI對于CPU的要求也很高。
鑒于這個原因,React開發者要主動決定需要渲染的內容。如果你知道某個行為不會影響對應的組件,你應當告知React不要去分析組件的變動--這可以節省大量的資源,顯著地提升應用的性能。
事實上,可能沒有辦法真正說明使用Virtual DOM比直接操作DOM快,因為要做這種比較需要考慮各種各樣的因素。但是主要還是取決于你如何更好的優化應用。
工具只是工具,關鍵看你怎么用它。React和Virtual DOM帶給我們的是:一種更新頁面的簡單方法。這種簡化能讓我們擺脫繁雜的工作,使優化UI變得簡單。這就是React帶來的真正好處--性能和生產力。
原文作者:Chris Minnick
翻譯:熊賢仁