2017-08-29讀書筆記(React’s diff algorithm)

這兩天其實(shí)是有一些想看看react的,總覺得同部門的人用react寫代碼,而我還在用著上一代的backbone,對(duì)react還不算熟(之前用過個(gè)把月,參加了一個(gè)項(xiàng)目的初期研發(fā))。今天準(zhǔn)備閱讀一篇之前熟悉react的時(shí)候看到的文章。

React的差異算法 React’s diff algorithm by Christopher Chedeau(Facebook Software Engineer )

在學(xué)習(xí)react的時(shí)候,你必須明白,這不是真的dom node,而是虛擬dom節(jié)點(diǎn)。

當(dāng)使用react來從上一個(gè)節(jié)點(diǎn)渲染下一個(gè)節(jié)點(diǎn)的時(shí)候,使用了代理 representation 來找到需要的最小步數(shù)。

文章舉了這么一個(gè)例子:

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>;
        }
    }
});

這時(shí)將<MyComponent first={true} /> 改成 <MyComponent first={false} />,最后再移除會(huì)發(fā)生什么。

發(fā)生的步驟分為三步

None to first
Create node: <div className="first"><span>A Span</span></div>

First to second
Replace attribute: className="first" by className="second"
Replace node: <span>A Span</span> by <p>A Paragraph</p>

Second to none
Remove node: <div className="second"><p>A Paragraph</p></div>

在任意兩個(gè)樹中找到修改的最小改變是一個(gè)O(n^3)時(shí)間復(fù)雜度的算法。這肯定不適用于生產(chǎn)環(huán)境。React使用了簡(jiǎn)單但是非常強(qiáng)大的搜索算法,能夠在接近O(n)的時(shí)間復(fù)雜度內(nèi)找到差異。

React使用的方法是一層一層的訪問樹結(jié)構(gòu)。這大大的降低了復(fù)雜度,并且不會(huì)有大的損失。原因是在web應(yīng)用中,幾乎沒有將一個(gè)組件移動(dòng)到樹中不同的層級(jí)上的情況,通常組件都是在children之間移動(dòng)。

舉個(gè)例子,如果有一個(gè)組件,在一次遍歷中渲染了五個(gè)組件,并且在下一次渲染時(shí)在其中插入了一個(gè)新組件。只通過這些信息來判斷并知道這兩個(gè)列表之間的映射關(guān)系是非常困難的。

在React的默認(rèn)情況下,會(huì)將前一個(gè)列表的第一個(gè)組件和下一個(gè)列表的第一個(gè)組件進(jìn)行關(guān)聯(lián),以此類推。你可以提供一個(gè)key屬性來幫助React發(fā)現(xiàn)映射。然后就可以很容易的在children中發(fā)現(xiàn)那個(gè)唯一的key.

通常情況下,一個(gè)React應(yīng)用是由許多自定義的組件組成的,最終組合成一個(gè)由div組成的樹。React會(huì)考慮這種額外的信息,并且只匹配有相同的class的組件。

例如,如果一個(gè)<Header>被一個(gè)<ExampleBock>替換了,React會(huì)移除<Header>,并且創(chuàng)建一個(gè)新的<ExampleBock>。我們不會(huì)花費(fèi)時(shí)間來試圖尋找這兩個(gè)組件不相同的部分。

也就是說,如果兩個(gè)自定義的組件class不同,就直接移除舊的并創(chuàng)建新的組件。如果class相同,才會(huì)去判斷與之前的兩個(gè)組件有什么不同。(按理來說,如果組件就不相同,應(yīng)該也是不會(huì)去判斷的,會(huì)直接移除舊的、創(chuàng)建新的)

使用原生的js在DOM節(jié)點(diǎn)中添加事件監(jiān)控是非常慢的,而且還非常耗費(fèi)內(nèi)存。然而,在React中實(shí)現(xiàn)了一種名為“事件委派 event delegation”的流行的技術(shù)。React甚至走的更遠(yuǎn),重新實(shí)現(xiàn)了一下W3C的事件系統(tǒng)。這意味著IE8的事件處理的(event-handling)bug(這里雖然寫的是bug,但是我覺得原作者可能是想表達(dá)對(duì)原生js的事件的不滿)已經(jīng)是過去式了,并且所有的事件名稱在不同的瀏覽器中是一致的。

讓我來解釋一下這是如何實(shí)現(xiàn)的。一個(gè)單事件監(jiān)聽被添加在document的根部。當(dāng)一個(gè)事件被觸發(fā)時(shí),瀏覽器會(huì)直接給出目標(biāo)DOM節(jié)點(diǎn)。這種方法能夠成功主要是因?yàn)槊總€(gè)React組件有一個(gè)用來編碼層級(jí)結(jié)構(gòu)的唯一id。React使用一組的字符串來得到所有父節(jié)點(diǎn)的id。而且發(fā)現(xiàn)將所有的事件監(jiān)聽存儲(chǔ)在hash map中,比將事件監(jiān)聽添加在虛擬DOM上的性能要好很多。

下面這個(gè)例子展示了當(dāng)一個(gè)事件通過虛擬DOM傳播時(shí)發(fā)生了什么:

// 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);

