嚴新巧 白俊峰
摘要:React組件化思想為開發(fā)者前端開發(fā)提供了新的思路,由于React的Visual Dom讓開發(fā)者不用擔心刷新頁面帶來渲染方面性能問題,而Visual Dom的核心算法就是React Diff算法,它確保只對界面上需要刷新的部分進行刷新,讓開發(fā)者只需關注于業(yè)務本身。該文基于對React Diff算法的研究來分析React組件的生命周期,理解Virtual Dom的核心算法,方便以后React程序優(yōu)化。
關鍵詞:React組件;虛擬Dom算法;渲染
中圖分類號:TP312 文獻標識碼:A 文章編號:1009-3044(2017)18-0076-03
1背景介紹
對于動態(tài)網(wǎng)站,需要經(jīng)常對網(wǎng)頁的元素進行操作,一般通過Dom樹結點的操作來更新界面,對于一些比較復雜的界面,需頻繁操作Dom結點會導致性能問題,并且對開發(fā)者而言,增加了開發(fā)的難度?;诖?,React專門實現(xiàn)了一套Visual Dom機制。基于React開發(fā)程序的操作都是通過內(nèi)置的Visual Dom來實現(xiàn),當有數(shù)據(jù)狀態(tài)發(fā)生改變時,會將前后兩個Dom樹進行對比,對比后的結果只對有區(qū)別的部分進行更新?;谝恍┎呗?,React能更好地處理Dom樹發(fā)生改變的部分。
Visual Dom的核心算法就是React Diff,它通過算法的優(yōu)化,讓Dom樹的更新不再是難題,這也讓開發(fā)者不用擔心由于Dom結點更新而導致的性能問題,通過計算出每次更新的Dom結點,只需要對局部進行操作就可以渲染出界面,這樣保證了更新的效率。
2傳統(tǒng)的Diff算法與Virtual Dom算法介紹
傳統(tǒng)web應用,一般是直接更新DOM操作,但是DOM更新通常比較昂貴。React為盡可能減少對DOM的操作,提供一種不同且強大的方式來更新DOM,從而代替直接DOM操作。Vir-tual DOM,一個輕量級虛擬的DOM,是React抽象出來的一個對象,描述DOM的樣子,以及如何呈現(xiàn)。通過Virtual DOM更新真實DOM,由VirtualDOM管理真實DOM的更新。
Virtual DOM操作更新更快,因為React的diff算法如圖1,更新Virtual DOM并不一定立即影響真實DOM,React會等到事件循環(huán)結束,然后利用diff算法,通過當前新DOM表述與之前的作比較,計算出最少步驟更新真實DOM。
對于一次刷新操作,傳統(tǒng)的計算方法是通過循環(huán)遞歸進行一一對比,從而分析出需要改變的Dom樹,其中的算法復雜度是O(n3),這就意味著如果對1000個節(jié)點進行比較的話,傳統(tǒng)算法就需要進行十億次操作。
傳統(tǒng)diff算法的復雜度為O(n3),顯然這無法滿足性能要求。React通過制定策略,將O(n3)復雜度轉換成0(n)復雜度。
由于傳統(tǒng)算法的復雜度無法滿足性能要求,而React通過算法的優(yōu)化,將指數(shù)的復雜度變成線性的復雜度,因為Dom樹在應用中具有以下特點,才讓Visual Dom得以實現(xiàn):
1)對于一些節(jié)點操作來說,Dom結點之間的跨層操作是非常少的
2)對于同一個類生成相同Dom的樹形結構,不同的類生成不同的Dom樹結構。
3)對于同一層次的子結點,他們的ID是不一樣的
3Diff策略分析
基于以上三個Dom的特點,React分別用三種不同的算法tree diff、component diff以及element diff進行Visual Dom操作。
3.1treediff策略
對于特點1,React采用了tree diff算法,因為更新都是基于同一層次的結點進行比較,即每一次只對同一層次的節(jié)點進行比較。這樣的操作對于Dom而言操作較少,React通過updat-eDepth算法來對Virtual Dom進行比較,即它會對同一個父結點進行子結點比較,當發(fā)現(xiàn)節(jié)點不存在了,則把該節(jié)點以及子結點都刪除,通過這個算法,只需要一次遍歷操作就可以完成對整個Dom樹的比較。
下面通過一個例子來說明這種情況,如圖2,需要將A結點以及A下面的結點移到到D結點下,由于React只對同一層次的結點考慮位置變換,所以對于這種情況,就會存在著創(chuàng)建與刪除的操作。當根結點發(fā)現(xiàn)A結點消失后,就會直接刪除A,而當發(fā)現(xiàn)D多了一個結點后,就會在D下面創(chuàng)建結點與子結點,所以它們的執(zhí)行順序是:create A→create B→create C→de-leteA。
由上面例子說明,當節(jié)點出現(xiàn)跨層次的移動時,是通過創(chuàng)建與刪除操作來執(zhí)行的,這樣比較影響性能,所以在使用React開發(fā)的時候盡量不要使用這個跨層次操作,如果在實際應用中必須有此需求,建議采用CSS隱藏或者顯示節(jié)點操作。
3.2 component diff算法
對同一個組件更新時,如果內(nèi)部沒有發(fā)生變化,則繼續(xù)后面的更新,同時React也會提供特定的算法來進行組件更新。若非同一個類型的組件,則會將該組件判斷為dirty組件,會直接替換組件下的所有節(jié)點如圖3。
當結點D需要替換成結點G時,即使他們的結構很類似,并且子結點都相同,但是React還是會直接刪除結點D,并且重新創(chuàng)建結點G,而不是直接進行簡單的替換。雖然這種算法會影響性能,但是對于這種情況相對較少,React沒有進行優(yōu)化,因為在實際應用中很難因為此操作而影響到性能問題。
3.3 element diff
React對同一層次結點進行大量的操作,因為基于此操作的情況最多,React會提供三種不同節(jié)點的操作:IN-SERT_MARKUP(插入)、MOVE_EXISTING(移動)和RE-MOVE_NODE(刪除)。
如果是全新的節(jié)點,則直接進行插入操作,而如果是可以移動的操作,則復用以前的操作,如果不能直接進行復用與更新操作,則進行刪除操作。
如圖4,如果之前的Dom樹上包含結點ABCD,更新后的順序為BADC,這個時候就會進行diff差異化比較,發(fā)現(xiàn)新的結點是B,這時會先刪除A,再插入B,然后刪除BCD,插入ADC,由些可見,這個操作也是比較繁瑣,并且沒有效率,執(zhí)行移動操作會比執(zhí)行插入與刪除有效率得多,React針對這種情況提出了優(yōu)化操作,允許對同一層次的同一個位置的結點,通過不同的Key來進行區(qū)分。
如圖5,與上例相同,對新老集合的節(jié)點進行diff差異化對比,通過key值來發(fā)現(xiàn)新老Dom組的節(jié)點都是相同結點,所以不需要進行刪除與創(chuàng)建操作,只需進行簡單的移動操作就可以達到目標。下面通過源碼進行詳細分析利用diff達到上述操作。
首先對新老Dom樹的節(jié)點進行一次循環(huán)遍歷,通過唯一的key值來判斷新老Dom樹的節(jié)點是否相同。如果結點相同,則進行移動操作,但在移動操作之前會將當前節(jié)點順序與最后一個結點順序進行比較,若非最后一個節(jié)點,才會進行移動操作。
以上圖5為例,更為清晰直觀描述diff差異對比過程:
從新集合中取得B,判斷老集合中存在相同節(jié)點B,通過對比節(jié)點位置判斷是否進行移動操作,B在老集合中的位置B._mountlndex=1,此時lastIndex=0,不滿足child._mountIn-dex 從新集合中取得A,判斷老集合中存在相同節(jié)點A,通過對比節(jié)點位置判斷是否進行移動操作,A在老集合中的位置A.mountlndex=0,此時lastIndex=1,滿足child._mountIndex 從新集合中取得D,判斷老集合中存在相同節(jié)點D,通過對比節(jié)點位置判斷是否進行移動操作,D在老集合中的位置D._mountIndex=3,此時lastIndex=1,不滿足child._mountIn-dex 從新集合中取得C,判斷老集合中存在相同節(jié)點C,通過對比節(jié)點位置判斷是否進行移動操作,C在老集合中的位置c._mountIndex=2,此時lastIndex=3,滿足child._mountIndex 以上情況主要針對新老Dom存在節(jié)點相同,但位置不同時進行的移動操作,而當新集合中存在新的節(jié)點插入并且需要執(zhí)行刪除操作時,需要進行diff操作。 如圖6,新的Dom在判斷第一個節(jié)點是B時,會在老Dom里去查找是否有B,而B在老集合的位置序號是1,因為當前判斷節(jié)點是0,所以不需要進行移動操作,更新當前節(jié)點位置為1,并將B的位置序號改為0,這樣依次循環(huán)。 從新集合中取得E,判斷老集合中不存在相同節(jié)點E,則創(chuàng)建新節(jié)點E;更新lastIndex=1,并將E的位置更新為新集合中的位置,nextIndex++進入下一個節(jié)點的判斷。 依次進行上述操作,對所有的結點進行Diff操作后,最后需要對老Dom樹進行遍歷操作,判斷是否有刪除操作,在上述例子中,需要刪除D,至此所有操作完成。 4總結 React通過模擬一個新的Visual Dom來解決更新問題,從而將復雜度為O(n3)轉換成線性復雜度問題,這樣非常適合處理一些復雜的應用場景。但通過算法的具體分析發(fā)現(xiàn),在寫React時應該盡量保持Dom樹的結構,并盡量減少大范圍的移動結點操作,從而使得React的渲染能力達到最優(yōu)。 5結束語 本文通過對React的Diff算法分析,深入剖析了Visual dom的更新原則,這讓開發(fā)者在寫代碼時無需關心這部分內(nèi)容,而只需注重業(yè)務邏輯問題,從而簡化了開發(fā)過程。正是由于第2節(jié)中提到的Dom的三個基本特性,使得算法得以實現(xiàn)。通過對算法的深入研究,對今后React開發(fā)優(yōu)化有了深入的理論基礎。