首頁 > webfront > ECMAS > react > > 正文

重談react優勢——react技術棧回顧

發布人:zhoulujun    點擊:

react剛剛推出的時候,講react優勢搜索結果是幾十頁。現在,react已經慢慢退火,該用用react技術棧的已經使用上,填過多少坑,加過多少班,

react剛剛推出的時候,講react優勢搜索結果是幾十頁。

現在,react已經慢慢退火,該用用react技術棧的已經使用上,填過多少坑,加過多少班,血淚控訴也不下千文。

今天,再談一遍react優勢,WTF?

React的收益有哪些?React的優勢是什么?react和vue、angularJS等其它框架對比優勢?


而作為總結回顧。react在工程實踐中,帶來哪些思想上的質變?


virtual dom虛擬DOM概念

它并不直接對DOM進行操作,引入了一個叫做virtual dom的概念,安插在javascript邏輯和實際的DOM之間,好處是減少DOM操作,減少DOM操作的目的是提高瀏覽器的渲染性能。

虛擬dom就中小型項目而言,的確從表象上看不出太多的優勢,因為它解決的是底層的dom渲染,IO開銷問題。但是想想facebook的體量,不難猜出react的誕生是為了解決更復雜更大型的項目開發和管理的。

實際上React和Vue其實也在操作DOM,只是比較高效地在操作DOM而已,虛擬DOM其實最終也會映射到真實DOM,雖然虛擬DOM只會將變化的部分更新到真實DOM,但實際上直接操作DOM也可以通過某些方式去優化,那么:

    1、操作data,不直接操作DOM有什么好處?

         更少的代碼做更多的事。

    2、操作data會給DOM操作帶來什么不好的地方嗎?

          不會,但是不是所有功能“使用操作data”都可以代替的。

    3、會不會比直接操作DOM存在什么難度?

         不會有難度,但是思維需要有一些轉變。


JSX雖然做了抽象視圖,但她是聲明式API,能夠保證你看一眼就知道組件樹的結構,譬如:


這結構還算清楚吧,基本一眼就知道這個一個面板由輸入框、列表、摘要組成,而且布局也清楚了,自上而下。而且,通過查看一個源文件就可以知道你的組件將會如何渲染。這是最大的好處,盡管這和 Angular 模板沒什么不同。具體參看:ReactJS For Stupid People


之前寫UI的時候往往為了性能,要設計很多DOM的操作邏輯,用了react之后,這些都不給你做了,由他的state跟props來傳遞給VDOM,很省事,更專注于UI層面。


學會了react以及這個JSX語法,你不光可以通過react寫web;也可以通過react-native寫ios或者android的應用;甚至可以通過react-blessed寫terminal可視化應用;當然也可以通過react-native-desktop寫桌面應用。因為JSX這種聲明式語法實際是在構建一個抽象的視圖層,這種抽象可以通過不同適配器適配到各種顯示終端,這總夠屌吧?


unidirectional data flow-單向數據流

React倡導使用flux模式來進行組件間數據傳輸,這種做法叫unidirectional data flow(單向數據流),單向數據流的好處是與之前angularJS提出的two-way data binding相比較而言,因為單向,所以各種變化都是可預計、可控制的。不像two-way data binding那樣,變化一但復雜起來,大家都互相觸發變化,到最后一個地方變了,你根本猜不出來她還會導致其他什么地方跟著一起變。這個需要大量實踐才能有所感受,如果你初學,那聽聽就算了,不必死磕。


react項目結構更加清晰:

virtual dom、redux、action,分部分別存放,就象java寫后臺查數據本來用jdbc一條sql就搞定,但分成action service dao分門別類地存放,這樣維護性好,大公司的代碼需要規范,這樣出了問題好找原因。


組件化

一切都是component:代碼更加模塊化,重用代碼更容易,可維護性高。

這里就涉及到react的 架構,比如:

smart, dumb component  

把組件分成兩大類 Smart Components (容器) & Dumb Components(顆粒化組件)

這樣做的好處:

  • 有助理你分離關注點,這樣的話更有助于理解你的app的業務邏輯 和 它的ui

  • 更有助于復用你的dumb組件,你可以將你的dumb組件復用于別的state下,而且這兩個state還完全不同

  • 本質上dumb 組件 其實 就是你的app的調色版。。你可以將它們放到一個頁面上。。然后讓設計師除了app的業務邏輯,樣式隨便怎么改,

參看文章:Smart and Dumb Components 

高階組件(HOC-higher order component) 

