1. 程式人生 > 實用技巧 >React相關問題學習

React相關問題學習

呼叫方法時為什麼需要bind this

第一次使用React的事件處理時,會有些疑惑為什麼需要進行手動繫結this。官網對 事件處理這樣解釋:

你必須謹慎對待 JSX 回撥函式中的 this,在 JavaScript 中,class 的方法預設不會繫結this。如果你忘記繫結 this.handleClick 並把它傳入了 onClick,當你呼叫這個函式的時候 this 的值為 undefined

這並不是 React 特有的行為;這其實與 JavaScript 函式工作原理有關。通常情況下,如果你沒有在方法後面新增 (),例如 onClick={this.handleClick},你應該為這個方法繫結 this

如果深究原因這主要是由於React的事件處理機制:

簡單理解就是:React在元件載入(Mount)和更新(Update)時,將事件通過addEventListener統一註冊到document上,然後會有一個事件池統一儲存所有事件,當事件觸發的時候,通過dispatchEvent派發事件。

幫助我們理解為什麼需要bind this,就可以理解為:事件處理程式會被當作回撥函式進行使用

由於JavaScript的this指向問題,回撥函式會丟失this指向,預設指向undifined

setState 同步還是非同步

1. setState 是同步還是非同步?

我的回答是執行過程程式碼同步的,只是合成事件和鉤子函式的呼叫順序在更新之前,導致在合成事件和鉤子函式中沒法立馬拿到更新後的值,形式了所謂的“非同步”

,所以表現出來有時是同步,有時是“非同步”。

2. 何時是同步,何時是非同步呢?

只在合成事件和鉤子函式中是“非同步”的,在原生事件和 setTimeout/setInterval等原生 API 中都是同步的。簡單的可以理解為被 React 控制的函式裡面就會表現出“非同步”,反之表現為同步。

3. 那為什麼會出現非同步的情況呢?

為了做效能優化,將 state 的更新延緩到最後批量合併再去渲染對於應用的效能優化是有極大好處的,如果每次的狀態改變都去重新渲染真實 dom,那麼它將帶來巨大的效能消耗。

React生命週期

掛載

元件首次被例項化建立並插入DOM中需要執行的生命週期函式:

  • constructor():

    需要在元件內初始化state或進行方法繫結時,需要定義constructor()函式。不可在constructor()函式中呼叫setState()

  • static getDerivedStateFromProps():

    執行getDerivedStateFromProps函式返回我們要的更新的stateReact會根據函式的返回值拿到新的屬性。

  • render():

    函式類元件必須定義的render()函式,是類元件中唯一必須實現的方法。render()函式應為純函式,也就是說只要元件stateprops沒有變化,返回的結果是相同的。其返回結果可以是:1、React元素;2、陣列或 fragments;3、Portals;4、字串或數值型別;5、布林值或null。不可在render()函式中呼叫setState()

  • componentDidMount():

    元件被掛載插入到Dom中呼叫此方法,可以在此方法內執行副作用操作,如獲取資料,更新state等操作。

更新

當元件的propsstate改變時會觸發更新需要執行的生命週期函式:

  • static getDerivedStateFromProps():

    getDerivedStateFromProps會在呼叫 render方法之前呼叫,並且在初始掛載及後續更新時都會被呼叫。它應返回一個物件來更新state,如果返回null則不更新任何內容。

  • shouldComponentUpdate():

    根據shouldComponentUpdate()的返回值,判斷React元件的是否執行更新操作。React.PureComponent就是基於淺比較進行效能優化的。一般在實際元件實踐中,不建議使用該方法阻斷更新過程,如果有需要建議使用React.PureComponent

  • render():

    在元件更新階段時如果shouldComponentUpdate()返回false值時,將不執行render()函式。

  • getSnapshotBeforeUpdate():

    該生命週期函式執行在pre-commit階段,此時已經可以拿到Dom節點資料了,該宣告週期返回的資料將作為componentDidUpdate()第三個引數進行使用。

  • componentDidUpdate():

    shouldComponentUpdate()返回值false時,則不會呼叫componentDidUpdate()

