自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

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

[[349492]]

前言

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

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

神奇的 children

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

看這樣一段代碼:

  1. import React, { useContext, useState } from "react"
  2.  
  3. const ThemeContext = React.createContext(); 
  4.  
  5. export function ChildNonTheme() { 
  6.   console.log("不關(guān)心皮膚的子組件渲染了"); 
  7.   return <div>我不關(guān)心皮膚,皮膚改變的時候別讓我重新渲染!</div>; 
  8.  
  9. export function ChildWithTheme() { 
  10.   const theme = useContext(ThemeContext); 
  11.   return <div>我是有皮膚的哦~ {theme}</div>; 
  12.  
  13. export default function App() { 
  14.   const [theme, setTheme] = useState("light"); 
  15.   const onChangeTheme = () => setTheme(theme === "light" ? "dark" : "light"); 
  16.   return ( 
  17.     <ThemeContext.Provider value={theme}> 
  18.       <button onClick={onChangeTheme}>改變皮膚</button> 
  19.       <ChildWithTheme /> 
  20.       <ChildNonTheme /> 
  21.       <ChildNonTheme /> 
  22.       <ChildNonTheme /> 
  23.       <ChildNonTheme /> 
  24.       <ChildNonTheme /> 
  25.       <ChildNonTheme /> 
  26.       <ChildNonTheme /> 
  27.     </ThemeContext.Provider> 
  28.   ); 

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

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

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

  1. const childNonThemeElement = { 
  2.   type: 'ChildNonTheme'
  3.   props: {} // <- 這個引用更新了 

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

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

  1. import React, { useContext, useState } from "react"
  2.  
  3. const ThemeContext = React.createContext(); 
  4.  
  5. function ChildNonTheme() { 
  6.   console.log("不關(guān)心皮膚的子組件渲染了"); 
  7.   return <div>我不關(guān)心皮膚,皮膚改變的時候別讓我重新渲染!</div>; 
  8.  
  9. function ChildWithTheme() { 
  10.   const theme = useContext(ThemeContext); 
  11.   return <div>我是有皮膚的哦~ {theme}</div>; 
  12.  
  13. function ThemeApp({ children }) { 
  14.   const [theme, setTheme] = useState("light"); 
  15.   const onChangeTheme = () => setTheme(theme === "light" ? "dark" : "light"); 
  16.   return ( 
  17.     <ThemeContext.Provider value={theme}> 
  18.       <button onClick={onChangeTheme}>改變皮膚</button> 
  19.       {children} 
  20.     </ThemeContext.Provider> 
  21.   ); 
  22.  
  23. export default function App() { 
  24.   return ( 
  25.     <ThemeApp> 
  26.       <ChildWithTheme /> 
  27.       <ChildNonTheme /> 
  28.       <ChildNonTheme /> 
  29.       <ChildNonTheme /> 
  30.       <ChildNonTheme /> 
  31.       <ChildNonTheme /> 
  32.       <ChildNonTheme /> 
  33.       <ChildNonTheme /> 
  34.     </ThemeApp> 
  35.   ); 

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

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

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

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

總結(jié)下來,就是要把渲染比較費時,但是不需要關(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 去做,很快代碼就寫好了:

  1. import React, { useContext, useState } from "react"
  2. import "./styles.css"
  3.  
  4. const LogContext = React.createContext(); 
  5.  
  6. function LogProvider({ children }) { 
  7.   const [logs, setLogs] = useState([]); 
  8.   const addLog = (log) => setLogs((prevLogs) => [...prevLogs, log]); 
  9.   return ( 
  10.     <LogContext.Provider value={{ logs, addLog }}> 
  11.       {children} 
  12.     </LogContext.Provider> 
  13.   ); 
  14.  
  15. function Logger1() { 
  16.   const { addLog } = useContext(LogContext); 
  17.   console.log('Logger1 render'
  18.   return ( 
  19.     <> 
  20.       <p>一個能發(fā)日志的組件1</p> 
  21.       <button onClick={() => addLog("logger1")}>發(fā)日志</button> 
  22.     </> 
  23.   ); 
  24.  
  25. function Logger2() { 
  26.   const { addLog } = useContext(LogContext); 
  27.   console.log('Logger2 render'
  28.   return ( 
  29.     <> 
  30.       <p>一個能發(fā)日志的組件2</p> 
  31.       <button onClick={() => addLog("logger2")}>發(fā)日志</button> 
  32.     </> 
  33.   ); 
  34.  
  35. function LogsPanel() { 
  36.   const { logs } = useContext(LogContext); 
  37.   return logs.map((log, index) => <p key={index}>{log}</p>); 
  38.  
  39. export default function App() { 
  40.   return ( 
  41.     <LogProvider> 
  42.       {/* 寫日志 */} 
  43.       <Logger1 /> 
  44.       <Logger2 /> 
  45.       {/* 讀日志 */} 
  46.       <LogsPanel /> 
  47.       </div> 
  48.     </LogProvider> 
  49.   ); 

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

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

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

這肯定不是我們預(yù)期的,假設(shè)在現(xiàn)實場景的代碼中,能寫日志的組件可多著呢,每次一寫入就導(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 包含了 logs 和 setLogs 屬性,所以兩者中任意一個發(fā)生變化,都會導(dǎo)致所有的訂閱了 LogProvider 的子組件重新渲染。

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

  1. function LogProvider({ children }) { 
  2.   const [logs, setLogs] = useState([]); 
  3.   const addLog = useCallback((log) => { 
  4.     setLogs((prevLogs) => [...prevLogs, log]); 
  5.   }, []); 
  6.   return ( 
  7.     <LogDispatcherContext.Provider value={addLog}> 
  8.       <LogStateContext.Provider value={logs}> 
  9.         {children} 
  10.       </LogStateContext.Provider> 
  11.     </LogDispatcherContext.Provider> 
  12.   ); 

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

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

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

Context 代碼組織

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

  1. import React from 'react' 
  2. import { LogStateContext } from './context' 
  3.  
  4. function App() { 
  5.   const logs = React.useContext(LogStateContext) 

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

  1. import React from 'react' 
  2. import { useLogState } from './context' 
  3.  
  4. function App() { 
  5.   const logs = useLogState() 
  6. // context 
  7. import React from 'react' 
  8.  
  9. const LogStateContext = React.createContext(); 
  10.  
  11. export function useLogState() { 
  12.   return React.useContext(LogStateContext) 

在加上點健壯性保證?

  1. import React from 'react' 
  2.  
  3. const LogStateContext = React.createContext(); 
  4. const LogDispatcherContext = React.createContext(); 
  5.  
  6. export function useLogState() { 
  7.   const context = React.useContext(LogStateContext) 
  8.   if (context === undefined) { 
  9.     throw new Error('useLogState must be used within a LogStateProvider'
  10.   } 
  11.   return context 
  12.  
  13. export function useLogDispatcher() { 
  14.   const context = React.useContext(LogDispatcherContext) 
  15.   if (context === undefined) { 
  16.     throw new Error('useLogDispatcher must be used within a LogDispatcherContext'
  17.   } 
  18.   return context 

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

  1. export function useLogs() { 
  2.   return [useLogState(), useLogDispatcher()] 
  3. export function App() { 
  4.   const [logs, addLogs] = useLogs() 
  5.   // ... 

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

組合 Providers

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

  1. const StateProviders = ({ children }) => ( 
  2.   <LogProvider> 
  3.     <UserProvider> 
  4.       <MenuProvider> 
  5.         <AppProvider> 
  6.           {children} 
  7.         </AppProvider> 
  8.       </MenuProvider> 
  9.     </UserProvider> 
  10.   </LogProvider> 
  11.  
  12. function App() { 
  13.   return ( 
  14.     <StateProviders> 
  15.       <Main /> 
  16.     </StateProviders> 
  17.   ) 

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

  1. function composeProviders(...providers) { 
  2.   return ({ children }) => 
  3.     providers.reduce( 
  4.       (prev, Provider) => <Provider>{prev}</Provider>, 
  5.       children, 
  6.     ) 

代碼就可以簡化成這樣:

  1. const StateProviders = composeProviders( 
  2.   LogProvider, 
  3.   UserProvider, 
  4.   MenuProvider, 
  5.   AppProvider, 
  6.  
  7. function App() { 
  8.   return ( 
  9.     <StateProvider> 
  10.       <Main /> 
  11.     </StateProvider> 
  12.   ) 

總結(jié)

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

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

本文轉(zhuǎn)載自微信公眾號「 前端從進(jìn)階到入院」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 前端從進(jìn)階到入院公眾號。

 

責(zé)任編輯:武曉燕 來源: 前端從進(jìn)階到入院
相關(guān)推薦

2022-03-27 09:06:04

React類型定義前端

2021-07-28 07:01:09

薅羊毛架構(gòu)Vue+SSR

2021-03-09 09:55:02

Vuejs前端代碼

2016-01-18 10:06:05

編程

2020-02-22 15:01:51

后端前端開發(fā)

2023-10-16 08:55:43

Redisson分布式

2020-12-31 10:47:03

開發(fā)Vuejs技術(shù)

2012-07-12 00:22:03

創(chuàng)業(yè)產(chǎn)品

2019-08-27 10:49:30

跳槽那些事兒技術(shù)Linux

2023-04-10 07:40:36

GraphQLRest通信模式

2019-08-16 17:14:28

跳槽那些事兒技術(shù)Linux

2020-07-06 15:24:50

技術(shù)人工智能面試

2023-12-30 21:02:36

2022-07-19 08:04:04

HTTP應(yīng)用層協(xié)議

2024-11-13 09:22:40

2023-06-03 00:05:18

TypeScriptJSDoc掃描器

2021-08-27 14:26:06

開發(fā)技能React

2017-02-05 17:53:12

2021-02-21 21:20:24

SpringBoot異步網(wǎng)絡(luò)

2015-07-20 10:02:57

Java團(tuán)隊領(lǐng)導(dǎo)人
點贊
收藏

51CTO技術(shù)棧公眾號