從Context源碼實現談React性能優化

2020-12-24 51CTO

學完這篇文章,你會收穫:

  1. 了解Context的實現原理
  2. 源碼層面掌握React組件的render時機,從而寫出高性能的React組件
  3. 源碼層面了解shouldComponentUpdate、React.memo、PureComponent等性能優化手段的實現

我會儘量將文章寫的通俗易懂。但是,要完全理解文章內容,需要你掌握這些前置知識:

  1. Fiber架構的大體工作流程
  2. 優先級與更新在React源碼中的意義

組件render的時機

Context的實現與組件的render息息相關。在講解其實現前,我們先來了解render的時機。

換句話說,組件在什麼時候render?

這個問題的答案,已經在React組件到底什麼時候render啊聊過。在這裡再概括下:

在React中,每當觸發更新(比如調用this.setState、useState),會為組件創建對應的fiber節點。

fiber節點互相連結形成一棵Fiber樹。

有2種方式創建fiber節點:

bailout,即復用前一次更新該組件對應的fiber節點作為本次更新的fiber節點。

render,經過diff算法後生成一個新fiber節點。組件的render(比如ClassComponent的render方法調用、FunctionComponent的執行)就發生在這一步。

經常有同學問:React每次更新都會重新生成一棵Fiber樹,性能不會差麼?

React性能確實不算很棒。但如你所見,Fiber樹生成過程中並不是所有組件都會render,有些滿足優化條件的組件會走bailout邏輯。

