成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

我在大廠寫React學到了什么?性能優化篇

開發 項目管理
我工作中的技術棧主要是 React + TypeScript,這篇文章我想總結一下如何在項目中運用 React 的一些技巧去進行性能優化,或者更好的代碼組織。

[[349492]]

前言

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

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

神奇的 children

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

看這樣一段代碼:

  1. import React, { useContext, useState } from "react"
  2.  
  3. const ThemeContext = React.createContext(); 
  4.  
  5. export function ChildNonTheme() { 
  6.   console.log("不關心皮膚的子組件渲染了"); 
  7.   return <div>我不關心皮膚,皮膚改變的時候別讓我重新渲染!</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 這個不關心皮膚的子組件,在皮膚狀態更改的時候也進行無效的重新渲染。

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

來看下 createElement 的返回結構:

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

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

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

  1. import React, { useContext, useState } from "react"
  2.  
  3. const ThemeContext = React.createContext(); 
  4.  
  5. function ChildNonTheme() { 
  6.   console.log("不關心皮膚的子組件渲染了"); 
  7.   return <div>我不關心皮膚,皮膚改變的時候別讓我重新渲染!</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.   ); 

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

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

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

在改變皮膚之后,控制臺空空如也!優化達成。

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

神奇的 children - 在線調試地址

當然,這個優化也一樣可以用 React.memo 包裹子組件來做,不過相對的增加維護成本,根據場景權衡選擇吧。

Context 讀寫分離

想象一下,現在我們有一個全局日志記錄的需求,我們想通過 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>一個能發日志的組件1</p> 
  21.       <button onClick={() => addLog("logger1")}>發日志</button> 
  22.     </> 
  23.   ); 
  24.  
  25. function Logger2() { 
  26.   const { addLog } = useContext(LogContext); 
  27.   console.log('Logger2 render'
  28.   return ( 
  29.     <> 
  30.       <p>一個能發日志的組件2</p> 
  31.       <button onClick={() => addLog("logger2")}>發日志</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.   ); 

我們已經用上了上一章節的優化小技巧,單獨的把 LogProvider 封裝起來,并且把子組件提升到外層傳入。

先思考一下最佳的情況,Logger 組件只負責發出日志,它是不關心logs的變化的,在任何組件調用 addLog 去寫入日志的時候,理想的情況下應該只有 LogsPanel 這個組件發生重新渲染。

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

這肯定不是我們預期的,假設在現實場景的代碼中,能寫日志的組件可多著呢,每次一寫入就導致全局的組件都重新渲染?這當然是不能接受的,發生這個問題的本質原因官網 Context 的部分已經講得很清楚了:

當 LogProvider 中的 addLog 被子組件調用,導致 LogProvider重渲染之后,必然會導致傳遞給 Provider 的 value 發生改變,由于 value 包含了 logs 和 setLogs 屬性,所以兩者中任意一個發生變化,都會導致所有的訂閱了 LogProvider 的子組件重新渲染。

那么解決辦法是什么呢?其實就是讀寫分離,我們把 logs(讀)和 setLogs(寫)分別通過不同的 Provider 傳遞,這樣負責寫入的組件更改了 logs,其他的「寫組件」并不會重新渲染,只有真正關心 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 的引用不能發生變化,所以這里自然要用 useCallback 把 addLog 方法包裹起來,才能保證 LogProvider 重渲染的時候,傳遞給的LogDispatcherContext的value 不發生變化。

現在我從任意「寫組件」發送日志,都只會讓「讀組件」LogsPanel 渲染。

Context 讀寫分離 - 在線調試

Context 代碼組織

上面的案例中,我們在子組件中獲取全局狀態,都是直接裸用 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 

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

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

根據場景,靈活運用這些技巧,讓你的代碼更加健壯優雅~

組合 Providers

假設我們使用上面的辦法管理一些全局的小狀態,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.   ) 

有沒有辦法解決呢?當然有,我們參考 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.   ) 

總結

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

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

本文轉載自微信公眾號「 前端從進階到入院」,可以通過以下二維碼關注。轉載本文請聯系 前端從進階到入院公眾號。

 

責任編輯:武曉燕 來源: 前端從進階到入院
相關推薦

2022-03-27 09:06:04

React類型定義前端

2021-07-28 07:01:09

薅羊毛架構Vue+SSR

2021-03-09 09:55:02

Vuejs前端代碼

2016-01-18 10:06:05

編程

2023-10-16 08:55:43

Redisson分布式

2020-02-22 15:01:51

后端前端開發

2020-12-31 10:47:03

開發Vuejs技術

2012-07-12 00:22:03

創業產品

2023-04-10 07:40:36

GraphQLRest通信模式

2019-08-27 10:49:30

跳槽那些事兒技術Linux

2019-08-16 17:14:28

跳槽那些事兒技術Linux

2020-07-06 15:24:50

技術人工智能面試

2023-12-30 21:02:36

2022-07-19 08:04:04

HTTP應用層協議

2024-11-13 09:22:40

2023-06-03 00:05:18

TypeScriptJSDoc掃描器

2021-08-27 14:26:06

開發技能React

2017-02-05 17:53:12

2021-02-21 21:20:24

SpringBoot異步網絡

2015-07-20 10:02:57

Java團隊領導人
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品夜间视频香蕉 | 亚洲最大的黄色网址 | 日韩欧美在线观看 | 久久久久久久久久久爱 | 欧美福利专区 | 久久久九九 | 亚洲精品中文字幕在线 | 黄a免费看 | 国产精品一区二区三区四区五区 | 色精品| 亚洲成人高清 | 亚洲一区二区三区视频 | 久久久久久成人 | 在线免费观看日本视频 | 国产在视频一区二区三区吞精 | 紧缚调教一区二区三区视频 | 久久精品网 | 久久精品国产亚洲一区二区三区 | 蜜桃视频在线观看免费视频网站www | 国产一区二区在线免费观看 | 91p在线观看 | 91久久久久久 | 欧美福利一区 | 亚洲精品第一 | 中文日韩字幕 | 欧美日韩国产一区 | 国产日韩电影 | 精品国产乱码一区二区三区 | 色网站在线| 久久久国产一区 | 国产精品永久在线观看 | 亚洲有码转帖 | 亚洲成人精品影院 | 精品一区二区三区四区在线 | 中文字幕一区二区三区乱码在线 | 亚洲最新在线 | 国产精品久久久久久福利一牛影视 | 亚洲精品久久区二区三区蜜桃臀 | 毛片视频免费观看 | 日韩一区二区三区四区五区 | 日韩在线观看一区 |