解除安裝

  • componentWillUnmount()

    會在元件解除安裝及銷燬之前直接呼叫。一般使用此方法用於執行副作用的清理操作,如取消定時器,取消事件繫結等。

React Diff演算法

React diff 作為 Virtual DOM 的加速器,其演算法上的改進優化是 React 整個介面渲染的基礎,以及效能提高的保障。

Diff演算法並不是由React首發,Diff演算法早已存在。但是傳統的Diff演算法,通過迴圈遞迴對比依次對比,效率低下,演算法複雜度達到 O(n^3)。而React則改進Diff演算法引入React。

React 分別對 tree diff、component diff 以及 element diff 進行演算法優化

Tree Diff

由於Web UI 中對DOM節點的跨層級操作很少,對樹進行分層比較,兩棵樹只會對同一層級的節點進行比較,即同一個父節點下的所有子節點。

當發現節點不存在時,則該節點及其子節點都會被刪除,不會用於進一步的比較。

Component Diff

React 是基於元件構建應用的,對於元件間的比較所採取的策略也是簡潔高效。

  • 如果是同一型別的元件,按照原策略繼續比較 virtual DOM tree。
  • 如果不是,則將該元件判斷為 dirty component,從而替換整個元件下的所有子節點。
  • 對於同一型別的元件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點那可以節省大量的 diff 運算時間,因此 React 允許使用者通過 shouldComponentUpdate() 來判斷該元件是否需要進行 diff。

如下圖,當 component D 改變為 component G 時,即使這兩個 component 結構相似,一旦 React 判斷 D 和 G 是不同型別的元件,就不會比較二者的結構,而是直接刪除 component D,重新建立 component G 以及其子節點。雖然當兩個 component 是不同型別但結構相似時,React diff 會影響效能,但正如 React 官方部落格所言:不同型別的 component 是很少存在相似 DOM tree 的機會,因此這種極端因素很難在實現開發過程中造成重大影響的。

Element Diff

當節點處於同一層級時,React diff 提供了三種節點操作,分別為:INSERT_MARKUP(插入)、MOVE_EXISTING(移動)和 REMOVE_NODE(刪除)。

  • INSERT_MARKUP,新的 component 型別不在老集合裡, 即是全新的節點,需要對新節點執行插入操作。
  • MOVE_EXISTING,在老集合有新 component 型別,且 element 是可更新的型別,generateComponentChildren 已呼叫 receiveComponent,這種情況下 prevChild=nextChild,就需要做移動操作,可以複用以前的 DOM 節點。
  • REMOVE_NODE,老 component 型別,在新集合裡也有,但對應的 element 不同則不能直接複用和更新,需要執行刪除操作,或者老 component 不在新集合裡的,也需要執行刪除操作。

如下圖,老集合中包含節點:A、B、C、D,更新後的新集合中包含節點:B、A、D、C,此時新老集合進行 diff 差異化對比,發現 B != A,則建立並插入 B 至新集合,刪除老集合 A;以此類推,建立並插入 A、D 和 C,刪除 B、C 和 D。

而React則Diff不會進行這種繁雜冗餘操作,React則允許開發者對同一層級的同一組子節點,新增唯一key進行區分。新老集合進行 diff 差異化對比,通過 key 發現新老集合中的節點都是相同的節點,因此無需進行節點刪除和建立,只需要將老集合中節點的位置進行移動,更新為新集合中節點的位置,

React 的 虛擬DOM

本質上是JavaScript物件,這個物件就是更加輕量級的對DOM的描述。

React實現了其對DOM節點的控制,但是DOM節點是非常複雜的,對節點的操作非常耗費資源,其例項屬性非常多,對於節點操作很多冗餘屬性。大部分屬性對於Diff操作並沒有用處,所以就使用更加輕量級的虛擬DOM對DOM進行描述。

