天天看點

React.memo()、useCallback()、useMemo()差別及基本使用

React.memo()、useCallback()、useMemo()差別及基本使用

先來看個簡單的例子

// Parent.jsx
import react, { useState } from 'react';
import Child from '../Child';


function Parent() {
  const [parentCount, setParentCount] = useState(0);
  console.log('父組件重新渲染--------------');
  return (
    <div style={{ background: 'lightseagreen' }}>
      <Child />
      <button type="button" onClick={() => { setParentCount(parentCount + 1); }}>父元件 +1</button>
    </div>
  );
}


export default Parent;      
// Child.jsx
import React from 'react';


function Child() {
  console.log('------------子組件重新渲染');
  return (
    <div style={{ background: 'pink', margin: '50px 0' }}>
      <button type="button">子組件</button>
    </div>
  );
}


export default Child;      

當點選父元件按鈕時,父元件的狀态父元件會被更新,導緻這些父元件重渲染,子元件也會重渲染;而此時我們的子元件和父元件之間并沒有依賴關系,是以重複渲染是優化掉的,可以使用React.memo 包裹子元件

// Child.jsx
import React from 'react';
// ...other code
export default React.memo(Child);      

React.memo(Comp[, fn])

用于減少子元件的渲染

React.memo 是一個高階元件(參數為元件,傳回的是新元件的函數即為高階元件)

對外部來說,React.memo 會檢查道具的變更,隻有當核心的道具發生變化時元件才會重新成型,紐扣我們再點選父元件,子就不會膨脹了。

React.memo 對複雜對象做淺層對比,可以通過過程第二個參數來控制對比

第二個參數為一個增強渲染細節的函數。

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

useMemo(fn[, DependentArray])

用于每次元件重新渲染時重複進行複雜的計算,參數為一個函數和依賴項數組,傳回函數和函數的調用結果。

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

useMemo作用類似于VUE的計算(計算屬性),不同之處在于需要手動傳入依賴項,依賴當項變更時會重新調用傳入的函數,傳回計算值。

依賴項為空數組時則直接傳回上次的計算結果。

不依賴項時,每次元件重新整理都會重新計算,應該在代碼能正常運作的情況下将其作為一種優化政策政策。

修改下我們的例子,請注意這裡包裹了子元件,保證測試時子元件重重渲染受重球的道具變化的影響。

// Parent.jsx
import React, { useState, useMemo } from 'react';
import Child from '../Child';


function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [otherCount, setOtherCount] = useState(0);
  console.log('父組件重新渲染--------------');


  // 一個複雜的計算
  const computedFn = (a, b) => {
    console.log('----重新執行了計算----');
    return a + b;
  };


  const computedValue = useMemo(() => {
    return computedFn(parentCount, 1);
  }, [parentCount]);


  return (
    <div style={{ background: 'lightseagreen' }}>
      <Child parentCount={parentCount} computedValue={computedValue} />
      <button type="button" onClick={() => { setParentCount(parentCount + 1); }}>父元件 +1</button>
      <button type="button" onClick={() => { setOtherCount(otherCount + 1); }}>父元件 otherCount+1</button>
    </div>
  );
}      

點選第一個按鈕,依賴項更改,輸出重執行了計算,點選第二個按鈕,更改更改的不是計算值,因為依賴項,是以不會重計算,元件也不會重渲染。

useCallback(fn[, DependentArray])

需要傳遞給子元件的函數,連接配接子元件的重複渲染,參數為一個函數和任選的依賴項數組,傳回出入函數的記憶版本。

// Parent.jsx
import React, { useState } from 'react';
import Child from '../Child';


function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [otherCount, setOtherCount] = useState(0);
  console.log('父組件重新渲染--------------');


  const computedFn = () => {
    return parentCount + 1;
  };


  return (
    <div style={{ background: 'lightseagreen' }}>
      <Child parentCount={parentCount} computedFn={computedFn} />
      <button type="button" onClick={() => { setParentCount(parentCount + 1); }}>父元件 +1</button>
      <button type="button" onClick={() => { setOtherCount(otherCount + 1); }}>父元件 otherCount+1</button>
    </div>
  );
}


