日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
我在大廠寫React學(xué)到了什么?性能優(yōu)化篇

前言

成都創(chuàng)新互聯(lián)公司公司2013年成立,先為梨樹等服務(wù)建站,梨樹等地企業(yè),進行企業(yè)商務(wù)咨詢服務(wù)。為梨樹企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。

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

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

神奇的 children

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

看這樣一段代碼:

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

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

這本質(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: {} // <- 這個引用更新了
  4. }

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

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

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

沒錯,唯一的區(qū)別就是我把控制狀態(tài)的組件和負責展示的子組件給抽離開了,通過 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: {}
  5. }

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

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

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

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

Context 讀寫分離

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

 
 
 
 
  1. import React, { useContext, useState } from "react";
  2. import "./styles.css";
  3. const LogContext = React.createContext();
  4. function LogProvider({ children }) {
  5.   const [logs, setLogs] = useState([]);
  6.   const addLog = (log) => setLogs((prevLogs) => [...prevLogs, log]);
  7.   return (
  8.     
  9.       {children}
  10.     
  11.   );
  12. }
  13. function Logger1() {
  14.   const { addLog } = useContext(LogContext);
  15.   console.log('Logger1 render')
  16.   return (
  17.     <>
  18.       

    一個能發(fā)日志的組件1

  19.        addLog("logger1")}>發(fā)日志
  20.     
  21.   );
  22. }
  23. function Logger2() {
  24.   const { addLog } = useContext(LogContext);
  25.   console.log('Logger2 render')
  26.   return (
  27.     <>
  28.       

    一個能發(fā)日志的組件2

  29.        addLog("logger2")}>發(fā)日志
  30.     
  31.   );
  32. }
  33. function LogsPanel() {
  34.   const { logs } = useContext(LogContext);
  35.   return logs.map((log, index) => {log}

    );
  36. }
  37. export default function App() {
  38.   return (
  39.     
  40.       {/* 寫日志 */}
  41.       
  42.       
  43.       {/* 讀日志 */}
  44.       
  45.       
  •     
  •   );
  • }
  • 我們已經(jīng)用上了上一章節(jié)的優(yōu)化小技巧,單獨的把 LogProvider 封裝起來,并且把子組件提升到外層傳入。

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

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

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

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

    那么解決辦法是什么呢?其實就是讀寫分離,我們把 logs(讀)和 setLogs(寫)分別通過不同的 Provider 傳遞,這樣負責寫入的組件更改了 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.     
    8.       
    9.         {children}
    10.       
    11.     
    12.   );
    13. }

    我們剛剛也提到,需要保證 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. function App() {
    4.   const logs = React.useContext(LogStateContext)
    5. }

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

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

    在加上點健壯性保證?

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

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

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

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

    組合 Providers

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

     
     
     
     
    1. const StateProviders = ({ children }) => (
    2.   
    3.     
    4.       
    5.         
    6.           {children}
    7.         
    8.       
    9.     
    10.   
    11. )
    12. function App() {
    13.   return (
    14.     
    15.       
    16.     
    17.   )
    18. }

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

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

    代碼就可以簡化成這樣:

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

    總結(jié)

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

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

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


    網(wǎng)頁題目:我在大廠寫React學(xué)到了什么?性能優(yōu)化篇
    URL分享:http://www.dlmjj.cn/article/cddppjc.html