高階組件(HOC)是react中對組件邏輯進行重用的高級技術。但高階組件本身并不是React API。它只是一種模式,這種模式是由react自身的組合性質必然產生的。

具體而言,高階組件就是一個函數,且該函數接受一個組件作為參數,并返回一個新的組件

const EnhancedComponent = higherOrderComponent(WrappedComponent);

對比組件將props屬性轉變成UI,高階組件則是將一個組件轉換成另一個新組件。

好處:使用高階組件(HOC)解決交叉問題

參看文章:高階組件

總結下,看看一個人的組件化水準,

  • pure component

  • functional component

  • smart, dumb component 

  • higher order component

  • hoc render hijacking

  • 會用 props.children React.children cloneElement

  • 提供 instance method

  • context

并理解react 內部實現原理

  • 懂 setState  是異步的

  • 懂 synthetic event

  • 懂 react-dom 分層和 react 沒有關系

  • 懂 reconciler

  • 懂 fiber  

具體問題如下:

  • 1. 怎么抽象一個帶搜索,單多選復合,有請求的 Selector,區分 smart 和 dumped。如果我再往上加功能,比如 autocomplete  等

  • 2. 怎么實現對表單的抽象,數據驗證怎么統一處理

  • 3. 用 react 來實現一個可視化編輯器的引擎,怎么設計,怎么抽象與 model 的交互,再引入 redux 呢,怎么支持第三方組件熱插拔

  • 4. 用 react 和 redux 模擬多人協作的 Todo,node 作為后端,怎么設計


同構、純粹的javascrip

因為搜索引擎的爬蟲程序依賴的是服務端響應而不是JavaScript的執行,預渲染你的應用有助于搜索引擎優化。


react一些常見問題:

setState()函數在任何情況下都會導致組件重渲染嗎?如果setState()中參數還是原來沒有發生任何變化的state呢?

對setState用得深了,就容易犯錯,所以我們開門見山先把理解setState的關鍵點列出來。

  • setState不會立刻改變React組件中state的值;

  • setState通過引發一次組件的更新過程來引發重新繪制;

  • 多次setState函數調用產生的效果會合并

  • setState后,知道reader時,才真正改變state的值

    shouldComponentUpdate函數返回false,因為更新被中斷,所以不調用render,但是React不會放棄掉對this.state的更新的,依然會更新this.state


傳入 setState 函數的第二個參數的作用是什么?

該函數會在setState函數調用完成并且組件開始重渲染的時候被調用,我們可以用該函數來監聽渲染是否完成(一般沒有什么卵用)

 調用 setState 之后發生了什么?

 在代碼中調用setState函數之后,React 會將傳入的參數對象與組件當前的狀態合并,然后觸發所謂的調和過程(Reconciliation)。經過調和過程,React 會以相對高效的方式根據新的狀態構建 React 元素樹并且著手重新渲染整個UI界面。在 React 得到元素樹之后,React 會自動計算出新的樹與老樹的節點差異,然后根據差異對界面進行最小化重渲染。在差異計算算法中,React 能夠相對精確地知道哪些位置發生了改變以及應該如何改變,這就保證了按需更新,而不是全部重新渲染。


用shouldComponentUpdate做優化的意義大嗎?shouldComponentUpdate將帶來可測量和可感知的提升?

如果不能,那就別用:你可能應該避免用它。據React團隊的說,shouldComponentUpdate是一個保證性能的緊急出口,意思就是你不到萬不得已就別用它。具體參考:什么時候使用shouldComponentUpdate方法?

一般情況下setState() 確立后總是觸發一次重繪,除非在 shouldComponentUpdate() 中實現了條件渲染邏輯。如果使用可變的對象,但是又不能在 shouldComponentUpdate() 中實現這種邏輯,僅在新 state 和之前的 state 存在差異的時候調用 setState() 可以避免不必要的重新渲染。


react異步數據如ajax請求應該放在哪個生命周期?

對于同步的狀態改變,是可以放在componentWillMount,對于異步的,最好好放在componentDidMount。但如果此時有若干細節需要處理,比如你的組件需要渲染子組件,而且子組件取決于父組件的某個屬性,那么在子組件的componentDidMount中進行處理會有問題:因為此時父組件中對應的屬性可能還沒有完整獲取,因此就讓其在子組件的componentDidUpdate中處理。

具體參考:《react異步數據如ajax請求應該放在哪個生命周期?


React 中的 keys 是什么,為什么它們很重要?