那麼現在的過程就是這樣:

  1. 維護一個使用 JS 物件表示的 Virtual DOM,與真實 DOM 一一對應
  2. 對前後兩個 Virtual DOM 做 diff ,生成變更(Mutation)
  3. 把變更應用於真實 DOM,生成最新的真實 DOM

通過以上,我們發現 虛擬DOM優點是通過Diff演算法減少JavaScript操作真實DOM效能消耗,但這僅僅只是其中之一的優點。

Virtual DOM更多作用

  1. Virtual DOM通過犧牲了部分效能的前提下,增加了可維護性。

  2. 實現了對DOM的集中化操作,當資料改變時先修改Virtual DOM,再統一反映到真實DOM中,用最小的代價更新DOM,提高了效率。

  3. 抽象了原本的渲染過程,實現了跨平臺的能力,而不僅僅侷限於瀏覽器DOM中,也可以使用到安卓和IOS的原生元件。

  4. 打開了函式式UI程式設計的大門。

Virtual DOM的缺點

  1. 首次渲染DOM的時候,由於多一層Virtual DOM,會比innerHTML插入慢。
  2. 虛擬DOM需要在記憶體中維護一份DOM副本。

React 效能優化

函式元件效能優化

主要講函式元件的效能優化方式,對類元件暫不說明。

React 函式元件優化思路主要有兩個:

  1. 減少render的次數,因為在React所花的時間最多的就是進行render
  2. 減少計算的量。主要是減少重複計算的量,因為函式元件重新渲染時會從頭開始進行函式呼叫。

在使用類元件的時候,使用的 React 優化 API 主要是:shouldComponentUpdatePureComponent,這兩個 API 所提供的解決思路都是為了減少重新 render 的次數,主要是減少父元件更新而子元件也更新的情況。

但是我們使用函式時元件,並沒有生命週期和類,我們就需要換種方式進行效能優化。

減少render次數

通常來說,有三種原因會進行重新render

  1. 自身狀態改變
  2. 父元件重新渲染,導致子元件重新渲染,但是父元件傳遞的props並沒有發生改變。
  3. 父元件重新渲染,導致子元件重新渲染,但是元件傳遞的的props發生改變。
React.memo

首先要介紹的就是 React.memo,這個 API 可以說是對標類元件裡面的 PureComponent,這是可以減少重新 render 的次數的。

我們在開發時會遇到,更改父元件狀態,父元件進行重新渲染,子元件的props並沒有發生改變,但子元件依然會重新渲染。

我們就可以使用React.memo包裹元件,如果元件Props未發生變化的話就不會進行重新渲染。

import React from "react";

function Child(props) {
  console.log(props.name)
  return <h1>{props.name}</h1>
}

export default React.memo(Child)

預設情況下,React.memo只會進行淺層比較,如果想要自己控制可以考慮,傳入第二個引數,自定義進行控制。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 傳入 render 方法的返回結果與
  將 prevProps 傳入 render 方法的返回結果一致則返回 true,
  否則返回 false
  */
}
export default React.memo(MyComponent, areEqual);
useCallback

當父元件傳遞給子元件一個函式進行呼叫的時候,如果父元件重新渲染,由於元件中函式會被重新呼叫一遍,那麼前後兩個傳遞的函式引用並不會是一個,所以 父元件傳遞給子元件的props發生了改變

這種情況就可以使用useCallback,在函式沒有改變的時候,返回記憶化的函式,傳遞給子元件。

// index.js
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Child from "./child";