export default Parent;


// Child.jsx
import React from 'react';


function Child(props) {
  const { computedValue, computedFn } = props;
  console.log('------------子組件重新渲染');
  return (
    <div style={{ background: 'pink', margin: '50px 0' }}>
      <div>
        父元件傳入的計算結果:
        {computedValue}
      </div>
      <button type="button" onClick={computedFn}>子組件</button>
    </div>
  );
}


export default React.memo(Child);      

當點選第二個按鈕時,子元件就可以重新渲染

給computedFn 加上useCallBack

// Parent.jsx
import React, { useState, useCallback } from 'react';


// ...other code


  const computedFn = useCallback(() => {
    console.log(parentCount);
    return parentCount + 1;
  }, [parentCount]) ;


// ...other code


export default Parent;      

一次再點選父元件第二個按鈕子元件,子元件不會變大,使用Callback的依賴項沒有變化,傳回的是上一次渲染的函數,是以元件子元件的道具沒有變,元件不會重渲染。

需要是,被useCallback儲存的函數内部作用域也不會發生變化,是以,當項數組為注意的時候,依賴useCallback的函數的内部通過閉包取的元件内的要素值始終不變。

import React, { useState, useCallback } from 'react';
import Child from '../Child';


let a = 0;
function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [otherCount, setOtherCount] = useState(0);
  console.log('父組件重新渲染--------------');


  const computedFn = useCallback(() => {
    // 依賴項為空,這裡的列印值始終不變;
    // 因為元件state變化時會重新渲染整個元件,而這裡parentCount取的始終是第一次渲染版本的值
    console.log(parentCount); 
    // 這裡的列印值會實時更新,因為變量直接定義在元件外部,不受元件重新渲染影響
    console.log(a);
    return parentCount + 1;
  }, []) ;


  return (
    <div style={{ background: 'lightseagreen' }}>
      <Child parentCount={parentCount} computedFn={computedFn} />
      <button type="button" onClick={() => { setParentCount(parentCount + 1); a += 1; }}>父元件 +1</button>
      <button type="button" onClick={() => { setOtherCount(otherCount + 1); }}>父元件 otherCount+1</button>
    </div>
  );
}


export default Parent;      

因為useCallback目的是減少子元件重渲染,是以需要搭配子元件的shouldComponentUpdate或React.memo一起使用優化意義。

以上是依賴項更改不希望的情況,當依賴項更改的時候,使用回調的記憶效果就不好,可以使用引用依賴項解決。

function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();


  useEffect(() => {
    textRef.current = text; // 把它寫入 ref
  });


  const handleSubmit = useCallback(() => {
    // ref 對象在元件的整個生命周期内保持不變
    // 從 ref 讀取它,current的變更不會引起元件的重新渲染,而函數内部又能拿到正确的值
    const currentText = textRef.current; 
    alert(currentText);
  }, [textRef]);


  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}      
使用引用

看看官方介紹

const refContainer = useRef(initialValue);      
useRef 傳回一個附屬的 ref 對象,其 .current 被初始化為粒子的參數(initialValue)。傳回的 ref 對象在元件的整個生命周期内保持不變

可以了解為:使用引用建立的對象擁有目前的屬性,這個像個盒子,啥可能存,包括DOM節點;傳回的引用對象在元件的整個生命周期内保持不變,即存在目前的值元件模糊渲染影響,始終保持着一開始的引用;同時屬性的修改也不會觸發元件的渲染;這個屬性的最終效果是useRef的參數

看看官方例子

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂載到 DOM 上的文本輸入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}      

當把useRef建立的對象傳給DOM元素的ref屬性時,将動态DOM元素的引用存入目前屬性,這樣就可以通過ref對象直接操作DOM元素了。

學習更多技能

請點選下方公衆号