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

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

  1. 瞭解Context的實現原理web

  2. 源碼層面掌握React組件的render時機,從而寫出高性能的React組件算法

  3. 源碼層面瞭解shouldComponentUpdateReact.memoPureComponent等性能優化手段的實現性能優化

我會盡可能將文章寫的通俗易懂。可是,要徹底理解文章內容,須要你掌握這些前置知識:markdown

  1. Fiber架構的大致工做流程架構

  2. 優先級更新React源碼中的意義ide

若是你還不具有前置知識,能夠先閱讀React技術揭祕函數

組件render的時機

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

換句話說,組件在何時render性能

這個問題的答案,已經在React組件到底何時render啊 聊過。在這裏再歸納下:

React中,每當觸發更新(好比調用this.setStateuseState),會爲組件建立對應的fiber節點。

fiber節點互相連接造成一棵Fiber樹。

有2種方式建立fiber節點:

  1. bailout,即複用前一次更新該組件對應的fiber節點做爲本次更新的fiber節點。

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

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

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

好比,對於以下Demo:

function Son() {
  console.log('child render!');
  return <div>Son</div>;
}


function Parent(props) {
  const [count, setCount] = React.useState(0);

  return (
    <div onClick={() => {setCount(count + 1)}}> count:{count} {props.children} </div>
  );
}


function App() {
  return (
    <Parent> <Son/> </Parent>
  );
}

const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);
複製代碼

在線Demo地址

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

這是由於Son組件會進入bailout邏輯。

bailout的條件

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

  1. oldProps === newProps

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

注意這裏是全等比較

咱們知道組件render會返回JSXJSXReact.createElement的語法糖。

因此render的返回結果其實是React.createElement的執行結果,即一個包含props屬性的對象。

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

  1. context value沒有變化

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

  1. workInProgress.type === current.type

更新先後fiber.type不變,好比div沒變爲p

  1. !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爲何被廢棄呢?由於他無法和shouldComponentUpdateMemo等性能優化手段配合。

shouldComponentUpdate的實現

要探究更深層的緣由,咱們須要瞭解shouldComponentUpdate的原理,後文簡稱其爲SCU

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

剛纔咱們介紹了bailout須要知足的條件。那麼SCU是做用於這4個條件的哪一個呢?

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

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

-- oldProps === newProps

++ SCU === false

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

-- oldProps === newProps

++ 淺比較oldProps與newsProps相等

回到老ContextAPI。

當這些性能優化手段:

  • 使組件命中bailout邏輯

  • 同時若是組件的子樹都知足bailout的條件4

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

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

這種狀況下,即便context value變化,子孫組件也無法檢測到。

新Context API的實現

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

當經過:

ctx = React.createContext();
複製代碼

建立context實例後,須要使用Provider提供value,使用ConsumeruseContext訂閱value

如:

ctx = React.createContext();

const NumProvider = ({children}) => {
  const [num, add] = useState(0);

  return (
    <Ctx.Provider value={num}> <button onClick={() => add(num + 1)}>add</button> {children} </Ctx.Provider>
  )
}
複製代碼

使用:

const Child = () => {
  const {num} = useContext(Ctx);
  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.Providercontext value變化,Ctx.Provider向下找到消費context value的組件Child,爲其fiber觸發一次更新。

Child對應fiber就不知足條件4。

這就解決了老ContextAPI的問題:

因爲Child對應fiber不知足條件4,因此從Ctx.ProviderChild,這棵子樹無法知足:

子樹中全部子孫節點都知足條件4

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

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

const Child = () => {
  const {num} = useContext(Ctx);
  return <p>{num}</p>
}
複製代碼

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

總結

React性能一大關鍵在於:減小沒必要要的render

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

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

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

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

相關文章
相關標籤/搜索