function App() {
  const [title, setTitle] = useState("這是一個 title");
  const [subtitle, setSubtitle] = useState("我是一個副標題");

  const callback = () => {
    setTitle("標題改變了");
  };

  // 通過 useCallback 進行記憶 callback,並將記憶的 callback 傳遞給 Child
  const memoizedCallback = useCallback(callback, [])
  
  return (
    <div className="App">
      <h1>{title}</h1>
      <h2>{subtitle}</h2>
      <button onClick={() => setSubtitle("副標題改變了")}>改副標題</button>
      <Child onClick={memoizedCallback} name="桃桃" />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

減少計算的量

useMemo

假設我們有一個函式內部每次都有一個很大的重複計算量,如果每次都是重複呼叫函式,那麼效能會被很大的消耗。

我們可以使用useMemo進行計算的快取

function computeExpensiveValue() {
  // 計算量很大的程式碼
  return xxx
}

const memoizedValue = useMemo(computeExpensiveValue, [a, b]);

useMemo 的第一個引數就是一個函式,這個函式返回的值會被快取起來,同時這個值會作為 useMemo 的返回值,第二個引數是一個數組依賴,如果數組裡面的值有變化,那麼就會重新去執行第一個引數裡面的函式,並將函式返回的值快取起來並作為 useMemo 的返回值 。

通用React 效能優化

懶載入元件

匯入多個檔案合併到一個檔案中的過程叫打包,使應用不必匯入大量外部檔案。所有主要元件和外部依賴項都合併為一個檔案,通過網路傳送出去以啟動並執行 Web 應用。這樣可以節省大量網路呼叫,但這個檔案會變得很大,消耗大量網路頻寬。應用需要等待這個檔案的載入和執行,所以傳輸延遲會帶來嚴重的影響。

為了解決這個問題,我們引入程式碼拆分的概念。像 webpack 這樣的打包器支援就支援程式碼拆分,它可以為應用建立多個包,並在執行時動態載入,減少初始包的大小。

import React, { lazy, Suspense } from "react";

export default class CallingLazyComponents extends React.Component {
  render() {
    
    var ComponentToLazyLoad = null;
    
    if(this.props.name == "Mayank") { 
      ComponentToLazyLoad = lazy(() => import("./mayankComponent"));
    } else if(this.props.name == "Anshul") {
      ComponentToLazyLoad = lazy(() => import("./anshulComponent"));
    }
    return (
        <div>
            <h1>This is the Base User: {this.state.name}</h1>
            <Suspense fallback={<div>Loading...</div>}>
                <ComponentToLazyLoad />
            </Suspense>
        </div>
    )
  }
}

上面的程式碼中有一個條件語句,它查詢 props 值,並根據指定的條件載入主元件中的兩個元件。

我們可以按需懶惰載入這些拆分出來的元件,增強應用的整體效能。

不可變的資料結構

按照React的渲染規則,會有很多效能浪費在不必要的渲染上。所以我們使用Immutable進行精確渲染,簡化shouldComponentUpdate比較。

Immutable Data 就是一旦建立,就不能再被更改的資料。對 Immutable 物件的任何修改或新增刪除操作都會返回一個新的 Immutable 物件。Immutable 實現的原理是 Persistent Data Structure(持久化資料結構),也就是使用舊資料建立新資料時,要保證舊資料同時可用且不變。同時為了避免 deepCopy 把所有節點都複製一遍帶來的效能損耗,Immutable 使用了 Structural Sharing(結構共享),即如果物件樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享。

Hooks

React 一直都提倡使用函式元件,但是有時候需要使用 state 或者其他一些功能時,只能使用類元件,因為函式元件沒有例項,沒有生命週期函式,只有類元件才有,而Hooks就是解決這種問題。

為什麼要使用Hooks

類元件的不足:

  • 趨向複雜難以維護:在生命週期函式中混雜不相干的邏輯(如:在 componentDidMount 中註冊事件以及其他的邏輯,在 componentWillUnmount 中解除安裝事件,這樣分散不集中的寫法,很容易寫出 bug )
  • this 指向問題:父元件給子元件傳遞函式時,必須繫結 this
  • 很難在元件之間複用狀態邏輯:在元件之間複用狀態邏輯很難,可能要用到 render props渲染屬性)或者 HOC高階元件),但無論是渲染屬性,還是高階元件,都會在原先的元件外包裹一層父容器(一般都是 div 元素),導致層級冗餘

