React Hooks 學習之 useCallback實踐

@React Hook ---- useCallback詳解

React Hooks 學習之 useCallback實踐

前言:
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
本文主要介紹React中內置的Hook API — useCallback。
在開始閱讀之前,本文默認讀者已對React Hook有基礎瞭解,如果你剛開始接觸 Hook,那麼可能需要先查閱 Hook 概覽,再閱讀本文。

useCallback語法

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

該方法返回一個 memoized(記憶化)回調函數 ,什麼是記憶化函數呢?後文會就JavaScript中的Memoization做簡單的介紹。

useCallback 的第一個參數是一個函數用來執行一些操作和計算。第二個參數是一個數組,當這個數組裏面的值改變時 useCallback回調函數會重新執行,更新這個匿名函數裏面引用到的值,當數組裏面的值沒有改變時,會返回該回調函數的 memoized 版本。

這樣描述可能有點不太好理解,下面看一個例子:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: [1, 2, 3],
      name: '123'
    };
  }
  handleChangeNum = () => {
    this.setState({
      value: [4, 5, 6]
    });
  }
  render() {
    const {value, name} = this.state;
    return (
      <div className="App">
        <button onClick={this.handleChangeNum}>修改傳入的value值</button>
        <TestUseCallback value={value} name={name} />
      </div>
    );
  }
}

我們寫一個簡單的demo, 當點擊按鈕時,改變組件的value值,觀察當傳入子組件的value值發生改變時,以下三種情況下,useCallback函數的返回值。

第一種情況:
當依賴項數組爲空時

function TestUseCallback({value, name}) {
  const memoizedCallback = useCallback(
    () => {
      return value;
    },
    // 此處爲空
    [],
  );
  console.log('記憶中 value > ', memoizedCallback(), name);
  console.log('修改後 value > ', value, name);
  return (
    <div>
      <p>TestUseCallback</p>
    </div>
  );
}

在這裏插入圖片描述
可以看到,當依賴項數組爲空時,即使value值發生了改變, memoized函數依然返回緩存中的value值。

第二種情況:
當依賴項數組有值,但是這個值沒有發生改變

function TestUseCallback({value, name}) {
  const memoizedCallback = useCallback(
    () => {
      return value;
    },
    // 此處添加依賴數組,但是name值沒有發生變化
    [name],
  );
  console.log('記憶中 value > ', memoizedCallback(), name);
  console.log('修改後 value > ', value, name);
  return (
    <div>
      <p>TestUseCallback</p>
    </div>
  );
}

在這裏插入圖片描述
可以看到,當依賴數組中的值沒有發生改變時,memoized函數依然返回緩存中的value值。

第三種情況:
當依賴項數組有值,且這個值發生了變化

function TestUseCallback({value, name}) {
  const memoizedCallback = useCallback(
    () => {
      return value;
    },
    // 此處添加依賴數組,且value值發生改變
    [value],
  );
  console.log('記憶中 value > ', memoizedCallback(), name);
  console.log('修改後 value > ', value, name);
  return (
    <div>
      <p>TestUseCallback</p>
    </div>
  );
}

在這裏插入圖片描述
可以看到,當依賴項組織中的值發生改變之後,callback函數返回的不再是緩存中的數據,而是經過重新計算的最新value值。

通過上面簡單的demo用例,相信大家已經對useCallback函數的語法及使用有了基礎的認識。

那麼這個函數是怎麼實現的,以及我們應該什麼時候使用這個方法呢?下面我們先介紹一下什麼是Memoization,也就是javascript緩存。

Memoization

爲什麼會出現Memoization這個概念,首先我們渴望可以提高應用程序的性能,當一個複雜計算需要佔用大量的CPU時間的時候,我們希望可以把這個結果緩存下來,當下一次調用這個計算方法的時候可以直接從緩存中讀取數據,而不是重新進行一次耗時計算。
Memoization是JavaScript中的一種技術,通過緩存結果並在下一個操作中重新使用緩存來加速查找費時的操作,簡單來說就是將純函數的運算結果記錄下來,下一次調用時可以通過緩存來加速,是一種用空間換時間的性能優化技術。

技術實現:

function memoize(fn) {
    return function () {
        var args = Array.prototype.slice.call(arguments)
        fn.cache = fn.cache || {};
        return fn.cache[args] ? fn.cache[args] : (fn.cache[args] = fn.apply(this,args))
    }
}

