淺談 React Hooks(二)

上一篇文章中,咱們談到 Hooks 給 React 帶來的一些在開發體驗上的改變,若是你已經開始嘗試 React Hooks,也許你會跟我同樣碰到一個使人疑惑的地方,若是沒有的話,那就再好不過啦,我就權當作個記錄,以便他人之需。javascript

如何綁定事件?

咱們先以官方的例子開始:java

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
  );
}
複製代碼

看到onClick綁定的那個匿名函數了嗎?這樣寫的話,每次 render 的時候都會從新生成一個新的函數。這在以前可能不須要太在乎,由於咱們通常只是拿 Function Component 來實現一些展現型組件,在其之下不會有太多的子組件。可是若是咱們擁抱 Hooks 以後,那麼就不可控了。react

雖說在通常狀況下,這並不會形成太大的性能問題,並且 Function Component 自己的性能就要比 Class Component 更好一點,可是不免會碰到須要優化的時候,比方說在重構原來的 Class Component 的時候,其中有個子組件是個PureComponent,便會使子組件的這個優化失效 ,那麼怎麼解決呢?git

使用useCallbackuseMemo來保存函數的引用,避免重複生成新的函數github

function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    setCount(count => count + 1)
  }, []);
  
  // 或者用useMemo
  // const handleClick = useMemo(() => () => {setCount(count => count + 1)}, []);
   
  return (
    <div> <p>count: {count}</p> {/* Child爲PureComponent */} <Child callback={handleClick} /> </div> ) } 複製代碼

可見useCallback(fn, inputs)等同於useMemo(() => fn, inputs),那麼這兩個 Hook 具體是怎麼作到的呢?咱們能夠從源碼中一窺究竟,咱們以useCallback爲例(useMemo大致上都是同樣的,就返回值不一樣,後面會提到)。ide

首先,在第一次執行useCallback時,React內部會調用ReactFiberHooks中的mountCallback,以後再次執行時調用的都是updateCallback,具體代碼能夠看這裏:github.com/facebook/re…函數

咱們一點點來看,先看下mountCallback:post

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
複製代碼

發現核心在於mountWorkInProgressHook這個方法性能

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
複製代碼

代碼比較簡單,就不一一解釋了,從上面的代碼咱們能夠得知 Hooks 的本體:優化

const hook = {
  memoizedState: null,
  baseState: null,
  queue: null,
  baseUpdate: null,
  next: null,
}
複製代碼

咱們主要關注memoizedStatenextmemoizedState在不一樣的 Hook 中存放的值會有所不一樣,在useCallback中存的就是入參的值[callback, deps]next的值就是下一個 hook,也就是說 Hooks 其實就是一個單向鏈表,這也就解釋了爲何 Hooks 須要在頂層調用,不能在循環、條件語句、嵌套函數中使用,由於須要保證每次調用的順序一致。

再來看以後的updateCallback:

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 這個hook就是第一次mount的hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 因此這裏的memoizedState就是mount時候存着的[callback, deps]
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 比較兩次的deps,相同的話就直接返回以前存的callback,而不是新傳進來的callback
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
複製代碼

useMemo的實現與useCallback相似,大概看一下:

function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  
  // 與useCallback不一樣的地方就是memoizedState中存的是nextCreate執行以後的結果
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
    
  // 返回執行結果
  return nextValue;
}

function updateMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  
  // 這裏也同樣,存的是nextCreate執行以後的結果
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  
    // 返回執行結果
  return nextValue;
}
複製代碼

由以上代碼即可以看出useCallbackuseMemo在用法上的區別了。

除了這兩個方法之外,還能夠經過context來傳遞由useReducer生成的dispatch方法,來避免直接傳遞callback,由於dispatch是不變的。這個方法跟前面兩種有本質上的區別,它從源頭上就阻止了callback的傳遞,因此也就不會有前面提到的性能方面的顧慮,這也是官方推薦的方法,特別是組件樹很大的狀況下。因此上面的代碼若是經過這種方式來寫的話,就會是下面這樣,有點像Redux

import React, { useReducer, useContext } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    default:
      throw new Error();
  }
}

const TodosDispatch = React.createContext(null);

function Counter() {
  const [state, dispatch] = useReducer(reducer, {count: 0});

  return (
    <div> <p>count: {state.count}</p> <TodosDispatch.Provider value={dispatch}> <Child /> </TodosDispatch.Provider> </div> ) } function Child() { const dispatch = useContext(TodosDispatch); return ( <button onClick={() => dispatch({type: 'increment'})}> click </button> ) } 複製代碼

總結

  • 通常狀況下,事件綁定能夠直接經過箭頭函數處理,不會有明顯的性能問題,寫起來也方便。
  • 若有需求,能夠經過useCallbackuseMemo來優化。
  • 若是組件樹比較大,傳遞callback的層級可能會很深,能夠經過useReducer配合context來處理。

以上只是我我的的一些想法,若有不對之處,歡迎指正~~

原文連接:淺談 React Hooks(二)

相關文章
相關標籤/搜索