瀏覽器為每個(gè)活動(dòng)事件 event 和事件監(jiān)聽 listener 創(chuàng)建了一個(gè)新的對(duì)象。這些對(duì)象有很好的屬性,可以保存它的引用,甚至可以去修改它。但是,這意味著這樣會(huì)帶來很高的內(nèi)存花費(fèi)。React啟動(dòng)時(shí)為這些對(duì)象分配了一個(gè)內(nèi)存池。當(dāng)需要一個(gè)事件對(duì)象時(shí),可以在內(nèi)存池中取到這個(gè)對(duì)象。這顯著的減少了垃圾回收機(jī)制garbage collection所需要的內(nèi)存 。(相關(guān)的垃圾回收機(jī)制可以參考這篇Memory Management

當(dāng)你調(diào)用了一個(gè)組件上的setState時(shí),React會(huì)將其標(biāo)記為臟的 dirty。當(dāng)事件循環(huán)結(jié)束時(shí),React查詢所有臟 dirty 的組件,并且重新渲染他們。

該合并意味著在事件循環(huán)中,只有一個(gè)確切的DOM被更新的時(shí)間點(diǎn)。這是構(gòu)建一個(gè)高性能的應(yīng)用的關(guān)鍵,并且在其他JavaScript代碼中很難實(shí)現(xiàn)。在React應(yīng)用中,你自然而然的使用它。

當(dāng)setState被調(diào)用,組件會(huì)為其孩子重新構(gòu)建虛擬DOM。如果你在根元素上調(diào)用setState,整個(gè)React應(yīng)用會(huì)被重新渲染。所有的組件,甚至你從來沒改變的組件,都會(huì)調(diào)用其render方法。這聽起來很恐怖、效率很低,但是實(shí)際中,還是很可行的,因?yàn)檫@操作沒有觸摸實(shí)際的DOM。

這么做的原因有兩點(diǎn):

1、從展示用戶界面的角度來說。因?yàn)槠聊坏目臻g是有限的,很多情況下你需要一次展示成百上千個(gè)元素。JavaScript對(duì)于整個(gè)業(yè)務(wù)邏輯的接口管理來說已經(jīng)足夠的快了。

2、正常來說,每次事情發(fā)生變化時(shí),通常不會(huì)在根節(jié)點(diǎn)上調(diào)用setState。你在接收到事件變化的節(jié)點(diǎn)或者上面一些節(jié)點(diǎn)上調(diào)用它。很少情況下你會(huì)到達(dá)根節(jié)點(diǎn)。這意味著更新只在用戶交互的局部發(fā)生。

3、如果你使用了組件中的shouldComponentUpdate方法,這可以阻止一些子樹 sub-tree 的重新渲染。

boolean shouldComponentUpdate(object nextProps, object nextState)

基于組件的前一個(gè)和下一個(gè)props/state,你可以告訴React這個(gè)組件沒有改變并且不需要重新渲染。當(dāng)正確實(shí)現(xiàn)時(shí),會(huì)給你的應(yīng)用帶來巨大的性能的提升。

為了能夠使用它,你不得不比較JavaScript對(duì)象。有許多問題會(huì)浮現(xiàn)出來,例如應(yīng)該是淺比較還是深比較;如果是深比較,我們應(yīng)該使用不可改變的數(shù)據(jù)結(jié)構(gòu)還是使用深度拷貝。

并且你需要記住,即使重新渲染不是必須的,但是這個(gè)shouldComponentUpdate函數(shù)每次都會(huì)render的時(shí)候被調(diào)用,所以應(yīng)該確保它花費(fèi)的計(jì)算時(shí)間比React的搜索和重新渲染該組件的時(shí)間少,不然這個(gè)方法就毫無意義。

總結(jié)

讓React實(shí)現(xiàn)更快的方法不是使用新的技術(shù)。很長(zhǎng)時(shí)間以來我們已經(jīng)知道了觸摸DOM的花費(fèi)是昂貴的,你應(yīng)該打包你的讀寫操作,讓事件委派的操作可以更快…

人們?nèi)匀辉谧h論這些問題,因?yàn)樵趯?shí)際中,使用原聲js代碼很難將這些問題簡(jiǎn)單化。而React能夠脫穎而出是因?yàn)椋@些所有的優(yōu)化已經(jīng)被寫在了框架里面。

React性能消耗的模型理解起來很簡(jiǎn)單:每次setState都會(huì)重新渲染整個(gè)子樹。

如果你想要提升你的React應(yīng)用的性能,你可以降低調(diào)用setState的頻率,并且合理使用shouldComponentUpdate來阻止一些大子樹的重新渲染。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南,這只是我在學(xué)習(xí)過程中的一些閱讀筆記,個(gè)人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,954評(píng)論 1 18
  • It's a common pattern in React to wrap a component in an ...
    jplyue閱讀 3,411評(píng)論 0 2
  • 最近看了一本關(guān)于學(xué)習(xí)方法論的書,強(qiáng)調(diào)了記筆記和堅(jiān)持的重要性。這幾天也剛好在學(xué)習(xí)React,所以我打算每天堅(jiān)持一篇R...
    gaoer1938閱讀 1,819評(píng)論 0 5
  • 原文地址:Learning React.js is easier than you think原文作者:Samer...
    sunshine小小倩閱讀 4,347評(píng)論 3 41
  • 本筆記基于React官方文檔,當(dāng)前React版本號(hào)為15.4.0。 1. 安裝 1.1 嘗試 開始之前可以先去co...
    Awey閱讀 7,936評(píng)論 14 128

友情鏈接更多精彩內(nèi)容