我們可以看到這段代碼接收另外一個函數作爲參數並返回。
要使用此函數,我們調用memoize將要緩存的函數作爲參數傳遞。

使用方法:
比如著名的斐波那契系列(Fibonacci)

function fibonacci(num) {
    if (num == 1 || num == 2) {
        return 1
    }
    return fibonacci(num-1) + fibonacci(num-2)
}
const memFib = memoize(fibonacci)
console.log('profiling tests for fibonacci')
console.time("non-memoized call")
console.log(memFib(6))
console.timeEnd("non-memoized call")
console.time("memoized call")
console.log(memFib(6))
console.timeEnd("memoized call")

在這裏插入圖片描述
可以看到,一個很小的數字,通過緩存方法,運行效率就已經有了不小的提高。

參考文獻:https://juejin.im/post/5bf7c563e51d452d705fe8d1#heading-5

源碼參考:

export function useCallback<T>(
  callback: T,
  inputs: Array<mixed> | void | null,
): T {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
  workInProgressHook = createWorkInProgressHook();
  // 需要保存下來的依賴數組,用作下次取用的key
  const nextInputs =
    inputs !== undefined && inputs !== null ? inputs : [callback];

  const prevState = workInProgressHook.memoizedState; // 獲取之前緩存的值
  if (prevState !== null) {
    const prevInputs = prevState[1];
    if (areHookInputsEqual(nextInputs, prevInputs)) {
      // 如果依賴數組中的值不變,直接返回緩存中的值
      return prevState[0];
    }
  }
  // 如果值改變,將新的值存入緩存
  workInProgressHook.memoizedState = [callback, nextInputs];
  return callback;
}

使用場景

useCallback到底是用來做什麼的?應該什麼時候使用?不知道大家有沒有思考過這個問題,很多人似乎覺得不管什麼情況,只要用 useMemo 或者 useCallback 「包裹一下」,似乎就能使應用遠離性能的問題,其實這不是絕對的。
首先,我們需要知道 useCallback本身也有開銷。剛纔講緩存時有講到,useCallback會把某些值記錄下來,這是一個空間換時間的過程,會消耗計算機內存,並且會將依賴數組中的值取出來和上一次記錄的值進行比較,這需要消耗計算機的計算資源。因此,過度使用 useCallback 可能會影響程序的性能。

接下來我們就來研究一下useCallback的使用場景。

一 、useCallback的合理適用場景

1、有些計算開銷很大,我們就需要緩存它的返回值,避免每次 render 都去重新計算。
這種情況很好理解,我們引入緩存這個概念,就是用來解決這種大運算量的問題。

2、由於值的引用發生變化,導致下游組件重新渲染,我們也需要「記住」這個值。

我們都知道子組件的重新渲染取決於父組件傳遞的props的值是否發生變化,如果我們props的值是一個引用類型的數據(Array, Object, Function),而這個數據的指向一但發生變化,那麼就算這個數據的值實際上並沒有發生改變,子組件也會重新渲染。
舉例:

function Example() {
  const users = [1, 2, 3];

  return <ExpensiveComponent users={users} />
}

以上面代碼舉例,每當Example組件render的時候,users變量都會被重新賦值,儘管每次users裏面的數據都並沒有發生改變,但由於users是引用數據,每次渲染時,users的指向都會發生變化,引發ExpensiveComponent子組件的重新渲染。

如果我們想在重新渲染時保持值的引用不變,不用每次都觸發子組件的重新渲染,就可以用useCallback方法

function Example() {
  const users = useCallback(() => [1, 2, 3], [])();

  return <ExpensiveComponent users={users} />
}

這樣每次render的時候,users的值就不會發生變化,子組件就不會重新渲染。

另外,應用useMemo和useRef都可以達到類似的效果
const users = useMemo(() => [1, 2, 3], []);
const {current: users} = useRef([1, 2, 3]);

二、無需使用 useCallback 的場景

1、如果返回的值是原始值: string, boolean, null, undefined, number, symbol(不包括動態聲明的 Symbol),一般不需要使用 useCallback。
2、僅在組件內部用到的 object、array、函數等(沒有作爲 props 傳遞給子組件),且沒有用到其他 Hook 的依賴數組中,一般不需要使用 useCallback。

發佈了5 篇原創文章 · 獲贊 5 · 訪問量 624
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章