比如,對於如下Demo:

  1. function Son() { 
  2.   console.log('child render!'); 
  3.   return <div>Son</div>; 
  4.  
  5.  
  6. function Parent(props) { 
  7.   const [count, setCount] = React.useState(0); 
  8.  
  9.   return ( 
  10.     <div onClick={() => {setCount(count + 1)}}> 
  11.       count:{count
  12.       {props.children} 
  13.     </div> 
  14.   ); 
  15.  
  16.  
  17. function App() { 
  18.   return ( 
  19.     <Parent> 
  20.       <Son/> 
  21.     </Parent> 
  22.   ); 
  23.  
  24. const rootEl = document.querySelector("#root"); 
  25. ReactDOM.render(<App/>, rootEl); 

在線Demo地址[2]

點擊Parent組件的div子組件,觸發更新,但是child render!並不會列印。

這是因為Son組件會進入bailout邏輯。

bailout的條件

要進入bailout邏輯,需同時滿足4個條件:

1.oldProps === newProps

即本次更新的props全等於上次更新的props。

注意這裡是全等比較。

我們知道組件render會返回JSX,JSX是React.createElement的語法糖。

所以render的返回結果實際上是React.createElement的執行結果,即一個包含props屬性的對象。

即使本次更新與上次更新props中每一項參數都沒有變化,但是本次更新是React.createElement的執行結果,是一個全新的props引用,所以oldProps !== newProps。

2.context value沒有變化

我們知道在當前React版本中,同時存在新老兩種context,這裡指老版本context。

3.workInProgress.type === current.type

更新前後fiber.type不變,比如div沒變為p。

4.!includesSomeLane(renderLanes, updateLanes) ?

當前fiber上是否存在更新,如果存在那麼更新的優先級是否和本次整棵Fiber樹調度的優先級一致?

如果一致代表該組件上存在更新,需要走render邏輯。

bailout的優化還不止如此。如果一棵fiber子樹所有節點都沒有更新,即使所有子孫fiber都走bailout邏輯,還是有遍歷的成本。

所以,在bailout中,會檢查該fiber的所有子孫fiber是否滿足條件4(該檢查時間複雜度O(1))。

如果所有子孫fiber本次都沒有更新需要執行,則bailout會直接返回null。整棵子樹都被跳過。

不會bailout也不會render,就像不存在一樣。對應的DOM不會產生任何變化。

老Context API的實現現

在我們大體了解了render的時機。有了這個概念,就能理解ContextAPI是如何實現的,以及為什麼被重構。

我們先看被廢棄的老ContextAPI的實現。

Fiber樹的生成過程是通過遍歷實現的可中斷遞歸,所以分為遞和歸2個階段。

Context對應數據會保存在棧中。

在遞階段,Context不斷入棧。所以Concumer可以通過Context棧向上找到對應的context value。

在歸階段,Context不斷出棧。

那麼老ContextAPI為什麼被廢棄呢?因為他沒法和shouldComponentUpdate或Memo等性能優化手段配合。

shouldComponentUpdate的實現

要探究更深層的原因,我們需要了解shouldComponentUpdate的原理,後文簡稱其為SCU。

使用SCU是為了減少不必要的render,換句話說:讓本該render的組件走bailout邏輯。

剛才我們介紹了bailout需要滿足的條件。那麼SCU是作用於這4個條件的哪個呢?

顯然是第一條:oldProps === newProps

當使用shouldComponentUpdate,這個組件bailout的條件會產生變化:

-- oldProps === newProps

++ SCU === false

同理,使用PureComponenet和React.memo時,bailout的條件也會產生變化:

-- oldProps === newProps

++ 淺比較oldProps與newsProps相等

回到老ContextAPI。

當這些性能優化手段:

使組件命中bailout邏輯

同時如果組件的子樹都滿足bailout的條件4

那麼該fiber子樹不會再繼續遍歷生成。

換言之,不會再經歷Context的入棧、出棧。

這種情況下,即使context value變化,子孫組件也沒法檢測到。

新Context API的實現

知道老ContextAPI的缺陷,我們再來看新ContextAPI是如何實現的。

當通過:

  1. ctx = React.createContext(); 

創建context實例後,需要使用Provider提供value,使用Consumer或useContext訂閱value。

如:

  1. ctx = React.createContext(); 
  2.  
  3. const NumProvider = ({children}) => { 
  4.   const [num, add] = useState(0); 
  5.  
  6.   return ( 
  7.     <Ctx.Provider value={num}> 
  8.       <button onClick={() => add(num + 1)}>add</button> 
  9.       {children} 
  10.     </Ctx.Provider> 
  11.   ) 

使用:

  1. const Child = () => { 
  2.   const {num} = useContext(Ctx); 
  3.   return <p>{num}</p> 

當遍歷組件生成對應fiber時,遍歷到Ctx.Provider組件,Ctx.Provider內部會判斷context value是否變化。

如果context value變化,Ctx.Provider內部會執行一次向下深度優先遍歷子樹的操作,尋找與該Provider配套的Consumer。

在上文的例子中會最終找到useContext(Ctx)的Child組件對應的fiber,並為該fiber觸發一次更新

注意這裡的實現非常巧妙:

一般更新是由組件調用觸發更新的方法產生。比如上文的NumProvider組件,點擊button調用add會觸發一次更新。

觸發更新的本質是為了讓組件創建對應fiber時不滿足bailout條件4:

!includesSomeLane(renderLanes, updateLanes) ?

從而進入render邏輯。

在這裡,Ctx.Provider中context value變化,Ctx.Provider向下找到消費context value的組件Child,為其fiber觸發一次更新。

則Child對應fiber就不滿足條件4。

這就解決了老ContextAPI的問題:

由於Child對應fiber不滿足條件4,所以從Ctx.Provider到Child,這棵子樹沒法滿足:

所以即使遍歷中途有組件進入bailout邏輯,也不會返回null,即不會無視這棵子樹的遍歷。

最終遍歷進行到Child,由於其不滿足條件4,會進入render邏輯,調用組件對應函數。

  1. const Child = () => { 
  2.   const {num} = useContext(Ctx); 
  3.   return <p>{num}</p> 

在函數調用中會調用useContext從Context棧中找到對應更新後的context value並返回。

總結

React性能一大關鍵在於:減少不必要的render。

從上文我們看到,本質就是讓組件滿足4個條件,從而進入bailout邏輯。

而ContextAPI本質是讓Consumer組件不滿足條件4。

我們也知道了,React雖然每次都會遍歷整棵樹,但會有bailout的優化邏輯,不是所有組件都會render。

極端情況下,甚至某些子樹會被跳過遍歷(bailout返回null)。

參考資料

[1]React技術揭秘: http://react.iamkasong.com/

[2]在線Demo地址: https://codesandbox.io/s/quirky-chaplygin-5bx67?file=/src/App.js

【編輯推薦】

【責任編輯:

姜華

TEL:(010)68476606】

點讚 0

相關焦點

  • react-router v4 源碼分析
    2 什麼是 react-routerreact-router v4 是一個使用純 react 實現的路由解決方案,可以理解為是對 history 庫的 react 封裝,v4 與其之前的版本在設計方式和理念上有較大的差別,為重寫庫。
  • React SSR 同構入門與原理
    通過 <script src="/index.js"></script> 這段腳本加載了客戶端打包後的 React 代碼,這樣就實現了客戶端渲染,因此一個簡單同構項目就這樣實現了。
  • Android性能優化:手把手帶你全面實現內存優化
    最近有想換工作的同學們,可參考《5月技術崗位內推|RN開發招聘啦》,再往下看,一篇關於性能優化的好文章,很值得去學習。前言在 Android開發中,性能優化策略十分重要本文主要講解性能優化中的內存優化,希望你們會喜歡目錄
  • PHP7每次更新與發布,都注重性能的優化
    利用這個功能,能夠將框架,或者是類庫預加載到內存中,以進一步提高性能,在ZF框架的測試中,開啟opcache.preload後性能提升30%到50%。其實不難發現,過去十年網際網路江湖風雲變幻。但無論技術如何迭代,網際網路界都有一個亙古不變的追求——性能優化、高並發。
  • Flink優化器與源碼解析系列--算子Chain策略優化
    是通過兩種方法disableChaining和startNewChain來設置的,singleOutputOperator的兩種方法實現(其他算子也是如此)源碼如下:@PublicEvolvingpublic SingleOutputStreamOperator<T> disableChaining() { return setChainingStrategy
  • 與阿迪的boost相比,耐克的新科技react怎麼樣?
    於是在這種強烈的危機感之下,Nike公司在去年推出了一項全新的科技——react,對其的要求定位在了和Adidas的看家技術boost同一高度上。和boost一樣,react作為一項全新的緩震技術,它的一次亮相也同樣選在了籃球鞋上。而且之後為了和boost爭奪市場,react同樣被Nike運用到了其他的運動鞋之上。
  • SPA單頁面應用優化VUE性能優化
    Spa單頁面應用比如vue、react、angular都是屬於單頁面應用,那麼如何優化呢?咱們拿vue舉例。SPA單頁面應用優化VUE性能優化>單頁面應用離不開構建工具比如webpack、fis3、gulp等,目前應用最多的就是webpack,咱們就用webpack來做優化,webpack能做哪些優化呢?
  • React Native痛點解析之性能調優
    優化切換動畫卡頓的問題:1、使用API InteractionManager,它的作用就是可以使本來JS的一些操作在動畫完成之後執行,這樣就可確保動畫的流程性。當然這是在延遲執行為代價上來獲得幀數的提高。
  • 適合Vue用戶的React教程,你值得擁有
    對於React的props,我們不僅僅可以傳入普通的屬性,還可以傳入一個函數,這時候我們就可以在傳入的這個函數裡面返回JSX,從而就實現了具名插槽的功能。import React from 'react'export interface CardProps { title?
  • 如何在React應用中實現「使用GitHub登錄」
    我遇到一個場景,該場景需要使用Create-React-App在React 應用中實現「使用Github登錄」功能。雖然這聽起來很簡單,但在嘗試做這件事時,你可能會遇到一些麻煩。因此,本文的目的是提供一個指南,幫助你在你的應用程式中實現這樣的功能。讓我們現在就開始吧!
  • 官宣:ReactNative導航庫重大更新
    構建原生導航器新版中使用了[react-native-screens](kmagiera/react-native-screens)庫,構建了Android和ios系統原生的導航器組件,使用視覺效果和原生一樣其他的改進
  • Java 資料庫設計 性能優化 電商秒殺系統附帶源碼
    課程名稱: JAVA 資料庫設計 性能優化 電商秒殺系統附帶源碼課程簡介: Java 資料庫設計 性能優化 電商秒殺系統附帶源碼----------------------課程目錄----------------------------
  • 程式設計師福利:用 Kotlin 實現的一個 Dribbble 客戶端等源碼
    源碼來源:安卓巴士官網精選源碼使用 Kotlin 實現的一個 Dribbble 客戶端項目介紹:Kotlin:是的,完全用Kotlin寫的(約96%)。清潔架構:整個項目基於Bob叔叔的清潔架構方法。它繼承自ViewGroup 而不是FrameLayout或LinearLayout,提高了性能。Android仿照36Kr官方新聞項目課程源碼項目介紹:一個仿照36Kr官方,實時抓取36Kr官網數據的資訊類新聞客戶端。包括首頁新聞,詳情,發現,活動,實時數據抓取,側滑效果,第三方登錄以及分享,消息推送等相關功能客戶端。
  • 一對一視頻聊天交友APP源碼和約單app源碼 如何快速實現上線?
    功能強大的交友軟體,在當前的社交時代是極具實用價值的,其實很多社交軟體,都已經對個性化和互動性提出了嚴苛的要求,為的就是利用軟體獨特魅力對目標用戶群體產生強勁吸引力,所以說交友APP源碼的開發體系,還是要儘可能完善,功能設計也要確保思路的拓寬,軟體的運行狀態和功能體驗才會穩定。
  • 精通react/vue組件設計之配合React Portals實現一個(Drawer)組件
    通過組件的設計過程,大家會接觸到一個完成健壯的組件設計思路和方法,也能在實現組件的過程逐漸對react/vue的高級知識和技巧有更深的理解和掌握,並且在企業實際工作做遊刃有餘.作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少了工作中大量的冗餘代碼, 一切皆組件的思想深得人心.
  • Vue轉React不完全指北
    卸載三個方面Vue 提供的比較多,但是常用的: created/mounted/destroyedReact 新版廢棄了一些,常用的: componentDidMount/componentDidUpdate/componentWillUnmount,Hooks 更是沒有四、數據流Vue雙向綁定,單向數據流:vue2.x 通過 v-model 實現雙向綁定
  • Android 布局優化真的難,從入門到放棄
    前言Android的繪製優化其實可以分為兩個部分,即布局(UI)優化和卡頓優化,而布局優化的核心問題就是要解決因布局渲染性能不佳而導致應用卡頓的問題,所以它可以認為是卡頓優化的一個子集。本文主要包括以下內容:1.為什麼要進行布局優化及android繪製,布局加載原理。
  • react中關於hook介紹及其使用
    前言最近由於公司的項目開發,就學習了在react關於hook的使用,對其有個基本的認識以及如何在項目中去應用hook。原因:是因為react中的單向數據源,這樣的話,能夠保證你的數據源流向會更加的清楚,這也是react所區別於vue中雙向數據源綁定的一點hook中設置多個全局變量的方式在hook中,如果我們需要去設置多個類似於上面所說的count,那麼就需要多次使用useState