<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從Context源碼實(shí)現(xiàn)談React性能優(yōu)化

          共 5994字,需瀏覽 12分鐘

           ·

          2021-02-24 18:58

          學(xué)完這篇文章,你會(huì)收獲:

          1. 了解Context的實(shí)現(xiàn)原理

          2. 源碼層面掌握React組件的render時(shí)機(jī),從而寫出高性能的React組件

          3. 源碼層面了解shouldComponentUpdateReact.memo、PureComponent等性能優(yōu)化手段的實(shí)現(xiàn)

          我會(huì)盡量將文章寫的通俗易懂。但是,要完全理解文章內(nèi)容,需要你掌握這些前置知識(shí):

          1. Fiber架構(gòu)的大體工作流程

          2. 優(yōu)先級(jí)更新React源碼中的意義

          如果你還不具備前置知識(shí),可以先閱讀React技術(shù)揭秘[1](點(diǎn)擊閱讀原文)

          組件render的時(shí)機(jī)

          Context的實(shí)現(xiàn)與組件的render息息相關(guān)。在講解其實(shí)現(xiàn)前,我們先來了解render的時(shí)機(jī)。

          換句話說,組件在什么時(shí)候render

          這個(gè)問題的答案,已經(jīng)在React組件到底什么時(shí)候render啊聊過。在這里再概括下:

          React中,每當(dāng)觸發(fā)更新(比如調(diào)用this.setState、useState),會(huì)為組件創(chuàng)建對(duì)應(yīng)的fiber節(jié)點(diǎn)。

          fiber節(jié)點(diǎn)互相鏈接形成一棵Fiber樹。

          有2種方式創(chuàng)建fiber節(jié)點(diǎn):

          1. bailout,即復(fù)用前一次更新該組件對(duì)應(yīng)的fiber節(jié)點(diǎn)作為本次更新的fiber節(jié)點(diǎn)。

          2. render,經(jīng)過diff算法后生成一個(gè)新fiber節(jié)點(diǎn)。組件的render(比如ClassComponentrender方法調(diào)用、FunctionComponent的執(zhí)行)就發(fā)生在這一步。

          經(jīng)常有同學(xué)問:React每次更新都會(huì)重新生成一棵Fiber樹,性能不會(huì)差么?

          React性能確實(shí)不算很棒。但如你所見,Fiber樹生成過程中并不是所有組件都會(huì)render,有些滿足優(yōu)化條件的組件會(huì)走bailout邏輯。

          比如,對(duì)于如下Demo:

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


          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地址[2]

          點(diǎn)擊Parent組件的div子組件,觸發(fā)更新,但是child render!并不會(huì)打印。

          這是因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: " operator mono, consolas, monaco, menlo, monospace;word-break: break-all;background: rgba(14, 210, 247, 0.15);>Son組件會(huì)進(jìn)入bailout邏輯。

          bailout的條件

          要進(jìn)入bailout邏輯,需同時(shí)滿足4個(gè)條件:

          1. oldProps === newProps

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

          注意這里是全等比較。

          我們知道組件render會(huì)返回JSX,JSXReact.createElement的語法糖。

          所以render的返回結(jié)果實(shí)際上是React.createElement的執(zhí)行結(jié)果,即一個(gè)包含props屬性的對(duì)象。

          即使本次更新與上次更新props中每一項(xiàng)參數(shù)都沒有變化,但是本次更新是React.createElement的執(zhí)行結(jié)果,是一個(gè)全新的props引用,所以oldProps !== newProps。

          1. context value沒有變化

          我們知道在當(dāng)前React版本中,同時(shí)存在新老兩種context,這里指老版本context。

          1. workInProgress.type === current.type

          更新前后fiber.type不變,比如div沒變?yōu)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: " operator mono, consolas, monaco, menlo, monospace;word-break: break-all;background: rgba(14, 210, 247, 0.15);>p。

          1. !includesSomeLane(renderLanes, updateLanes) ?

          當(dāng)前fiber上是否存在更新,如果存在那么更新優(yōu)先級(jí)是否和本次整棵Fiber樹調(diào)度的優(yōu)先級(jí)一致?

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

          bailout的優(yōu)化還不止如此。如果一棵fiber子樹所有節(jié)點(diǎn)都沒有更新,即使所有子孫fiber都走bailout邏輯,還是有遍歷的成本。

          所以,在bailout中,會(huì)檢查該fiber的所有子孫fiber是否滿足條件4(該檢查時(shí)間復(fù)雜度O(1))。

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

          不會(huì)bailout也不會(huì)render,就像不存在一樣。對(duì)應(yīng)的DOM不會(huì)產(chǎn)生任何變化。

          老Context API的實(shí)現(xiàn)

          現(xiàn)在我們大體了解了render的時(shí)機(jī)。有了這個(gè)概念,就能理解ContextAPI是如何實(shí)現(xiàn)的,以及為什么被重構(gòu)。

          我們先看被廢棄的老ContextAPI的實(shí)現(xiàn)。

          Fiber樹的生成過程是通過遍歷實(shí)現(xiàn)的可中斷遞歸,所以分為2個(gè)階段。

          Context對(duì)應(yīng)數(shù)據(jù)會(huì)保存在棧中。

          階段,Context不斷入棧。所以Concumer可以通過Context棧向上找到對(duì)應(yīng)的context value

          階段,Context不斷出棧。

          那么老ContextAPI為什么被廢棄呢?因?yàn)樗麤]法和shouldComponentUpdateMemo等性能優(yōu)化手段配合。

          shouldComponentUpdate的實(shí)現(xiàn)

          要探究更深層的原因,我們需要了解shouldComponentUpdate的原理,后文簡(jiǎn)稱其為SCU。

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

          剛才我們介紹了bailout需要滿足的條件。那么SCU是作用于這4個(gè)條件的哪個(gè)呢?

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

          當(dāng)使用shouldComponentUpdate,這個(gè)組件bailout的條件會(huì)產(chǎn)生變化:

          -- oldProps === newProps

          ++ SCU === false

          同理,使用PureComponenetReact.memo時(shí),bailout的條件也會(huì)產(chǎn)生變化:

          -- oldProps === newProps

          ++ 淺比較oldProps與newsProps相等

          回到老ContextAPI。

          當(dāng)這些性能優(yōu)化手段:

          • 使組件命中bailout邏輯

          • 同時(shí)如果組件的子樹都滿足bailout的條件4

          那么該fiber子樹不會(huì)再繼續(xù)遍歷生成。

          換言之,不會(huì)再經(jīng)歷Context的入棧、出棧。

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

          新Context API的實(shí)現(xiàn)

          知道老ContextAPI的缺陷,我們?cè)賮砜葱?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: " operator mono, consolas, monaco, menlo, monospace;word-break: break-all;background: rgba(14, 210, 247, 0.15);>ContextAPI是如何實(shí)現(xiàn)的。

          當(dāng)通過:

          ctx = React.createContext();

          創(chuàng)建context實(shí)例后,需要使用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)}>addbutton>

                {children}
              Ctx.Provider>
            )
          }

          使用:

          const Child = () => {
            const {num} = useContext(Ctx);
            return <p>{num}p>
          }

          當(dāng)遍歷組件生成對(duì)應(yīng)fiber時(shí),遍歷到Ctx.Provider組件,Ctx.Provider內(nèi)部會(huì)判斷context value是否變化。

          如果context value變化,Ctx.Provider內(nèi)部會(huì)執(zhí)行一次向下深度優(yōu)先遍歷子樹的操作,尋找與該Provider配套的Consumer。

          在上文的例子中會(huì)最終找到useContext(Ctx)Child組件對(duì)應(yīng)的fiber并為該fiber觸發(fā)一次更新。

          注意這里的實(shí)現(xiàn)非常巧妙:

          一般更新是由組件調(diào)用觸發(fā)更新的方法產(chǎn)生。比如上文的NumProvider組件,點(diǎn)擊button調(diào)用add會(huì)觸發(fā)一次更新。

          觸發(fā)更新的本質(zhì)是為了讓組件創(chuàng)建對(duì)應(yīng)fiber時(shí)不滿足bailout條件4:

          !includesSomeLane(renderLanes, updateLanes) ?

          從而進(jìn)入render邏輯。

          在這里,Ctx.Providercontext value變化,Ctx.Provider向下找到消費(fèi)context value的組件Child,為其fiber觸發(fā)一次更新。

          Child對(duì)應(yīng)fiber就不滿足條件4。

          這就解決了老ContextAPI的問題:

          由于Child對(duì)應(yīng)fiber不滿足條件4,所以從Ctx.ProviderChild,這棵子樹沒法滿足:

          !! 子樹中所有子孫節(jié)點(diǎn)都滿足條件4

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

          最終遍歷進(jìn)行到Child,由于其不滿足條件4,會(huì)進(jìn)入render邏輯,調(diào)用組件對(duì)應(yīng)函數(shù)。

          const Child = () => {
            const {num} = useContext(Ctx);
            return <p>{num}p>
          }

          在函數(shù)調(diào)用中會(huì)調(diào)用useContextContext棧中找到對(duì)應(yīng)更新后的context value并返回。

          總結(jié)

          React性能一大關(guān)鍵在于:減少不必要的render。

          從上文我們看到,本質(zhì)就是讓組件滿足4個(gè)條件,從而進(jìn)入bailout邏輯。

          ContextAPI本質(zhì)是讓Consumer組件不滿足條件4。

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

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

          參考資料

          [1]

          React技術(shù)揭秘: http://react.iamkasong.com/

          [2]

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





          如果你喜歡探討技術(shù),或者對(duì)本文有任何的意見或建議,非常歡迎加魚頭微信好友一起探討,當(dāng)然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。魚頭的微信號(hào)是:krisChans95 也可以掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。



          瀏覽 85
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品在线99热 | a免费看AA | av一三五区 | 伊人五月综合 | 青青伊人国产 |