setState詳解與React性能優化

setState的同步和異步

1.爲何使用setState

  • 開發中咱們並不能直接經過修改 state 的值來讓界面發生更新react

    • 由於咱們修改了 state 以後, 但願 React 根據最新的 Stete 來從新渲染界面, 可是這種方式的修改 React 並不知道數據發生了變化
    • React 並無實現相似於 Vue2 中的 Object.defineProperty 或者 Vue3 中的Proxy的方式來監聽數據的變化
    • 咱們必須經過 setState 來告知 React 數據已經發生了變化
  • 疑惑: 在組件中並無實現 steState 方法, 爲何能夠調用呢?git

    • 緣由很簡單: setState方法是從 Component 繼承過來的

2.setState異步更新

<details>
<summary>setState是異步更新的</summary>
<img src="https://gitee.com/xmkm/cloudPic/raw/master/img/20201001214531.png" />
</details>github

  • 爲何setState設計爲異步呢?算法

  • 簡單的總結: setState設計爲異步, 能夠顯著的提升性能性能優化

    • 若是每次調用 setState 都進行一次更新, 那麼意味着 render 函數會被頻繁的調用界面從新渲染, 這樣的效率是很低的
    • 最好的方法是獲取到多個更新, 以後進行批量更新
  • 若是同步更新了 state, 但尚未執行 render 函數, 那麼stateprops不能保持同步異步

    • stateprops不能保持一致性, 會在開發中產生不少的問題

3.如何獲取異步的結果

  • 如何獲取 setState 異步更新state後的值?
  • 方式一: setState的回調函數

    • setState接收兩個參數: 第二個參數是回調函數(callback), 這個回調函數會在state更新後執行

  • 方式二: componentDidUpdate生命週期函數

3.setState必定是異步的嗎?

  • 其實能夠分紅兩種狀況
  • 在組件生命週期或React合成事件中, setState是異步的
  • setTimeou或原生DOM事件中, setState是同步的
  • 驗證一: 在setTimeout中的更新 —> 同步更新

  • 驗證二: 在原生DOM事件 —> 同步更新

4.源碼分析

setState的合併

1.數據的合併

  • 經過setState去修改message,是不會對其餘 state 中的數據產生影響的源碼分析

    • 源碼中實際上是有對 原對象新對象 進行合併的

2.多個state的合併

  • 當咱們的屢次調用setState, 只會生效最後一次state

  • setState合併時進行累加: 給setState傳遞函數, 使用前一次state中的值

React 更新機制

1.React 更新機制

  • 咱們在前面已經學習React的渲染流程:

  • 那麼 React 的更新流程呢?

2.React 更新流程

  • Reactprops state 發生改變時,會調用 React render 方法,會建立一顆不一樣的樹
  • React須要基於這兩顆不一樣的樹之間的差異來判斷如何有效的更新UI
  • 若是一棵樹參考另一棵樹進行徹底比較更新, 那麼即便是最早進的算法, 該算法的複雜程度爲 O(n$^3$),其中 n 是樹中元素的數量性能

    • 若是在 React 中使用了該算法, 那麼展現 1000 個元素所須要執行的計算量將在十億的量級範圍
  • 這個開銷太過昂貴了, React的更新性能會變得很是低效
  • 因而,React對這個算法進行了優化,將其優化成了O(n),如何優化的呢?學習

    • 同層節點之間相互比較不會跨節點比較
    • 不一樣類型的節點,產生不一樣的樹結構
    • 開發中,能夠經過key來指定哪些節點在不一樣的渲染下保持穩定

狀況一: 對比不一樣類型的元素

  • 節點爲不一樣的元素React會拆卸原有的樹而且創建起新的樹

    • 當一個元素從 <a> 變成 <img>,從 <Article> 變成 <Comment>,或從 <button> 變成 <div> 都會觸發一個完整的重建流程
    • 當卸載一棵樹時,對應的DOM節點也會被銷燬,組件實例將執行 componentWillUnmount() 方法
    • 當創建一棵新的樹時,對應的 DOM 節點會被建立以及插入到 DOM 中,組件實例將執行 componentWillMount() 方法,緊接着 componentDidMount() 方法
  • 好比下面的代碼更改:

    • React 會銷燬 Counter 組件而且從新裝載一個新的組件,而不會對Counter進行復用

狀況二: 對比同一類型的元素

  • 當比對兩個相同類型的 React 元素時,React 會保留 DOM 節點僅對比更新有改變的屬性
  • 好比下面的代碼更改:

    • 經過比對這兩個元素,React 知道只須要修改 DOM 元素上的 className 屬性

  • 好比下面的代碼更改:

    • 當更新 style 屬性時,React 僅更新有所改變的屬性。
    • 經過比對這兩個元素,React 知道只須要修改 DOM 元素上的 color 樣式,無需修改 fontWeight

  • 若是是同類型的組件元素:

    • 組件會保持不變,React會更新該組件的props,而且調用componentWillReceiveProps()componentWillUpdate() 方法
    • 下一步,調用 render() 方法,diff 算法將在以前的結果以及新的結果中進行遞歸

