1、react diff算法node
備註:傳統算法的複雜度計算方法有興趣能夠參考以下地址:https://grfia.dlsi.ua.es/ml/a...react
React的diff算法(React16如下版本)
(1)什麼是調和?算法
將Virtual DOM樹轉換成actual DOM樹的最少操做的過程 稱爲 調和 。
(2)什麼是React diff算法?api
diff算法是調和的具體實現。diff算法的本質是對傳統tree遍歷算法的優化
(3)diff策略瀏覽器
React用 三大策略 將O(n^3)複雜度 轉化爲 O(n)複雜度
策略一(tree diff):緩存
Web UI中DOM節點跨層級的移動操做特別少,能夠忽略不計。
策略二(component diff):數據結構
擁有相同類的兩個組件 生成類似的樹形結構, 擁有不一樣類的兩個組件 生成不一樣的樹形結構。
策略三(element diff):架構
對於同一層級的一組子節點,經過惟一id區分。
tree diff
(1)React經過updateDepth對Virtual DOM樹進行層級控制。
(2)對樹分層比較,兩棵樹 只對同一層次節點 進行比較。若是該節點不存在時,則該節點及其子節點會被徹底刪除,不會再進一步比較。
(3)只需遍歷一次,就能完成整棵DOM樹的比較。dom
以下圖所示:異步
那麼問題來了,若是DOM節點出現了跨層級操做,diff會咋辦呢?
答:diff只簡單考慮同層級的節點位置變換,若是是跨層級的話,只有建立節點和刪除節點的操做。
如上圖所示,以A爲根節點的整棵樹會被從新建立,而不是移動,所以 官方建議不要進行DOM節點跨層級操做,能夠經過CSS隱藏、顯示節點,而不是真正地移除、添加DOM節點。
component diff
React對不一樣的組件間的比較,有三種策略
(1)同一類型的兩個組件,按原策略(層級比較)繼續比較Virtual DOM樹便可。
(2)同一類型的兩個組件,組件A變化爲組件B時(A、B類型相同、結構相同),可能Virtual DOM沒有任何變化,若是知道這點(變換的過程當中,Virtual DOM沒有改變),可節省大量計算時間,因此 用戶 能夠經過 shouldComponentUpdate() 來判斷是否須要 判斷計算。
(3)不一樣類型的組件,將一個(將被改變的)組件判斷爲dirty component(髒組件),從而替換 整個組件的全部節點。
注意:若是組件D和組件G的結構類似,可是 React判斷是 不一樣類型的組件,則不會比較其結構,而是刪除 組件D及其子節點,建立組件G及其子節點。
element diff
當節點處於同一層級時,diff提供三種節點操做:刪除、插入、移動。
插入:組件 C 不在集合(A,B)中,須要插入
刪除:
(1)組件 D 在集合(A,B,D)中,但 D的節點已經更改,不能複用和更新,因此須要刪除 舊的 D ,再建立新的。
(2)組件 D 以前在 集合(A,B,D)中,但集合變成新的集合(A,B)了,D 就須要被刪除。
移動:組件D已經在集合(A,B,C,D)裏了,且集合更新時,D沒有發生更新,只是位置改變,如新集合(A,D,B,C),D在第二個,無須像傳統diff,讓舊集合的第二個B和新集合的第二個D 比較,而且刪除第二個位置的B,再在第二個位置插入D,而是 (對同一層級的同組子節點) 添加惟一key進行區分,移動便可。
重點說下移動的邏輯:
情形一:新舊集合中存在相同節點但位置不一樣時,如何移動節點
移動一、
(1)看着上圖的 B,React先重新中取得B,而後判斷舊中是否存在相同節點B,當發現存在節點B後,就去判斷是否移動B。
B在舊的節點中的index=1,它的lastIndex=0,不知足 index < lastIndex 的條件,所以 B 不作移動操做。此時,一個操做是,lastIndex=(index,lastIndex)中的較大數=1.
注意:lastIndex有點像浮標,或者說是一個map的索引,一開始默認值是0,它會與map中的元素進行比較,比較完後,會改變本身的值的(取index和lastIndex的較大數)。
(2)看着 A,A在舊的index=0,此時的lastIndex=1(由於先前與新的B比較過了),知足index<lastIndex,所以,對A進行移動操做,此時lastIndex=max(index,lastIndex)=1。
(3)看着D,同(1),不移動,因爲D在舊的index=3,比較時,lastIndex=1,因此改變lastIndex=max(index,lastIndex)=3
(4)看着C,同(2),移動,C在舊的index=2,知足index<lastIndex(lastIndex=3),因此移動。
因爲C已是最後一個節點,因此diff操做結束。
情形二:新集合中有新加入的節點,舊集合中有刪除的節點
移動二、
(1)B不移動,不贅述,更新l astIndex=1
(2)新集合取得 E,發現舊不存在,故在lastIndex=1的位置 建立E,更新lastIndex=1
(3)新集合取得C,C不移動,更新lastIndex=2
(4)新集合取得A,A移動,同上,更新lastIndex=2
(5)新集合對比後,再對舊集合遍歷。判斷 新集合 沒有,但 舊集合 有的元素(如D,新集合沒有,舊集合有),發現 D,刪除D,diff操做結束。
diff的不足與待優化的地方
移動三、
看圖的 D,此時D不移動,但它的index是最大的,致使更新lastIndex=3,從而使得其餘元素A,B,C的index<lastIndex,致使A,B,C都要去移動。
理想狀況是隻移動D,不移動A,B,C。所以,在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,會影響React的渲染性能。
2、 React Fiber(React16版本)
引言:
diff算法相對傳統算法已是比較高效的計算機制了,可是人老是要有追求,三年前左右react就發現了reconciliation的一個潛在問題,就是在對比兩顆樹的時候,花費的時間太長,可能致使瀏覽器假死,因此就啓動了一個項目來重寫reconciliation,那就是react fiber.
爲何?
這裏不得不提瀏覽器的渲染機制,如今基本上公認的是60fps,也就是說瀏覽器會在每秒內渲染60次,也就是基本上16.7ms渲染一次。
(爲何是60fps呢,這裏和硬件的刷新頻率有關係,有興趣的能夠查下)
基本渲染流程以下
1,執行js
2,樣式計算
3,計算佈局,執行
4,pait,繪製各層
5,合成各層的繪製結果,呈如今瀏覽器上。
因此基本上就是在16.7ms內執行完這些操做,就是比較完美的啦,可是事情不可能這麼完美,好比若是js代碼執行時間特別長的話,一直在等你的js執行完以後,纔會去渲染,頁面就是一直空白。
一、 React從版本16開始棄用diff算法,改成Fiber渲染方式進行組件差別化比較
舊版的diff算法是遞歸比較,對virtural dom的更新和渲染是同步的。就是當一次更新或者一次加載開始之後,virtual dom的diff比較而且渲染的過程是一口氣完成的。若是組件層級比較深,相應的堆棧也會很深,長時間佔用瀏覽器主線程,一些相似用戶輸入、鼠標滾動等操做得不到響應。形成線程柱塞,所以React官方改變了以前的Virtual Dom的渲染機制,新架構使用鏈表形式的虛擬 DOM,新的架構使原來同步渲染的組件如今能夠異步化,可中途中斷渲染,執行更高優先級的任務。釋放瀏覽器主線程。
咱們使用兩張圖來區分兩種算法之間的區別
這個就是之前的diff算法渲染圖:
當全部的事情都等待reconciliation結束的時候,可能有其餘更高級別的功能需求進來,好比用戶點擊輸入框,或者是點擊按鈕等操做,可是因爲還在執行,就會就一直卡住,讓用戶認爲頁面在假死。
因此最好的辦法,也是用的最多的辦法,無論是在計算機系統仍是哪裏,那就是分片,我借了你的東西,我用一段時間,就得過來就還給你,等你用完了以後,我再過來借一次,好借好還,再借不難。
這個是新的Fiber渲染機制:
這基本就是react fiber的核心所在!
同時應該說明:React15與React16 兩個 DOM 的結構和遍歷方式已經徹底不一樣。
二、 算法流程
fiber tree 算法
具體流程和原來的差很少,其實也仍是找出兩次更新之間的差別,而後渲染到瀏覽器上面。
fiber會在首次render函數執行完以後,react會保存一份react fiber樹,而後會循環利用,不會重複創建,稱爲current 樹。
2,當有setstate或者其餘更新的時候,就會根據如今的current樹從新生成一份包含變化的樹。這裏最重要的就是在對比兩顆樹的過程當中是異步的,隨時能夠中斷,恢復,可是當更新的時候是同步的,也就是說 diff 過程當中,是異步,commit是同步的。
diff 具體過程
這裏就是根據信息,來遍歷fibertree樹而後找不不一樣,這裏不同的一點是由於加了不少的指針,相似加了不少直達電梯,節省了不少時間,能夠直接到達。
任何一項工做都會有下面幾步, 首先獲取該在哪裏作,而後開始作,再接着就是花時間幹完這項工做,最後退出,繼續尋找下一步該在哪裏工做。
對應關係就是
獲取該在哪裏作: performUnitOfWork
開始作: beginWork
完成工做: completeUnitOfWork
尋找下一步哪裏作: completeWork
全部的函數都在(packages/react-reconciler/src/ReactFiberScheduler.js)
能夠看下別人作的效果圖
tree的執行順序: a1-b1-b1完成-b2-c1-d1-d1完成-d2-d2完成-c1完成-b2完成-b3-c2-c2完成-b3完成-a1完成。
fiber 首次 render 的時候,就會調用一次 requestIdeCallback,這個 api 會進行循環
這個循環,它負責變動 current fiber(當前的 fiber 節點) 前面提到,鏈表天生能夠拿到 節點自己,還能拿到父節點,兄弟節點,子節點
惟一要記住的一點就是這裏的過程是異步的,隨時可能會暫停,或者中止,或者須要恢復過來從新執行。
commit
這裏就是同步的了,不過速度也會很快的,由於這裏把哪些改變了的fiber node造成了一個鏈表,若是中間沒有更新的話,會快速的跳到下面去。
相似於下圖的鏈表
看一下fiber架構 組建的渲染順序:
加入fiber的react將組件更新分爲兩個時期Reconciliation Phase和Commit Phase。Reconciliation Phase的任務乾的事情是,找出要作的更新工做(Diff Fiber Tree),就是一個計算階段,計算結果能夠被緩存,也就能夠被打斷;Commmit Phase 須要提交全部更新並渲染,爲了防止頁面抖動,被設置爲不能被打斷。
這兩個時期以render爲分界,
render前的生命週期爲phase1,
render後的生命週期爲phase2
phase1的生命週期是能夠被打斷的,每隔一段時間它會跳出當前渲染進程,去肯定是否有其餘更重要的任務。此過程,React 在 workingProgressTree (並非真實的virtualDomTree)上覆用 current 上的 Fiber 數據結構來一步地(經過requestIdleCallback)來構建新的 tree,標記處須要更新的節點,放入隊列中。
phase2的生命週期是不可被打斷的,React 將其全部的變動一次性更新到DOM上。
這裏最重要的是phase1這是時期所作的事。所以咱們須要具體瞭解phase1的機制。
PS: componentWillMount componentWillReceiveProps componentWillUpdate 幾個生命週期方法,在Reconciliation Phase被調用,有被打斷的可能(時間用盡等狀況),因此可能被屢次調用。其實 shouldComponentUpdate 也可能被屢次調用,只是它只返回true或者false,沒有反作用,能夠暫時忽略。
若是不被打斷,那麼phase1執行完會直接進入render函數,構建真實的virtualDomTree
若是組件再phase1過程當中被打斷,即當前組件只渲染到一半(也許是在willMount,也許是willUpdate~反正是在render以前的生命週期),那麼react會怎麼幹呢? react會放棄當前組件全部幹到一半的事情,去作更高優先級更重要的任務(固然,也多是用戶鼠標移動,或者其餘react監聽以外的任務),當全部高優先級任務執行完以後,react經過callback回到以前渲染到一半的組件,從頭開始渲染。(看起來放棄已經渲染完的生命週期,會有點不合理,反而會增長渲染時長,可是react確實是這麼幹的)
看到這裏,相信聰明的同窗已經發現一些問題啦~
也就是 全部phase1的生命週期函數均可能被執行屢次,由於可能會被打斷重來
這樣的話,就和react16版本以前有很大區別了,由於可能會被執行屢次,那麼咱們最好就得保證phase1的生命週期每一次執行的結果都是同樣的,不然就會有問題,所以,最好都是純函數。
(因此react16目前都沒有把fiber enable,其實react16仍是以 同步的方式在作組建的渲染,由於這樣的話,不少咱們用老版本react寫的組件就有可能都會有問題,包括用的不少開源組件,可是後面應該會enable,讓開發者能夠開啓fiber異步渲染模式~)
對了,還有一個問題,飢餓問題,即若是高優先級的任務一直存在,那麼低優先級的任務則永遠沒法進行,組件永遠沒法繼續渲染。這個問題facebook目前好像還沒解決,但之後會解決~
因此,facebook在react16增長fiber結構,其實並非爲了減小組件的渲染時間,事實上也並不會減小,最重要的是如今可使得一些更高優先級的任務,如用戶的操做可以優先執行,提升用戶的體驗,至少用戶不會感受到卡頓~