在開發過程中,我們需要保證某個元素的 key 在其同級元素中具有唯一性。在 React Diff 算法中 React 會借助元素的 Key 值來判斷該元素是新近創建的還是被移動而來的元素,從而減少不必要的元素重渲染。此外,React 還需要借助 Key 值來判斷元素與本地狀態的關聯關系,因此我們絕不可忽視轉換函數中 Key 的重要性。

keys 是幫助 React 跟蹤哪些項目已更改、添加或從列表中刪除的屬性。

每個keys 在兄弟元素之間是獨一無二的。我們已經談過幾次關于一致化處理(reconciliation)的過程,而且這個一致化處理過程(reconciliation)中的一部分正在執行一個新的元素樹與最前一個的差異。keys 使處理列表時更加高效,因為 React 可以使用子元素上的 keys 快速知道元素是新的還是在比較樹時才被移動的。

而且 keys 不僅使這個過程更有效率,而且沒有keys,React 不知道哪個本地狀態對應于移動中的哪個項目。所以當你 map 的時候,不要忽略了 keys 。


受控組件( controlled component )與不受控制的組件( uncontrolled component )有什么區別?

React 的很大一部分是這樣的想法,即組件負責控制和管理自己的狀態(任何改變代用setSate處理)

那么不受控組件呢?組件數據不全部是setState來處理,還有DOM交互,比如refs這玩意來操控真實DOM

雖然不受控制的組件通常更容易實現,因為您只需使用引用從DOM獲取值,但是通常建議您通過不受控制的組件來支持受控組件。


主要原因是受控組件支持即時字段驗證,允許您有條件地禁用/啟用按鈕,強制輸入格式,并且更多的是 『the React way』。


描述事件在React中的處理方式


為了解決跨瀏覽器兼容性問題,您的 React 中的事件處理程序將傳遞SyntheticEvent 的實例,它是 React 的瀏覽器本機事件的跨瀏覽器包裝器。


這些 SyntheticEvent 與您習慣的原生事件具有相同的接口,除了它們在所有瀏覽器中都兼容。有趣的是,React 實際上并沒有將事件附加到子節點本身。React 將使用單個事件監聽器監聽頂層的所有事件。這對于性能是有好處的,這也意味著在更新DOM時,React 不需要擔心跟蹤事件監聽器。




在什么情況下你會優先選擇使用 Class Component 而不是 Functional Component?

在組件需要包含內部狀態或者使用到生命周期函數的時候使用 Class Component ,否則使用函數式組件。


簡單介紹下react的diff

計算一棵樹形結構轉換成另一棵樹形結構的最少操作,是一個復雜且值得研究的問題。傳統 diff 算法通過循環遞歸對節點進行依次對比,效率低下,算法復雜度達到 O(n^3),其中 n 是樹中節點的總數。O(n^3) 到底有多可怕,這意味著如果要展示1000個節點,就要依次執行上十億次的比較。這種指數型的性能消耗對于前端渲染場景來說代價太高了!現今的 CPU 每秒鐘能執行大約30億條指令,即便是最高效的實現,也不可能在一秒內計算出差異情況。。React 通過制定大膽的策略,將 O(n^3) 復雜度的問題轉換成 O(n) 復雜度的問題。

 react的diff 策略:

  •  Web UI 中 DOM 節點跨層級的移動操作特別少,可以忽略不計。

  •  擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構。 

  • 對于同一層級的一組子節點,它們可以通過唯一 id 進行區分。 

基于以上三個前提策略,React 分別對 tree diffcomponent diff 以及 element diff 進行算法優化,事實也證明這三個前提策略是合理且準確的,它保證了整體界面構建的性能。 


  • tree diff:

    基于策略一,React 對樹的算法進行了簡潔明了的優化,即對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。

既然 DOM 節點跨層級的移動操作少到可以忽略不計,針對這一現象,React 通過 updateDepth 對 Virtual DOM 樹進行層級控制,只會對相同顏色方框內的 DOM 節點進行比較,即同一個父節點下的所有子節點。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用于進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。

0c08dbb6b1e0745780de4d208ad51d34_hd.jpg

updateChildren: function(nextNestedChildrenElements, transaction, context) {
  updateDepth++;
  var errorThrown = true;
  try {
    this._updateChildren(nextNestedChildrenElements, transaction, context);
    errorThrown = false;
  } finally {
    updateDepth--;
    if (!updateDepth) {
      if (errorThrown) {
        clearQueue();
      } else {
        processQueue();
      }
    }
  }
}

分析至此,大部分人可能都存在這樣的疑問:如果出現了 DOM 節點跨層級的移動操作,React diff 會有怎樣的表現呢?是的,對此我也好奇不已,不如試驗一番。


