這篇文章給大家分享的是有關(guān)從Context源碼實現(xiàn)談React性能優(yōu)化的示例分析的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
創(chuàng)新互聯(lián)公司專注于門頭溝網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗。 熱誠為您提供門頭溝營銷型網(wǎng)站建設(shè),門頭溝網(wǎng)站制作、門頭溝網(wǎng)頁設(shè)計、門頭溝網(wǎng)站官網(wǎng)定制、小程序開發(fā)服務(wù),打造門頭溝網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供門頭溝網(wǎng)站排名全網(wǎng)營銷落地服務(wù)。
組件render的時機(jī)
Context的實現(xiàn)與組件的render息息相關(guān)。在講解其實現(xiàn)前,我們先來了解render的時機(jī)。
換句話說,組件在什么時候render?
這個問題的答案,已經(jīng)在React組件到底什么時候render啊聊過。在這里再概括下:
在React中,每當(dāng)觸發(fā)更新(比如調(diào)用this.setState、useState),會為組件創(chuàng)建對應(yīng)的fiber節(jié)點。
fiber節(jié)點互相鏈接形成一棵Fiber樹。
有2種方式創(chuàng)建fiber節(jié)點:
bailout,即復(fù)用前一次更新該組件對應(yīng)的fiber節(jié)點作為本次更新的fiber節(jié)點。
render,經(jīng)過diff算法后生成一個新fiber節(jié)點。組件的render(比如ClassComponent的render方法調(diào)用、FunctionComponent的執(zhí)行)就發(fā)生在這一步。
經(jīng)常有同學(xué)問:React每次更新都會重新生成一棵Fiber樹,性能不會差么?
React性能確實不算很棒。但如你所見,F(xiàn)iber樹生成過程中并不是所有組件都會render,有些滿足優(yōu)化條件的組件會走bailout邏輯。
比如,對于如下Demo:
function Son() { console.log('child render!'); returnSon; } function Parent(props) { const [count, setCount] = React.useState(0); return ({setCount(count + 1)}}> count:{count} {props.children}); } function App() { return (); } const rootEl = document.querySelector("#root"); ReactDOM.render( , rootEl);
在線Demo地址[2]
點擊Parent組件的div子組件,觸發(fā)更新,但是child render!并不會打印。
這是因為Son組件會進(jìn)入bailout邏輯。
bailout的條件
要進(jìn)入bailout邏輯,需同時滿足4個條件:
1.oldProps === newProps
即本次更新的props全等于上次更新的props。
注意這里是全等比較。
我們知道組件render會返回JSX,JSX是React.createElement的語法糖。
所以render的返回結(jié)果實際上是React.createElement的執(zhí)行結(jié)果,即一個包含props屬性的對象。
即使本次更新與上次更新props中每一項參數(shù)都沒有變化,但是本次更新是React.createElement的執(zhí)行結(jié)果,是一個全新的props引用,所以oldProps !== newProps。
2.context value沒有變化
我們知道在當(dāng)前React版本中,同時存在新老兩種context,這里指老版本context。
3.workInProgress.type === current.type
更新前后fiber.type不變,比如div沒變?yōu)閜。
4.!includesSomeLane(renderLanes, updateLanes) ?
當(dāng)前fiber上是否存在更新,如果存在那么更新的優(yōu)先級是否和本次整棵Fiber樹調(diào)度的優(yōu)先級一致?
如果一致代表該組件上存在更新,需要走render邏輯。
bailout的優(yōu)化還不止如此。如果一棵fiber子樹所有節(jié)點都沒有更新,即使所有子孫fiber都走bailout邏輯,還是有遍歷的成本。
所以,在bailout中,會檢查該fiber的所有子孫fiber是否滿足條件4(該檢查時間復(fù)雜度O(1))。
如果所有子孫fiber本次都沒有更新需要執(zhí)行,則bailout會直接返回null。整棵子樹都被跳過。
不會bailout也不會render,就像不存在一樣。對應(yīng)的DOM不會產(chǎn)生任何變化。
老Context API的實現(xiàn)現(xiàn)
在我們大體了解了render的時機(jī)。有了這個概念,就能理解ContextAPI是如何實現(xiàn)的,以及為什么被重構(gòu)。
我們先看被廢棄的老ContextAPI的實現(xiàn)。
Fiber樹的生成過程是通過遍歷實現(xiàn)的可中斷遞歸,所以分為遞和歸2個階段。
Context對應(yīng)數(shù)據(jù)會保存在棧中。
在遞階段,Context不斷入棧。所以Concumer可以通過Context棧向上找到對應(yīng)的context value。
在歸階段,Context不斷出棧。
那么老ContextAPI為什么被廢棄呢?因為他沒法和shouldComponentUpdate或Memo等性能優(yōu)化手段配合。
shouldComponentUpdate的實現(xiàn)
要探究更深層的原因,我們需要了解shouldComponentUpdate的原理,后文簡稱其為SCU。
使用SCU是為了減少不必要的render,換句話說:讓本該render的組件走bailout邏輯。
剛才我們介紹了bailout需要滿足的條件。那么SCU是作用于這4個條件的哪個呢?
顯然是第一條:oldProps === newProps
當(dāng)使用shouldComponentUpdate,這個組件bailout的條件會產(chǎn)生變化:
-- oldProps === newProps
++ SCU === false
同理,使用PureComponenet和React.memo時,bailout的條件也會產(chǎn)生變化:
-- oldProps === newProps
++ 淺比較oldProps與newsProps相等
回到老ContextAPI。
當(dāng)這些性能優(yōu)化手段:
使組件命中bailout邏輯
同時如果組件的子樹都滿足bailout的條件4
那么該fiber子樹不會再繼續(xù)遍歷生成。
換言之,不會再經(jīng)歷Context的入棧、出棧。
這種情況下,即使context value變化,子孫組件也沒法檢測到。
新Context API的實現(xiàn)
知道老ContextAPI的缺陷,我們再來看新ContextAPI是如何實現(xiàn)的。
當(dāng)通過:
ctx = React.createContext();
創(chuàng)建context實例后,需要使用Provider提供value,使用Consumer或useContext訂閱value。
如:
ctx = React.createContext(); const NumProvider = ({children}) => { const [num, add] = useState(0); return ({children} ) }
使用:
const Child = () => { const {num} = useContext(Ctx); return{num}
}
當(dāng)遍歷組件生成對應(yīng)fiber時,遍歷到Ctx.Provider組件,Ctx.Provider內(nèi)部會判斷context value是否變化。
如果context value變化,Ctx.Provider內(nèi)部會執(zhí)行一次向下深度優(yōu)先遍歷子樹的操作,尋找與該Provider配套的Consumer。
在上文的例子中會最終找到useContext(Ctx)的Child組件對應(yīng)的fiber,并為該fiber觸發(fā)一次更新。
注意這里的實現(xiàn)非常巧妙:
一般更新是由組件調(diào)用觸發(fā)更新的方法產(chǎn)生。比如上文的NumProvider組件,點擊button調(diào)用add會觸發(fā)一次更新。
觸發(fā)更新的本質(zhì)是為了讓組件創(chuàng)建對應(yīng)fiber時不滿足bailout條件4:
!includesSomeLane(renderLanes, updateLanes) ?
從而進(jìn)入render邏輯。
在這里,Ctx.Provider中context value變化,Ctx.Provider向下找到消費context value的組件Child,為其fiber觸發(fā)一次更新。
則Child對應(yīng)fiber就不滿足條件4。
這就解決了老ContextAPI的問題:
由于Child對應(yīng)fiber不滿足條件4,所以從Ctx.Provider到Child,這棵子樹沒法滿足:
!! 子樹中所有子孫節(jié)點都滿足條件4
所以即使遍歷中途有組件進(jìn)入bailout邏輯,也不會返回null,即不會無視這棵子樹的遍歷。
最終遍歷進(jìn)行到Child,由于其不滿足條件4,會進(jìn)入render邏輯,調(diào)用組件對應(yīng)函數(shù)。
const Child = () => { const {num} = useContext(Ctx); return{num}
}
在函數(shù)調(diào)用中會調(diào)用useContext從Context棧中找到對應(yīng)更新后的context value并返回。
感謝各位的閱讀!關(guān)于“從Context源碼實現(xiàn)談React性能優(yōu)化的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!