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

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

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
React 性能優(yōu)化技巧總結(jié)

本文將從 render 函數(shù)的角度總結(jié) React App 的優(yōu)化技巧。需要提醒的是,文中將涉及 React 16.8.2 版本的內(nèi)容(也即 Hooks),因此請(qǐng)至少了解 useState 以保證食用效果。

創(chuàng)新新互聯(lián),憑借十載的網(wǎng)站制作、成都網(wǎng)站制作經(jīng)驗(yàn),本著真心·誠心服務(wù)的企業(yè)理念服務(wù)于成都中小企業(yè)設(shè)計(jì)網(wǎng)站有數(shù)千家案例。做網(wǎng)站建設(shè),選創(chuàng)新互聯(lián)公司。

正文開始。

當(dāng)我們討論 React App 的性能問題時(shí),組件的 渲染 速度是一個(gè)重要問題。在進(jìn)入到具體優(yōu)化建議之前,我們先要理解以下 3 點(diǎn):

  1. 當(dāng)我們?cè)谡f「render」時(shí),我們?cè)谡f什么?
  2. 什么時(shí)候會(huì)執(zhí)行「render」?
  3. 在「render」過程中會(huì)發(fā)生什么?

解讀 render 函數(shù)

這部分涉及 reconciliation 和 diffing 的概念,當(dāng)然官方文檔在 這里 。

當(dāng)我們?cè)谡f「render」時(shí),我們?cè)谡f什么?

這個(gè)問題其實(shí)寫過 React 的人都會(huì)知道,這里再簡單說下:

在 class 組件中,我們指的是 render 方法:

 
 
 
  1. class Foo extends React.Component {  
  2.  render() {  
  3.    return 

     Foo 

    ;  
  4.  }  
  5. }  

在函數(shù)式組件中,我們指的是函數(shù)組件本身:

 
 
 
  1. function Foo() {  
  2.   return 

     Foo 

    ;  
  3. }  

什么時(shí)候會(huì)執(zhí)行「render」?

render 函數(shù)會(huì)在兩種場(chǎng)景下被調(diào)用:

1. 狀態(tài)更新時(shí)

