學完這篇文章,你會收穫:react
瞭解Context
的實現原理web
源碼層面掌握React
組件的render
時機,從而寫出高性能的React
組件算法
源碼層面瞭解shouldComponentUpdate
、React.memo
、PureComponent
等性能優化手段的實現性能優化
我會盡可能將文章寫的通俗易懂。可是,要徹底理解文章內容,須要你掌握這些前置知識:markdown
Fiber
架構的大致工做流程架構
優先級
與更新
在React
源碼中的意義ide
若是你還不具有前置知識,能夠先閱讀React技術揭祕。函數
Context
的實現與組件的render
息息相關。在講解其實現前,咱們先來了解render
的時機。oop
換句話說,組件
在何時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:
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);
複製代碼
點擊Parent
組件的div
子組件,觸發更新,可是child render!
並不會打印。
這是由於Son
組件會進入bailout
邏輯。
要進入bailout
邏輯,需同時知足4個條件:
oldProps === newProps
即本次更新的props
全等於上次更新的props
。
注意這裏是全等比較。
咱們知道組件render
會返回JSX
,JSX
是React.createElement
的語法糖。
因此render
的返回結果其實是React.createElement
的執行結果,即一個包含props
屬性的對象。
即便本次更新與上次更新props
中每一項參數都沒有變化,可是本次更新是React.createElement
的執行結果,是一個全新的props
引用,因此oldProps !== newProps
。
context value
沒有變化咱們知道在當前React
版本中,同時存在新老兩種context
,這裏指老版本context
。
workInProgress.type === current.type
更新先後fiber.type
不變,好比div
沒變爲p
。
!includesSomeLane(renderLanes, updateLanes) ?
當前fiber
上是否存在更新
,若是存在那麼更新
的優先級
是否和本次整棵Fiber
樹調度的優先級
一致?
若是一致表明該組件上存在更新,須要走render
邏輯。
bailout
的優化還不止如此。若是一棵fiber
子樹全部節點都沒有更新,即便全部子孫fiber
都走bailout
邏輯,仍是有遍歷的成本。
因此,在bailout
中,會檢查該fiber
的全部子孫fiber
是否知足條件4(該檢查時間複雜度O(1)
)。
若是全部子孫fiber
本次都沒有更新須要執行,則bailout
會直接返回null
。整棵子樹都被跳過。
不會bailout
也不會render
,就像不存在同樣。對應的DOM不會產生任何變化。
如今咱們大致瞭解了render
的時機。有了這個概念,就能理解Context
API是如何實現的,以及爲何被重構。
咱們先看被廢棄的老Context
API的實現。
Fiber
樹的生成過程是經過遍歷實現的可中斷遞歸,因此分爲遞和歸2個階段。
Context
對應數據會保存在棧中。
在遞階段,Context
不斷入棧。因此Concumer
能夠經過Context棧
向上找到對應的context value
。
在歸階段,Context
不斷出棧。
那麼老Context
API爲何被廢棄呢?由於他無法和shouldComponentUpdate
或Memo
等性能優化手段配合。
要探究更深層的緣由,咱們須要瞭解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相等
回到老Context
API。
當這些性能優化手段:
使組件命中bailout
邏輯
同時若是組件的子樹都知足bailout
的條件4
那麼該fiber
子樹不會再繼續遍歷生成。
換言之,不會再經歷Context
的入棧、出棧。
這種狀況下,即便context value
變化,子孫組件也無法檢測到。
知道老Context
API的缺陷,咱們再來看新Context
API是如何實現的。
當經過:
ctx = React.createContext();
複製代碼
建立context
實例後,須要使用Provider
提供value
,使用Consumer
或useContext
訂閱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.Provider
中context value
變化,Ctx.Provider
向下找到消費context value
的組件Child
,爲其fiber
觸發一次更新。
則Child
對應fiber
就不知足條件4。
這就解決了老Context
API的問題:
因爲Child
對應fiber
不知足條件4,因此從Ctx.Provider
到Child
,這棵子樹無法知足:
子樹中全部子孫節點都知足條件4
因此即便遍歷中途有組件進入bailout
邏輯,也不會返回null
,即不會無視這棵子樹的遍歷。
最終遍歷進行到Child
,因爲其不知足條件4,會進入render
邏輯,調用組件對應函數。
const Child = () => {
const {num} = useContext(Ctx);
return <p>{num}</p>
}
複製代碼
在函數調用中會調用useContext
從Context
棧中找到對應更新後的context value
並返回。
React
性能一大關鍵在於:減小沒必要要的render
。
從上文咱們看到,本質就是讓組件知足4個條件,從而進入bailout
邏輯。
而Context
API本質是讓Consumer
組件不知足條件4。
咱們也知道了,React
雖然每次都會遍歷整棵樹,但會有bailout
的優化邏輯,不是全部組件都會render
。
極端狀況下,甚至某些子樹會被跳過遍歷(bailout
返回null
)。