<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>

          我在大廠寫React學(xué)到了什么?性能優(yōu)化篇

          共 5856字,需瀏覽 12分鐘

           ·

          2020-11-13 14:18

          前言

          我工作中的技術(shù)棧主要是 React + TypeScript,這篇文章我想總結(jié)一下如何在項目中運(yùn)用 React 的一些技巧去進(jìn)行性能優(yōu)化,或者更好的代碼組織。

          性能優(yōu)化的重要性不用多說,谷歌發(fā)布的很多調(diào)研精確的展示了性能對于網(wǎng)站留存率的影響,而代碼組織優(yōu)化則關(guān)系到后續(xù)的維護(hù)成本,以及你同事維護(hù)你代碼時候“口吐芬芳”的頻率?,本篇文章看完,你一定會有所收獲。

          神奇的 children

          我們有一個需求,需要通過 Provider 傳遞一些主題信息給子組件:

          看這樣一段代碼:

          import?React,?{?useContext,?useState?}?from?"react";

          const?ThemeContext?=?React.createContext();

          export?function?ChildNonTheme()?{
          ??console.log("不關(guān)心皮膚的子組件渲染了");
          ??return?<div>我不關(guān)心皮膚,皮膚改變的時候別讓我重新渲染!div>;
          }

          export?function?ChildWithTheme()?{
          ??const?theme?=?useContext(ThemeContext);
          ??return?<div>我是有皮膚的哦~?{theme}div>;
          }

          export?default?function?App()?{
          ??const?[theme,?setTheme]?=?useState("light");
          ??const?onChangeTheme?=?()?=>?setTheme(theme?===?"light"???"dark"?:?"light");
          ??return?(
          ????<ThemeContext.Provider?value={theme}>
          ??????<button?onClick={onChangeTheme}>改變皮膚button>

          ??????<ChildWithTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ????ThemeContext.Provider>
          ??);
          }

          這段代碼看起來沒啥問題,也很符合擼起袖子就干的直覺,但是卻會讓 ChildNonTheme 這個不關(guān)心皮膚的子組件,在皮膚狀態(tài)更改的時候也進(jìn)行無效的重新渲染。

          這本質(zhì)上是由于 React 是自上而下遞歸更新, 這樣的代碼會被 babel 翻譯成 React.createElement(ChildNonTheme) 這樣的函數(shù)調(diào)用,React官方經(jīng)常強(qiáng)調(diào) props 是immutable 的,所以在每次調(diào)用函數(shù)式組件的時候,都會生成一份新的 props 引用。

          來看下 createElement 的返回結(jié)構(gòu):

          const?childNonThemeElement?=?{
          ??type:?'ChildNonTheme',
          ??props:?{}?//?<-?這個引用更新了
          }

          正是由于這個新的 props 引用,導(dǎo)致 ChildNonTheme 這個組件也重新渲染了。

          那么如何避免這個無效的重新渲染呢?關(guān)鍵詞是「巧妙利用 children」。

          import?React,?{?useContext,?useState?}?from?"react";

          const?ThemeContext?=?React.createContext();

          function?ChildNonTheme()?{
          ??console.log("不關(guān)心皮膚的子組件渲染了");
          ??return?<div>我不關(guān)心皮膚,皮膚改變的時候別讓我重新渲染!div>;
          }

          function?ChildWithTheme()?{
          ??const?theme?=?useContext(ThemeContext);
          ??return?<div>我是有皮膚的哦~?{theme}div>;
          }

          function?ThemeApp({?children?})?{
          ??const?[theme,?setTheme]?=?useState("light");
          ??const?onChangeTheme?=?()?=>?setTheme(theme?===?"light"???"dark"?:?"light");
          ??return?(
          ????<ThemeContext.Provider?value={theme}>
          ??????<button?onClick={onChangeTheme}>改變皮膚button>

          ??????{children}
          ????ThemeContext.Provider>
          ??);
          }

          export?default?function?App()?{
          ??return?(
          ????<ThemeApp>
          ??????<ChildWithTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ??????<ChildNonTheme?/>
          ????ThemeApp>

          ??);
          }

          沒錯,唯一的區(qū)別就是我把控制狀態(tài)的組件和負(fù)責(zé)展示的子組件給抽離開了,通過 children 傳入后直接渲染,由于 children 從外部傳入的,也就是說 ThemeApp 這個組件內(nèi)部不會再有 React.createElement 這樣的代碼,那么在 setTheme 觸發(fā)重新渲染后,children 完全沒有改變,所以可以直接復(fù)用。

          讓我們再看一下被 ThemeApp 包裹下的 ,它會作為 children 傳遞給 ThemeAppThemeApp 內(nèi)部的更新完全不會觸發(fā)外部的 React.createElement,所以會直接復(fù)用之前的 element 結(jié)果:

          //?完全復(fù)用,props 也不會改變。
          const?childNonThemeElement?=?{
          ??type:?ChildNonTheme,
          ??props:?{}
          }

          在改變皮膚之后,控制臺空空如也!優(yōu)化達(dá)成。

          總結(jié)下來,就是要把渲染比較費(fèi)時,但是不需要關(guān)心狀態(tài)的子組件提升到「有狀態(tài)組件」的外部,作為 children 或者props傳遞進(jìn)去直接使用,防止被帶著一起渲染。

          神奇的 children - 在線調(diào)試地址

          當(dāng)然,這個優(yōu)化也一樣可以用 React.memo 包裹子組件來做,不過相對的增加維護(hù)成本,根據(jù)場景權(quán)衡選擇吧。

          Context 讀寫分離

          想象一下,現(xiàn)在我們有一個全局日志記錄的需求,我們想通過 Provider 去做,很快代碼就寫好了:

          import?React,?{?useContext,?useState?}?from?"react";
          import?"./styles.css";

          const?LogContext?=?React.createContext();

          function?LogProvider({?children?})?{
          ??const?[logs,?setLogs]?=?useState([]);
          ??const?addLog?=?(log)?=>?setLogs((prevLogs)?=>?[...prevLogs,?log]);
          ??return?(
          ????<LogContext.Provider?value={{?logs,?addLog?}}>
          ??????{children}
          ????LogContext.Provider>

          ??);
          }

          function?Logger1()?{
          ??const?{?addLog?}?=?useContext(LogContext);
          ??console.log('Logger1?render')
          ??return?(
          ????<>
          ??????<p>一個能發(fā)日志的組件1p>

          ??????<button?onClick={()?=>?addLog("logger1")}>發(fā)日志button>
          ????
          ??);
          }

          function?Logger2()?{
          ??const?{?addLog?}?=?useContext(LogContext);
          ??console.log('Logger2?render')
          ??return?(
          ????<>
          ??????<p>一個能發(fā)日志的組件2p>

          ??????<button?onClick={()?=>?addLog("logger2")}>發(fā)日志button>
          ????
          ??);
          }

          function?LogsPanel()?{
          ??const?{?logs?}?=?useContext(LogContext);
          ??return?logs.map((log,?index)?=>?<p?key={index}>{log}p>);
          }

          export?default?function?App()?{
          ??return?(
          ????<LogProvider>
          ??????{/*?寫日志?*/}
          ??????<Logger1?/>
          ??????<Logger2?/>
          ??????{/*?讀日志?*/}
          ??????<LogsPanel?/>
          ??????div>

          ????</LogProvider>
          ??);
          }

          我們已經(jīng)用上了上一章節(jié)的優(yōu)化小技巧,單獨(dú)的把 LogProvider 封裝起來,并且把子組件提升到外層傳入。

          先思考一下最佳的情況,Logger 組件只負(fù)責(zé)發(fā)出日志,它是不關(guān)心logs的變化的,在任何組件調(diào)用 addLog 去寫入日志的時候,理想的情況下應(yīng)該只有 LogsPanel 這個組件發(fā)生重新渲染。

          但是這樣的代碼寫法卻會導(dǎo)致每次任意一個組件寫入日志以后,所有的 LoggerLogsPanel 都發(fā)生重新渲染。

          這肯定不是我們預(yù)期的,假設(shè)在現(xiàn)實(shí)場景的代碼中,能寫日志的組件可多著呢,每次一寫入就導(dǎo)致全局的組件都重新渲染?這當(dāng)然是不能接受的,發(fā)生這個問題的本質(zhì)原因官網(wǎng) Context 的部分已經(jīng)講得很清楚了:

          當(dāng) LogProvider 中的 addLog 被子組件調(diào)用,導(dǎo)致 LogProvider重渲染之后,必然會導(dǎo)致傳遞給 Provider 的 value 發(fā)生改變,由于 value 包含了 logssetLogs 屬性,所以兩者中任意一個發(fā)生變化,都會導(dǎo)致所有的訂閱了 LogProvider 的子組件重新渲染。

          那么解決辦法是什么呢?其實(shí)就是讀寫分離,我們把 logs(讀)和 setLogs(寫)分別通過不同的 Provider 傳遞,這樣負(fù)責(zé)寫入的組件更改了 logs,其他的「寫組件」并不會重新渲染,只有真正關(guān)心 logs 的「讀組件」會重新渲染。

          function?LogProvider({?children?})?{
          ??const?[logs,?setLogs]?=?useState([]);
          ??const?addLog?=?useCallback((log)?=>?{
          ????setLogs((prevLogs)?=>?[...prevLogs,?log]);
          ??},?[]);
          ??return?(
          ????<LogDispatcherContext.Provider?value={addLog}>
          ??????<LogStateContext.Provider?value={logs}>
          ????????{children}
          ??????LogStateContext.Provider>

          ????LogDispatcherContext.Provider>
          ??);
          }

          我們剛剛也提到,需要保證 value 的引用不能發(fā)生變化,所以這里自然要用 useCallbackaddLog 方法包裹起來,才能保證 LogProvider 重渲染的時候,傳遞給的LogDispatcherContext的value 不發(fā)生變化。

          現(xiàn)在我從任意「寫組件」發(fā)送日志,都只會讓「讀組件」LogsPanel 渲染。

          Context 讀寫分離 - 在線調(diào)試

          Context 代碼組織

          上面的案例中,我們在子組件中獲取全局狀態(tài),都是直接裸用 useContext

          import?React?from?'react'
          import?{?LogStateContext?}?from?'./context'

          function?App()?{
          ??const?logs?=?React.useContext(LogStateContext)
          }

          但是是否有更好的代碼組織方法呢?比如這樣:

          import?React?from?'react'
          import?{?useLogState?}?from?'./context'

          function?App()?{
          ??const?logs?=?useLogState()
          }
          //?context
          import?React?from?'react'

          const?LogStateContext?=?React.createContext();

          export?function?useLogState()?{
          ??return?React.useContext(LogStateContext)
          }

          在加上點(diǎn)健壯性保證?

          import?React?from?'react'

          const?LogStateContext?=?React.createContext();
          const?LogDispatcherContext?=?React.createContext();

          export?function?useLogState()?{
          ??const?context?=?React.useContext(LogStateContext)
          ??if?(context?===?undefined)?{
          ????throw?new?Error('useLogState?must?be?used?within?a?LogStateProvider')
          ??}
          ??return?context
          }

          export?function?useLogDispatcher()?{
          ??const?context?=?React.useContext(LogDispatcherContext)
          ??if?(context?===?undefined)?{
          ????throw?new?Error('useLogDispatcher?must?be?used?within?a?LogDispatcherContext')
          ??}
          ??return?context
          }

          如果有的組件同時需要讀寫日志,調(diào)用兩次很麻煩?

          export?function?useLogs()?{
          ??return?[useLogState(),?useLogDispatcher()]
          }
          export?function?App()?{
          ??const?[logs,?addLogs]?=?useLogs()
          ??//?...
          }

          根據(jù)場景,靈活運(yùn)用這些技巧,讓你的代碼更加健壯優(yōu)雅~

          組合 Providers

          假設(shè)我們使用上面的辦法管理一些全局的小狀態(tài),Provider 變的越來越多了,有時候會遇到嵌套地獄的情況:

          const?StateProviders?=?({?children?})?=>?(
          ??<LogProvider>
          ????<UserProvider>
          ??????<MenuProvider>
          ????????<AppProvider>
          ??????????{children}
          ????????AppProvider>

          ??????MenuProvider>
          ????UserProvider>
          ??LogProvider>
          )

          function?App()?{
          ??return?(
          ????<StateProviders>
          ??????<Main?/>
          ????StateProviders>

          ??)
          }

          有沒有辦法解決呢?當(dāng)然有,我們參考 redux 中的 compose 方法,自己寫一個 composeProvider 方法:

          function?composeProviders(...providers)?{
          ??return?({?children?})?=>
          ????providers.reduce(
          ??????(prev,?Provider)?=>?<Provider>{prev}Provider>,
          ??????children,
          ????)
          }

          代碼就可以簡化成這樣:

          const?StateProviders?=?composeProviders(
          ??LogProvider,
          ??UserProvider,
          ??MenuProvider,
          ??AppProvider,
          )

          function?App()?{
          ??return?(
          ????<StateProvider>
          ??????<Main?/>
          ????Provider>

          ??)
          }

          總結(jié)

          本篇文章主要圍繞這 Context 這個 API,講了幾個性能優(yōu)化和代碼組織的優(yōu)化點(diǎn),總結(jié)下來就是:

          1. 盡量提升渲染無關(guān)的子組件元素到「有狀態(tài)組件」的外部。
          2. 在需要的情況下對 Context 進(jìn)行讀寫分離。
          3. 包裝Context 的使用,注意錯誤處理。
          4. 組合多個 Context,優(yōu)化代碼。



          推薦閱讀




          我的公眾號能帶來什么價值?(文末有送書規(guī)則,一定要看)

          每個前端工程師都應(yīng)該了解的圖片知識(長文建議收藏)

          為什么現(xiàn)在面試總是面試造火箭?

          瀏覽 64
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  国产精华7777777 | 国产成人无码区免费视频 | 超碰人人摸人人草 | 国产插逼区| 夜夜撸日日 |