a. 繼承自 React.Component 的 class 組件更新狀態(tài)時(shí)

 
 
 
  1. import React from "react";  
  2. import ReactDOM from "react-dom";  
  3.   
  4. class App extends React.Component {  
  5.   render() {  
  6.     return ;  
  7.   }  
  8. }  
  9.   
  10. class Foo extends React.Component {  
  11.   state = { count: 0 };  
  12.   
  13.   increment = () => {  
  14.     const { count } = this.state;  
  15.   
  16.     const newCount = count < 10 ? count + 1 : count;  
  17.   
  18.     this.setState({ count: newCount });  
  19.   };  
  20.   
  21.   render() {  
  22.     const { count } = this.state;  
  23.     console.log("Foo render");  
  24.   
  25.     return (  
  26.       
      
  27.         

     {count} 

      
  28.         Increment  
  29.       
  
  •     );  
  •   }  
  • }  
  •   
  • const rootElement = document.getElementById("root");  
  • ReactDOM.render(, rootElement);  
  • 可以看到,代碼中的邏輯是我們點(diǎn)擊就會(huì)更新 count,到 10 以后,就會(huì)維持在 10。增加一個(gè) console.log,這樣我們就可以知道 render 是否被調(diào)用了。從執(zhí)行結(jié)果可以知道,即使 count 到了 10 以上,render 仍然會(huì)被調(diào)用。

    總結(jié):繼承了 React.Component 的 class 組件,即使?fàn)顟B(tài)沒變化,只要調(diào)用了setState 就會(huì)觸發(fā) render。

    b. 函數(shù)式組件更新狀態(tài)時(shí)

    我們用函數(shù)實(shí)現(xiàn)相同的組件,當(dāng)然因?yàn)橐袪顟B(tài),我們用上了 useState hook:

     
     
     
    1. import React, { useState } from "react";  
    2. import ReactDOM from "react-dom";  
    3.   
    4. class App extends React.Component {  
    5.   render() {  
    6.     return ;  
    7.   }  
    8. }  
    9.   
    10. function Foo() {  
    11.   const [count, setCount] = useState(0);  
    12.   
    13.   function increment() {  
    14.     const newCount = count < 10 ? count + 1 : count;  
    15.     setCount(newCount);  
    16.   }  
    17.   
    18.   console.log("Foo render");  
    19.     
    20.   return (  
    21.     
        
    22.       

       {count} 

        
    23.       Increment  
    24.     
      
  •   );  
  • }  
  •   
  • const rootElement = document.getElementById("root");  
  • ReactDOM.render(, rootElement);  
  • 我們可以注意到,當(dāng)狀態(tài)值不再改變之后,render 的調(diào)用就停止了。

    總結(jié):對(duì)函數(shù)式組件來說,狀態(tài)值改變時(shí)才會(huì)觸發(fā) render 函數(shù)的調(diào)用。

    2. 父容器重新渲染時(shí)

     
     
     
    1. import React from "react";  
    2. import ReactDOM from "react-dom";  
    3.   
    4. class App extends React.Component {  
    5.   state = { name: "App" };  
    6.   render() {  
    7.     return (  
    8.         
    9.           
    10.          this.setState({ name: "App" })}>  
    11.           Change name  
    12.           
    13.       
      
  •     );  
  •   }  
  • }  
  •   
  • function Foo() {  
  •   console.log("Foo render");  
  •   
  •   return (  
  •     
      
  •       

     Foo 

      
  •     
  •   
  •   );  
  • }  
  •   
  • const rootElement = document.getElementById("root");  
  • ReactDOM.render(, rootElement);  
  • 只要點(diǎn)擊了 App 組件內(nèi)的 Change name 按鈕,就會(huì)重新 render。而且可以注意到,不管 Foo 具體實(shí)現(xiàn)是什么,F(xiàn)oo 都會(huì)被重新渲染。

    總結(jié):無論組件是繼承自 React.Component 的 class 組件還是函數(shù)式組件,一旦父容器重新 render,組件的 render 都會(huì)再次被調(diào)用。

    在「render」過程中會(huì)發(fā)生什么?

    只要 render 函數(shù)被調(diào)用,就會(huì)有兩個(gè)步驟按順序執(zhí)行。這兩個(gè)步驟非常重要,理解了它們才好知道如何去優(yōu)化 React App。

    Diffing

    在此步驟中,React 將新調(diào)用的 render 函數(shù)返回的樹與舊版本的樹進(jìn)行比較,這一步是 React 決定如何更新 DOM 的必要步驟。雖然 React 使用高度優(yōu)化的算法執(zhí)行此步驟,但仍然有一定的性能開銷。

    Reconciliation

    基于 diffing 的結(jié)果,React 更新 DOM 樹。這一步因?yàn)樾枰遁d和掛載 DOM 節(jié)點(diǎn)同樣存在許多性能開銷。

    開始我們的 Tips

    Tip #1:謹(jǐn)慎分配 state 以避免不必要的 render 調(diào)用

    我們以下面為例,其中 App 會(huì)渲染兩個(gè)組件:

     
     
     
    1. CounterLabel  
    2. List  
     
     
     
    1. import React, { useState } from "react";  
    2. import ReactDOM from "react-dom";  
    3.   
    4. const ITEMS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];  
    5.   
    6. function App() {  
    7.   const [count, setCount] = useState(0);  
    8.   const [items, setItems] = useState(ITEMS);  
    9.   return (  
    10.       
    11.        setCount(count + 1)} />  
    12.         
    13.     
      
  •   );  
  • }  
  •   
  • function CounterLabel({ count, increment }) {  
  •   return (  
  •     <>  
  •       

    {count} 

      
  •        Increment   
  •       
  •   );  
  • }  
  •   
  • function List({ items }) {  
  •   console.log("List render");  
  •   
  •   return (  
  •     
        
    •       {items.map((item, index) => (  
    •         {item} 
    •   
    •       ))}  
    •     
      
  •   );  
  • }  
  •   
  • const rootElement = document.getElementById("root");  
  • ReactDOM.render(, rootElement);  
  • 執(zhí)行上面代碼可知,只要父組件 App 中的狀態(tài)被更新, CounterLabel 和 List 就都會(huì)更新。

    當(dāng)然, CounterLabel 重新渲染是正常的,因?yàn)?count 發(fā)生了變化,自然要重新渲染;但是對(duì)于 List 而言,就完全是不必要的更新了,因?yàn)樗匿秩九c count 無關(guān)。 盡管 React 并不會(huì)在 reconciliation 階段真的更新 DOM,畢竟完全沒變化,但是仍然會(huì)執(zhí)行 diffing 階段來對(duì)前后的樹進(jìn)行對(duì)比,這仍然存在性能開銷。

    還記得 render 執(zhí)行過程中的 diffing 和 reconciliation 階段嗎?前面講過的東西在這里碰到了。

    因此,為了避免不必要的 diffing 開銷,我們應(yīng)當(dāng)考慮將特定的狀態(tài)值放到更低的層級(jí)或組件中(與 React 中所說的「提升」概念剛好相反)。在這個(gè)例子中,我們可以通過將 count 放到 CounterLabel 組件中管理來解決這個(gè)問題。

    Tip #2:合并狀態(tài)更新

    因?yàn)槊看螤顟B(tài)更新都會(huì)觸發(fā)新的 render 調(diào)用,那么更少的狀態(tài)更新也就可以更少的調(diào)用 render 了。

    我們知道,React class 組件有 componentDidUpdate(prevProps, prevState) 的鉤子,可以用來檢測(cè) props 或 state 有沒有發(fā)生變化。盡管有時(shí)有必要在 props 發(fā)生變化時(shí)再觸發(fā) state 更新,但我們總可以避免在一次 state 變化后再進(jìn)行一次 state 更新這種操作:

     
     
     
    1. import React from "react";  
    2. import ReactDOM from "react-dom";  
    3.   
    4. function getRange(limit) {  
    5.   let range = [];  
    6.   
    7.   for (let i = 0; i < limit; i++) {  
    8.     range.push(i);  
    9.   }  
    10.   
    11.   return range;  
    12. }  
    13.   
    14. class App extends React.Component {  
    15.   state = {  
    16.     numbers: getRange(7),  
    17.     limit: 7  
    18.   };  
    19.   
    20.   handleLimitChange = e => {  
    21.     const limit = e.target.value;  
    22.     const limitChanged = limit !== this.state.limit;  
    23.   
    24.     if (limitChanged) {  
    25.       this.setState({ limit });  
    26.     }  
    27.   };  
    28.   
    29.   componentDidUpdate(prevProps, prevState) {  
    30.     const limitChanged = prevState.limit !== this.state.limit;  
    31.     if (limitChanged) {  
    32.       this.setState({ numbers: getRange(this.state.limit) });  
    33.     }  
    34.   }  
    35.   
    36.   render() {  
    37.     return (  
    38.       
        
    39.         
    40.           onChange={this.handleLimitChange}  
    41.           placeholder="limit"  
    42.           value={this.state.limit}  
    43.         />  
    44.         {this.state.numbers.map((number, idx) => (  
    45.           {number} 

        
    46.         ))}  
    47.       
      
  •     );  
  •   }  
  • }  
  •   
  • const rootElement = document.getElementById("root");  
  • ReactDOM.render(, rootElement);  
  • 這里渲染了一個(gè)范圍數(shù)字序列,即范圍為 0 到 limit。只要用戶改變了 limit 值,我們就會(huì)在 componentDidUpdate 中進(jìn)行檢測(cè),并設(shè)定新的數(shù)字列表。

    毫無疑問,上面的代碼是可以滿足需求的,但是,我們?nèi)匀豢梢赃M(jìn)行優(yōu)化。

    上面的代碼中,每次 limit 發(fā)生改變,我們都會(huì)觸發(fā)兩次狀態(tài)更新:***次是為了修改 limit,第二次是為了修改展示的數(shù)字列表。這樣一來,每次 limit 的變化會(huì)帶來兩次 render 開銷:

     
     
     
    1. // 初始狀態(tài)  
    2. { limit: 7, numbers: [0, 1, 2, 3, 4, 5, 6]  
    3. // 更新 limit -> 4  
    4. render 1: { limit: 4, numbers: [0, 1, 2, 3, 4, 5, 6] } //   
    5. render 2: { limit: 4, numbers: [0, 2, 3]  

    我們的代碼邏輯帶來了下面的問題:

    • 我們觸發(fā)了比實(shí)際需要更多的狀態(tài)更新;
    • 我們出現(xiàn)了「不連續(xù)」的渲染結(jié)果,即數(shù)字列表與 limit 不匹配。

    為了改進(jìn),我們應(yīng)避免在不同的狀態(tài)更新中改變數(shù)字列表。事實(shí)上,我們可以在一次狀態(tài)更新中搞定:

     
     
     
    1. import React from "react";  
    2. import ReactDOM from "react-dom";  
    3.   
    4. function getRange(limit) {  
    5.   let range = [];  
    6.   
    7.   for (let i = 0; i < limit; i++) {  
    8.     range.push(i);  
    9.   }  
    10.   
    11.   return range;  
    12. }  
    13.   
    14. class App extends React.Component {  
    15.   state = {  
    16.     numbers: [1, 2, 3, 4, 5, 6],  
    17.     limit: 7  
    18.   };  
    19.   
    20.   handleLimitChange = e => {  
    21.     const limit = e.target.value;  
    22.     const limitChanged = limit !== this.state.limit;  
    23.     if (limitChanged) {  
    24.       this.setState({ limit, numbers: getRange(limit) });  
    25.     }  
    26.   };  
    27.   
    28.   render() {  
    29.     return (  
    30.       
        
    31.         
    32.           onChange={this.handleLimitChange}  
    33.           placeholder="limit"  
    34.           value={this.state.limit}  
    35.         />  
    36.         {this.state.numbers.map((number, idx) => (  
    37.           {number} 

        
    38.         ))}  
    39.       
      
  •     );  
  •   }  
  • }  
  •   
  • const rootElement = document.getElementById("root");  
  • ReactDOM.render(, rootElement);  
  • Tip #3:使用 PureComponent 和 React.memo 以避免不必要的 render 調(diào)用

    我們?cè)谥暗睦又锌吹綄⑻囟顟B(tài)值放到更低的層級(jí)來避免不必要渲染的方法,不過這并不總是有用。

    我們來看下下面的例子:

     
     
     
    1. import React, { useState } from "react";  
    2. import ReactDOM from "react-dom";  
    3.   
    4. function App() {  
    5.   const [isFooVisible, setFooVisibility] = useState(false);  
    6.   
    7.   return (  
    8.       
    9.       {isFooVisible ? (  
    10.          setFooVisibility(false)} />  
    11.       ) : (  
    12.          setFooVisibility(true)}>Show Foo   
    13.       )}  
    14.         
    15.     
      
  •   );  
  • }  
  •   
  • function Foo({ hideFoo }) {  
  •   return (  
  •     <>  
  •       

    Foo

      
  •       Hide Foo  
  •       
  •   );  
  • }  
  •   
  • function Bar({ name }) {  
  •   return 

    {name}

    ;  
  • }  
  •   
  • const rootElement = document.getElementById("root");  
  • ReactDOM.render(, rootElement);  
  • 可以看到,只要父組件 App 的狀態(tài)值 isFooVisible 發(fā)生變化,F(xiàn)oo 和 Bar 就都會(huì)被重新渲染。

    這里因?yàn)闉榱藳Q定 Foo 是否要被渲染出來,我們需要將 isFooVisible 放在 App中維護(hù),因此也就不能將狀態(tài)拆出放到更低的層級(jí)。不過,在 isFooVisible 發(fā)生變化時(shí)重新渲染 Bar 仍然是不必要的,因?yàn)?Bar 并不依賴 isFooVisible。我們只希望 Bar 在傳入屬性 name 變化時(shí)重新渲染。

    那我們?cè)撛趺锤隳兀績煞N方法。

    其一,對(duì) Bar 做記憶化(memoize):

     
     
     
    1. const Bar = React.memo(function Bar({name}) {  
    2.   return 

      {name}

      ;  
    3. });  

    這就能保證 Bar 只在 name 發(fā)生變化時(shí)才重新渲染。

    此外,另一個(gè)方法就是讓 Bar 繼承 React.PureComponent 而非 React.Component:

     
     
     
    1. class Bar extends React.PureComponent {  
    2.  render() {  
    3.    return 

      {name}

      ;  
    4.  }  
    5. }  

    是不是很熟悉?我們經(jīng)常提到使用 React.PureComponent 能帶來一定的性能提升,避免不必要的 render。

    總結(jié):避免組件不必要的渲染的方法有:React.memo 包裹的函數(shù)式組件,繼承自 React.PureComponent 的 class 組件。

    為什么不讓每個(gè)組件都繼承 PureComponent 或者用 memo 包呢?

    如果這條建議可以讓我們避免不必要的重新渲染,那我們?yōu)槭裁床话衙總€(gè) class 組件變成 PureComponent、把每個(gè)函數(shù)式組件用 React.memo 包起來?為什么有了更好的方法還要保留 React.Component 呢?為什么函數(shù)式組件不默認(rèn)記憶化呢?

    毫無疑問,這些方法并不總是萬靈藥。

    嵌套對(duì)象的問題

    我們先來考慮下 PureComponent 和 React.memo 的組件到底做了什么?

    每次更新的時(shí)候(包括狀態(tài)更新或上層組件重新渲染),它們就會(huì)在新 props、state 和舊 props、state 之間對(duì) key 和 value 進(jìn)行淺比較。淺比較是個(gè)嚴(yán)格相等的檢查,如果檢測(cè)到差異,render 就會(huì)執(zhí)行:

     
     
     
    1. // 基本類型的比較  
    2. shallowCompare({ name: 'bar'}, { name: 'bar'}); // output: true  
    3. shallowCompare({ name: 'bar'}, { name: 'bar1'}); // output: false  

    盡管基本類型(如字符串、數(shù)字、布爾)的比較可以工作的很好,但對(duì)象這類復(fù)雜的情況可能就會(huì)帶來意想不到的行為:

     
     
     
    1. shallowCompare({ name: {first: 'John', last: 'Schilling'}},  
    2.                { name: {first: 'John', last: 'Schilling'}}); // output: false  

    上述兩個(gè) name 對(duì)應(yīng)的對(duì)象的引用是不同的。

    我們重新看下之前的例子,然后修改我們傳入 Bar 的 props:

     
     
     
    1. import React, { useState } from "react";  
    2. import ReactDOM from "react-dom";  
    3.   
    4. const Bar = React.memo(function Bar({ name: { first, last } }) {  
    5.   console.log("Bar render");  
    6.   
    7.   return (  
    8.     

        

    9.       {first} {last}  
    10.       
    11.   );  
    12. });  
    13.   
    14. function Foo({ hideFoo }) {  
    15.   return (  
    16.     <>  
    17.       

      Foo

        
    18.       Hide Foo  
    19.       
    20.   );  
    21. }  
    22.   
    23. function App() {  
    24.   const [isFooVisible, setFooVisibility] = useState(false);  
    25.   
    26.   return (  
    27.       
    28.       {isFooVisible ? (  
    29.          setFooVisibility(false)} />  
    30.       ) : (  
    31.          setFooVisibility(true)}>Show Foo  
    32.       )}  
    33.         
    34.     
      
  •   );  
  • }  
  •   
  • const rootElement = document.getElementById("root");  
  • ReactDOM.render(, rootElement);  
  • 盡管 Bar 做了記憶化且 props 值并沒有發(fā)生變動(dòng),每次父組件重新渲染時(shí)它仍然會(huì)重新渲染。這是因?yàn)楸M管每次比較的兩個(gè)對(duì)象擁有相同的值,引用并不同。

    函數(shù) props 的問題

    我們也可以把函數(shù)作為 props 向組件傳遞,當(dāng)然,在 JavaScript 中函數(shù)也會(huì)傳遞引用,因此淺比較也是基于其傳遞的引用。

    因此,如果我們傳遞的是箭頭函數(shù)(匿名函數(shù)),組件仍然會(huì)在父組件重新渲染時(shí)重新渲染。

    Tip #4:更好的 props 寫法

    前面的問題的一種解決方法是改寫我們的 props。

    我們不傳遞對(duì)象作為 props,而是 將對(duì)象拆分成基本類型 :

     
     
     
    1.   

    而對(duì)于傳遞箭頭函數(shù)的場(chǎng)景,我們可以代以只***聲明過一次的函數(shù),從而總可以拿到相同的引用,如下所示:

     
     
     
    1. class App extends React.Component{  
    2.   constructor(props) {  
    3.     this.doSomethingMethod = this.doSomethingMethod.bind(this);      
    4.   }  
    5.   doSomethingMethod () { // do something}  
    6.     
    7.   render() {  
    8.     return   
    9.   }  
    10. }  

    Tip #5:控制更新

    還是那句話,任何方法總有其適用范圍。

    第三條建議雖然處理了不必要的更新問題,但我們也不總能使用它。

    而第四條,在某些情況下我們并不能拆分對(duì)象,如果我們傳遞了某種嵌套確實(shí)復(fù)雜的數(shù)據(jù)結(jié)構(gòu),那我們也很難將其拆分開來。

    不僅如此,我們也不總能傳遞只聲明了一次的函數(shù)。比如在我們的例子中,如果 App 是個(gè)函數(shù)式組件,恐怕就不能做到這一點(diǎn)了(在 class 組件中,我們可以用 bind 或者類內(nèi)箭頭函數(shù)來保證 this 的指向及***聲明,而在函數(shù)式組件中則可能會(huì)有些問題)。

    幸運(yùn)的是, 無論是 class 組件還是函數(shù)式組件,我們都有辦法控制淺比較的邏輯 。

    在 class 組件中,我們可以使用生命周期鉤子 shouldComponentUpdate(prevProps, prevState)來返回一個(gè)布爾值,當(dāng)返回值為 true 時(shí)才會(huì)觸發(fā) render。

    而如果我們使用 React.memo,我們可以傳遞一個(gè)比較函數(shù)作為第二個(gè)參數(shù)。

    注意! React.memo 的第二參數(shù)(比較函數(shù))和 shouldComponentUpdate 的邏輯是相反的,只有當(dāng)返回值為 false 的時(shí)候才會(huì)觸發(fā) render。 參考文檔 。

     
     
     
    1. const Bar = React.memo(  
    2.   function Bar({ name: { first, last } }) {  
    3.     console.log("update");  
    4.     return (  
    5.       

        

    6.         {first} {last}  
    7.         
    8.     );  
    9.   },  
    10.   (prevProps, newProps) =>  
    11.     prevProps.name.first === newProps.name.first &&  
    12.     prevProps.name.last === newProps.name.last  
    13. );  

    盡管這條建議是可行的,但我們?nèi)砸⒁?nbsp;比較函數(shù)的性能開銷 。如果 props 對(duì)象過深,反而會(huì)消耗不少的性能。

    總結(jié)

    上述場(chǎng)景仍不夠全面,但多少能帶來一些啟發(fā)性思考。當(dāng)然在性能方面,我們還有許多其他的問題需要考慮,但遵守上述的準(zhǔn)則仍能帶來相當(dāng)不錯(cuò)的性能提升。


    分享名稱:React 性能優(yōu)化技巧總結(jié)
    轉(zhuǎn)載注明:http://www.dlmjj.cn/article/dpgsepg.html