關於react diff 演算法(譯文)

國傑老溼發表於2017-11-03

React是由facebook開發,用於構建使用者介面的js類庫,以提升效能為設計理念。在本文中,我將為大家介紹在React中的diff演算法,以及它的渲染機制,以便於你能夠更好的優化你的程式。

Diff演算法

在深入瞭解實現細節之前,瞭解React如何工作是很重要的。

var MyComponent = React.createClass({ 
    render: function() { 
        if (this.props.first) { 
            return <div className="first"><span>A Span</span></div>; 
        } else { 
            return <div className="second"><p>A Paragraph</p></div>; 
        } 
    } 
});複製程式碼

在任何時候,你可以將UI描述你想要的樣子。你必須明白它渲染的結果不是一個真實的DOM。渲染結果只是輕量級的JavaScript物件。我們稱之為虛擬DOM。
React將使用此表示法來嘗試找到從上一個渲染到下一個渲染的最小步數,比如,如果我們要用 <MyComponent first={false} />去替代<MyComponent first={true} />,插入真實DOM, 然後移除它,下面是DOM指令的結果:

從無到有
建立DOM節點:<div className="first"><span>A Span</span></div>

從一到二
className="second"去替換屬性 className="first"
<p>A Paragraph</p> 去替換節點<span>A Span</span>

從二到無
移除節點:<div className="second"><p>A Paragraph</p></div>

逐級比較

尋找兩個任意樹之間最小的修改數需要執行n3次,你可以想象,這對於我們來說是難以接受的。而React用了一個簡單而且目前來說都非常強大的計算方法去找到他們的變化,而執行的次數僅僅為n次。

React通過逐級的去比較兩顆節點樹的差異,這大大降低了複雜性,而且精準度上損失也不大,因為在Web應用程式中將元件樹不同級的移動比較是非常罕見的。 元件通常只能在子元件橫向移動。

列表List

假設我們有一個元件,它在一個迭代中渲染了5個元件,而下一次渲染的時候在元件列表的中間插入一個新的元件。 只是通過這個資訊真的很難知道如何在兩個元件列表之間進行對映。

預設情況下,React將先前列表的第一個元件與下一個列表的第一個元件相關聯,等等。您可以提供一個Key屬性,以幫助React去找到他們的對映關係。 在實際中,這通常很容易把剛剛插入的元件從他們當中找出來。

元件

一個React應用程式通常由許多使用者定義的元件組成,最終會變成一個主要由div組成的樹。 通過diff演算法考慮了這些附加資訊,因為React只匹配具有相同類的元件。

例如,如果<Header><ExampleBlock>替換,則React將刪除<Header>並建立一個<ExampleBlock>。 我們不需要花費寶貴的時間嘗試匹配不太可能有相似之處的兩個元件。

事件委託

將事件監聽器附加到DOM節點是痛苦的緩慢而且消耗記憶體的事情。但是,React實現了一種稱為“事件委託”的流行技術。 React更進一步,並重新實現了符合W3C標準的事件系統。這意味著Internet Explorer 8事件處理相容問題將成為過去的事情,所有的事件名稱在瀏覽器之間是一致的。

讓我解釋一下它的實現。單個事件監聽器附加到文件的根節點上。當事件被觸發時,瀏覽器告訴我們觸發事件的DOM節點。為了通過DOM節點傳播事件,React並沒有在虛擬DOM層次結構上進行迭代。

相反,每個React元件都有唯一id用來表示他的層級。 我們可以使用簡單的字串操作來獲取所有父節點的id。 通過將事件偵聽器儲存在雜湊對映中,我們發現它比將它們附加到虛擬DOM更好。

以下是通過虛擬DOM分派事件時發生的一個示例。

// dispatchEvent('click', 'a.b.c', event) 
clickCaptureListeners['a'](event); 
clickCaptureListeners['a.b'](event); 
clickCaptureListeners['a.b.c'](event); 
clickBubbleListeners['a.b.c'](event);
clickBubbleListeners['a.b'](event); 
clickBubbleListeners['a'](event);複製程式碼

瀏覽器為每個事件和監聽器建立一個新的事件物件。 這個物件有一個非常好的的屬性,你可以保留事件物件的引用甚至修改它。 但是,這意味著要做大量的記憶體分配。React在一開始會為這些物件分配記憶體池。 每當需要事件物件時,它將從記憶體池中被重新使用,這大大減少了記憶體垃圾回收。

渲染

批量處理:

每當您在元件上呼叫setState時,React會將其標記為髒。 在事件迴圈結束時,React會檢視所有髒元件並重新渲染它們。如果是批處理也就意味著在事件迴圈期間,正在更新渲染DOM的時間正好是一次。 這個屬性是構建一個應用程式的效能好壞的關鍵,但是寫常用的JavaScript非常難以獲得。 在React應用程式中,預設情況下可以獲得。

子樹渲染:

呼叫setState時,元件將重建其子項的虛擬DOM。如果您在根元素上呼叫setState,則會重新渲染整個React應用程式。所有的元件,即使它們沒有改變,也會使用它們的渲染方法。這可能聽起來很可怕,效率低下,但在實踐中,這樣可以正常工作,因為我們沒有碰到實際的DOM。
首先,我們討論一下顯示使用者介面。由於螢幕空間有限,您通常一次按順序的顯示數百到數千個元素。 JavaScript已經可以足夠快的速度處理業務邏輯和整個介面管理。
另一個重要的一點,當編寫React程式碼時,每次發生變化通常不會都在根節點上呼叫setState。你可以在接收到事件變化的元件或父元件上呼叫它。我們很少走到頂端,通常使用者的互動都是發生在對應的元件上變化。

選擇子樹渲染:

最後,您可以防止一些子樹重新渲染。 如果在元件上使用以下方法:
boolean shouldComponentUpdate(object nextProps, object nextState)
基於元件的上一個和下一個props/state,您可以告訴React該元件沒有更改,並且不需要重新渲染。 正確使用該方法可以大大提高效能。為了能夠使用它,您需要比較JavaScript物件。這裡面還是有很多問題的,比如js物件的比較是深點還是淺點,如果深度比較,我是用不可變資料結構還是做深拷貝。而且你還要記住,這個函式將一直被呼叫,所以你想要確保計算時間要比渲染元件所用的時間要少,即使渲染是多餘的。

 到底什麼情況下使用shouldComponentUpdate?
    按照React團隊的說法,shouldComponentUpdate是保證效能的緊急出口  
    http://jamesknelson.com/should-i-use-shouldcomponentupdate
    http://www.infoq.com/cn/news/2016/07/react-shouldComponentUpdate複製程式碼

結論

讓React變得如此快的技術已經不是什麼新鮮的事情,而且我們很久之前就知道,操作DOM是昂貴的,所以你應該對DOM進行批量的讀寫、使用事件委託,這些都能使你的程式變得更快。
大家還一直在討論React, 因為事實上,使用常規的Javascript程式碼很難去實現這些優化的方法,而React預設就能實現,這也是為什麼React為什麼能脫穎而出的原因。
React的效能成本模型也很容易理解:每次setState都會重新呈現整個子樹。 如果要提高效能,請儘可能少呼叫setState,並使用shouldComponentUpdate來防止重新所有子樹。

原文地址:calendar.perfplanet.com/2013/diff/

相關文章