Hooks的優勢:

  • 優化類元件的三大問題
  • 能將元件中相互關聯的部分拆分成更小的函式(比如設定訂閱或請求資料)
  • 副作用的關注點分離:副作用指那些沒有發生在資料向檢視轉換過程中的邏輯,如 ajax 請求、訪問原生dom 元素、本地持久化快取、繫結/解綁事件、新增訂閱、設定定時器、記錄日誌等。

useEffect

  • effect(副作用):指那些沒有發生在資料向檢視轉換過程中的邏輯,如 ajax 請求、訪問原生dom 元素、本地持久化快取、繫結/解綁事件、新增訂閱、設定定時器、記錄日誌等。
  • useEffect 就是一個 Effect Hook,給函式元件增加了操作副作用的能力。它跟 class 元件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不過被合併成了一個 API
  • useEffect 接收一個函式,該函式會在元件渲染到螢幕之後才執行,該函式有要求:要麼返回一個能清除副作用的函式,要麼就不返回任何內容
function Counter(){
    let [number,setNumber] = useState(0);
    let [text,setText] = useState('');
    // 相當於componentDidMount 和 componentDidUpdate
    useEffect(()=>{
        console.log('開啟一個新的定時器')
        let $timer = setInterval(()=>{
            setNumber(number=>number+1);
        },1000);
        // useEffect 如果返回一個函式的話,該函式會在元件解除安裝和更新時呼叫
        // useEffect 在執行副作用函式之前,會先呼叫上一次返回的函式
        // 如果要清除副作用,要麼返回一個清除副作用的函式
       /*  return ()=>{
            console.log('destroy effect');
            clearInterval($timer);
        } */
    });
    // },[]);//要麼在這裡傳入一個空的依賴項陣列,這樣就不會去重複執行
    return (
        <>
          <input value={text} onChange={(event)=>setText(event.target.value)}/>
          <p>{number}</p>
          <button>+</button>
        </>
    )
}

useState

React 假設當你多次呼叫 useState 的時候,你能保證每次渲染時它們的呼叫順序是不變的。

通過在函式元件裡呼叫它來給元件新增一些內部 state,React會 在重複渲染時保留這個 state

useState 唯一的引數就是初始 state

useState 會返回一個數組一個 state,一個更新 state 的函式

  • 在初始化渲染期間,返回的狀態 (state) 與傳入的第一個引數 (initialState) 值相同
  • 你可以在事件處理函式中或其他一些地方呼叫這個函式。它類似 class 元件的 this.setState,但是它不會把新的 state 和舊的 state 進行合併,而是直接替換
// 這裡可以任意命名,因為返回的是陣列,陣列解構
const [state, setState] = useState(initialState);

特點:

  • 每次渲染都是一個獨立的閉包:

    • 每一次渲染都有它自己的 Props 和 State
    • 每一次渲染都有它自己的事件處理函式
    • 當點選更新狀態的時候,函式元件都會重新被呼叫,那麼每次渲染都是獨立的,取到的值不會受後面操作的影響
  • 函式式更新

    • 如果新的 state 需要通過使用先前的 state 計算得出,那麼可以將回調函式當做引數傳遞給 setState。該回調函式將接收先前的 state,並返回一個更新後的值。
  • 惰性初始化state

    • initialState 引數只會在元件的初始化渲染中起作用,後續渲染時會被忽略
    • 如果初始 state 需要通過複雜計算獲得,則可以傳入一個函式,在函式中計算並返回初始的 state,此函式只在初始渲染時被呼叫

參考文件

根據 React 歷史來聊如何理解虛擬 DOM

如何對React函式元件進行效能優化

21個React效能優化技巧