狀況三: 對子節點進行遞歸

  • 在默認條件下,當遞歸 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表;當產生差別時,生成一個 mutation

    • 咱們來看一下在最後插入一條數據的狀況:👇

    • 前面兩個比較是徹底相同的,因此不會產生mutation
    • 最後一個比較,產生一個mutation,將其插入到新的DOM樹中便可
  • 可是若是咱們是在前面插入一條數據:

    • React會對每個子元素產生一個mutation,而不是保持 <li>星際穿越</li><li>盜夢空間</li> 的不變
    • 這種低效的比較方式會帶來必定的性能問題

React 性能優化

1.key的優化

  • 咱們在前面遍歷列表時,老是會提示一個警告,讓咱們加入一個key屬性:

  • 方式一:在<font color='red'>最後</font>位置插入數據

    • 這種狀況,有無key意義並不大
  • 方式二:在<font color='red'>前面</font>插入數據

    • 這種作法,在沒有 key 的狀況下,全部的<li>都須要進行修改
  • 在下面案例: 當子元素 (這裏的li元素) 擁有 key

    • React 使用 key 來匹配原有樹上的子元素以及最新樹上的子元素
    • <details>
      <summary>下面這種場景下, key爲 111 和 222 的元素僅僅進行位移,不須要進行任何的修改</summary>
      <img src="04-React性能優化與SetState詳解.assets/image-20200815145010219.png" style="zoom:80%;" />
      </details>
    • key333 的元素插入到最前面的位置便可

key的注意事項:

  • key應該是惟一的
  • key不要使用隨機數(隨機數在下一次render時,會從新生成一個數字)
  • 使用index做爲key,對性能是沒有優化的

2.render函數被調用

  • 咱們使用以前的一個嵌套案例:

    • 在App中,咱們增長了一個計數器的代碼
  • 當點擊 +1 時,會從新調用 Apprender 函數

    • 而當 App 的 render函數被調用時,全部的子組件的 render 函數都會被從新調用

  • 那麼,咱們能夠思考一下,在之後的開發中,咱們只要是修改 了App中的數據,全部的子組件都須要從新render,進行 diff 算法,性能必然是很低的:

    • 事實上,不少的組件沒有必需要從新render
    • 它們調用 render 應該有一個前提,就是依賴的數據(state、 props) 發生改變時再調用本身的render方法
  • 如何來控制 render 方法是否被調用呢?

    • 經過shouldComponentUpdate方法便可

3.shouldComponentUpdate

React給咱們提供了一個生命週期方法 shouldComponentUpdate(不少時候,咱們簡稱爲 SCU),這個方法接受參數,而且須要有返回值;主要做用是: 控制當前類組件對象是否調用render方法
  • 該方法有兩個參數:

    • 參數一: nextProps 修改以後, 最新的 porps 屬性
    • 參數二: nextState 修改以後, 最新的 state 屬性
  • 該方法返回值是一個 booolan 類型

    • 返回值爲true, 那麼就須要調用 render 方法
    • 返回值爲false, 那麼不須要調用 render 方法
  • 好比咱們在App中增長一個message屬性:

    • JSX中並沒有依賴這個message, 那麼它的改變不該該引發從新渲染
    • 可是經過setState修改 state 中的值, 因此最後 render 方法仍是被從新調用了
// 決定當前類組件對象是否調用render方法
// 參數一: 最新的props
// 參數二: 最新的state
shouldComponentUpdate(nextProps, nextState) {
  // 默認是: return true
  // 不須要在頁面上渲染則不調用render函數
  return false
}

4.PureComponent

  • 若是全部的類, 咱們都須要手動來實現 shouldComponentUpdate, 那麼會給咱們開發者增長很是多的工做量

    • 咱們設想一下在shouldComponentUpdate中的各類判斷目的是什麼?
    • props 或者 state 中數據是否發生了改變, 來決定shouldComponentUpdate返回 true false
  • 事實上 React 已經考慮到了這一點, 因此 React 已經默認幫咱們實現好了, 如何實現呢?

    • <font color='red'>將 class 繼承自 PureComponent</font>
    • 內部會進行淺層對比最新的 stateporps , 若是組件內沒有依賴 porpsstate 將不會調用render
    • 解決的問題: 好比某些子組件沒有依賴父組件的stateprops, 但卻調用了render函數

5.shallowEqual方法

這個方法中,調用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState),這個 shallowEqual 就是進行淺層比較:

6.高階組件memo

  • 函數式組件如何解決render: 在沒有依賴 stateprops 但卻從新渲染 render 問題
  • 咱們須要使用一個高階組件memo

    • 咱們將以前的Header、Banner、ProductList都經過 memo 函數進行一層包裹
    • Footer沒有使用 memo 函數進行包裹;
    • 最終的效果是,當counter發生改變時,Header、Banner、ProductList的函數不會從新執行,而 Footer 的函數會被從新執行
import React, { PureComponent, memo } from 'react'

// MemoHeader: 沒有依賴props,不會被從新調用render渲染
const MemoHeader = memo(function Header() {
  console.log('Header被調用')
  return <h2>我是Header組件</h2>
})

React知識點總結腦圖

相關文章
相關標籤/搜索