如下圖,A 節點(包括其子節點)整個被移動到 D 節點下,由于 React 只會簡單的考慮同層級節點的位置變換,而對于不同層級的節點,只有創建和刪除操作。當根節點發現子節點中 A 消失了,就會直接銷毀 A;當 D 發現多了一個子節點 A,則會創建新的 A(包括子節點)作為其子節點。此時,React diff 的執行情況:create A -> create B -> create C -> delete A。

d712a73769688afe1ef1a055391d99ed_hd (1).jpg

由此可發現,當出現節點跨層級移動時,并不會出現想象中的移動操作,而是以 A 為根節點的樹被整個重新創建,這是一種影響 React 性能的操作,因此 React 官方建議不要進行 DOM 節點跨層級的操作。


提示:在開發組件時,保持穩定的 DOM 結構會有助于性能的提升。例如,可以通過 CSS 隱藏或顯示節點,而不是真的移除或添加 DOM 節點。

component diff:

  • 如果是同一類型的組件,按照原策略繼續比較 virtual DOM tree。

  • 如果不是,則將該組件判斷為 dirty component,從而替換整個組件下的所有子節點。對于同一類型的組件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點那可以節省大量的 diff 運算時間,因此 React 允許用戶shouldComponentUpdate() 來判斷該組件是否需要進行 diff。

如下圖,當 component D 改變為 component G 時,即使這兩個 component 結構相似,一旦 React 判斷 D 和 G 是不同類型的組件,就不會比較二者的結構,而是直接刪除 component D,重新創建 component G 以及其子節點。雖然當兩個 component 是不同類型但結構相似時,React diff 會影響性能,但正如 React 官方博客所言:不同類型的 component 是很少存在相似 DOM tree 的機會,因此這種極端因素很難在實現開發過程中造成重大影響的。

52654992aba15fc90e2dac8b2387d0c4_hd.jpg

element diff:

當節點處于同一層級時,React diff 提供了三種節點操作,分別為:INSERT_MARKUP(插入)、MOVE_EXISTING(移動)和 REMOVE_NODE(刪除)

  • INSERT_MARKUP,新的 component 類型不在老集合里, 即是全新的節點,需要對新節點執行插入操作。

  • MOVE_EXISTING,在老集合有新 component 類型,且 element 是可更新的類型,generateComponentChildren 已調用 receiveComponent,這種情況下 prevChild=nextChild,就需要做移動操作,可以復用以前的 DOM 節點。

  • REMOVE_NODE,老 component 類型,在新集合里也有,但對應的 element 不同則不能直接復用和更新,需要執行刪除操作,或者老 component 不在新集合里的,也需要執行刪除操作。

如下圖,老集合中包含節點:A、B、C、D,更新后的新集合中包含節點:B、A、D、C,此時新老集合進行 diff 差異化對比,發現 B != A,則創建并插入 B 至新集合,刪除老集合 A;以此類推,創建并插入 A、D 和 C,刪除 B、C 和 D。

7541670c089b84c59b84e9438e92a8e9_hd.jpg

React 提出優化策略:允許開發者對同一層級的同組子節點,添加唯一 key 進行區分,雖然只是小小的改動,性能上卻發生了翻天覆地的變化!

7b9beae0cf0a5bc8c2e82d00c43d1c90_hd.jpg

總結

  • React 通過制定大膽的 diff 策略,將 O(n3) 復雜度的問題轉換成 O(n) 復雜度的問題;

  • React 通過分層求異的策略,對 tree diff 進行算法優化;

  • React 通過相同類生成相似樹形結構,不同類生成不同樹形結構的策略,對 component diff 進行算法優化;

  • React 通過設置唯一 key的策略,對 element diff 進行算法優化;

  • 建議,在開發組件時,保持穩定的 DOM 結構會有助于性能的提升;

  • 建議,在開發過程中,盡量減少類似將最后一個節點移動到列表首部的操作,當節點數量過大或更新操作過于頻繁時,在一定程度上會影響 React 的渲染性能。


diff算法作為react的核心,非三言兩語能夠說起道明,建議參看:React 源碼剖析系列 - 不可思議的 react diff 


怎么看待不可變數據?

這個暫待完善


ssr (server side render)會有什么性能問題,哪些會引起內存泄露,引入 redux 后怎么處理請求的邏輯


參考:從零開始搭建React同構應用(三):配置SSR


參考文章:

什么時候使用shouldComponentUpdate方法?

setState為什么不會同步更新組件狀態

setState:這個API設計到底怎么樣

高階組件