React Hooks徹底上手指南

做者:RichLab 衍良javascript

編者按:這是一篇關於 React Hooks 的深度好文,閱讀 & 理解須要必定時間,你們能夠先收藏,再慢慢看。更多精彩互動歡迎移步 知乎

1 Why Hook?

1.1 從React組件設計理論提及

React以一種全新的編程範式定義了前端開發約束,它爲視圖開發帶來了一種全新的心智模型:html

  • React認爲,UI視圖是數據的一種視覺映射,即UI = F(DATA),這裏的F須要負責對輸入數據進行加工、並對數據的變動作出響應
  • 公式裏的F在React裏抽象成組件,React是以組件(Component-Based)爲粒度編排應用的,組件是代碼複用的最小單元
  • 在設計上,React採用props屬性來接收外部的數據,使用state屬性來管理組件自身產生的數據(狀態),而爲了實現(運行時)對數據變動作出響應須要,React採用基於類(Class)的組件設計
  • 除此以外,React認爲組件是有生命週期的,所以開創性地將生命週期的概念引入到了組件設計,從組件的create到destory提供了一系列的API供開發者使用

這就是React組件設計的理論基礎,咱們最熟悉的React組件通常長這樣:前端

// React基於Class設計組件
class MyConponent extends React.Component {
  // 組件自身產生的數據
  state = {
    counts: 0
  }

  // 響應數據變動
  clickHandle = () => {
    this.setState({ counts: this.state.counts++ });
    if (this.props.onClick) this.props.onClick();
  }

  // lifecycle API
  componentWillUnmount() {
    console.log('Will mouned!');
  }

    // lifecycle API
  componentDidMount() {
    console.log('Did mouned!');
  }

  // 接收外來數據(或加工處理),並編排數據在視覺上的呈現
  render(props) {
    return (
      <>
        <div>Input content: {props.content}, btn click counts: {this.state.counts}</div>
        <button onClick={this.clickHandle}>Add</button>
      </>
    );
  }
}
複製代碼

1.2 Class Component的問題

1.2.1 組件複用困局

組件並非單純的信息孤島,組件之間是可能會產生聯繫的,一方面是數據的共享,另外一個是功能的複用:java

  • 對於組件之間的數據共享問題,React官方採用單向數據流(Flux)來解決
  • 對於(有狀態)組件的複用,React團隊給出過許多的方案,早期使用CreateClass + Mixins,在使用Class Component取代CreateClass以後又設計了Render PropsHigher Order Component,直到再後來的Function Component+ Hooks設計,React團隊對於組件複用的探索一直沒有中止

HOC使用(老生常談)的問題:react

  • 嵌套地獄,每一次HOC調用都會產生一個組件實例
  • 可使用類裝飾器緩解組件嵌套帶來的可維護性問題,但裝飾器本質上仍是HOC
  • 包裹太多層級以後,可能會帶來props屬性的覆蓋問題

Render Props:git

  • 數據流向更直觀了,子孫組件能夠很明確地看到數據來源
  • 但本質上Render Props是基於閉包實現的,大量地用於組件的複用將不可避免地引入了callback hell問題
  • 丟失了組件的上下文,所以沒有this.props屬性,不能像HOC那樣訪問this.props.children

1.2.2 Javascript Class的缺陷

一、this的指向(語言缺陷)github

class People extends Component {
  state = {
    name: 'dm',
    age: 18,
  }

  handleClick(e) {
    // 報錯!
    console.log(this.state);
  }

  render() {
    const { name, age } = this.state;
    return (<div onClick={this.handleClick}>My name is {name}, i am {age} years old.</div>);
  }
}
複製代碼

createClass不須要處理this的指向,到了Class Component稍微不慎就會出現因this的指向報錯。算法

二、編譯size(還有性能)問題:編程

// Class Component
class App extends Component {
  state = {
    count: 0
  }

  componentDidMount() {
    console.log('Did mount!');
  }

  increaseCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  decreaseCount = () => {
    this.setState({ count: this.state.count - 1 });
  }

  render() {
    return (
      <>
        <h1>Counter</h1>
        <div>Current count: {this.state.count}</div>
        <p>
          <button onClick={this.increaseCount}>Increase</button>
          <button onClick={this.decreaseCount}>Decrease</button>
        </p>
      </>
    );
  }
}

// Function Component
function App() {
  const [ count, setCount ] = useState(0);
  const increaseCount = () => setCount(count + 1);
  const decreaseCount = () => setCount(count - 1);

  useEffect(() => {
    console.log('Did mount!');
  }, []);

  return (
    <>
      <h1>Counter</h1>
      <div>Current count: {count}</div>
      <p>
        <button onClick={increaseCount}>Increase</button>
        <button onClick={decreaseCount}>Decrease</button>
      </p>
    </>
  );
}
複製代碼

Class Component編譯結果(Webpack):redux

var App_App = function (_Component) {
  Object(inherits["a"])(App, _Component);

  function App() {
    var _getPrototypeOf2;
    var _this;
    Object(classCallCheck["a"])(this, App);
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    _this = Object(possibleConstructorReturn["a"])(this, (_getPrototypeOf2 = Object(getPrototypeOf["a"])(App)).call.apply(_getPrototypeOf2, [this].concat(args)));
    _this.state = {
      count: 0
    };
    _this.increaseCount = function () {
      _this.setState({
        count: _this.state.count + 1
      });
    };
    _this.decreaseCount = function () {
      _this.setState({
        count: _this.state.count - 1
      });
    };
    return _this;
  }
  Object(createClass["a"])(App, [{
    key: "componentDidMount",
    value: function componentDidMount() {
      console.log('Did mount!');
    }
  }, {
    key: "render",
    value: function render() {
      return react_default.a.createElement(/*...*/);
    }
  }]);
  return App;
}(react["Component"]);
複製代碼

Function Component編譯結果(Webpack):

function App() {
  var _useState = Object(react["useState"])(0),
    _useState2 = Object(slicedToArray["a" /* default */ ])(_useState, 2),
    count = _useState2[0],
    setCount = _useState2[1];
  var increaseCount = function increaseCount() {
    return setCount(count + 1);
  };
  var decreaseCount = function decreaseCount() {
    return setCount(count - 1);
  };
  Object(react["useEffect"])(function () {
    console.log('Did mount!');
  }, []);
  return react_default.a.createElement();
}
複製代碼
  • Javascript實現的類自己比較雞肋,沒有相似Java/C++多繼承的概念,類的邏輯複用是個問題
  • Class Component在React內部是當作Javascript Function類來處理的
  • Function Component編譯後就是一個普通的function,function對js引擎是友好的
🤔問題:React是如何識別純函數組件和類組件的?

1.3 Function Component缺失的功能

不是全部組件都須要處理生命週期,在React發佈之初Function Component被設計了出來,用於簡化只有render時Class Component的寫法。

  • Function Component是純函數,利於組件複用和測試
  • Function Component的問題是隻是單純地接收props、綁定事件、返回jsx,自己是無狀態的組件,依賴props傳入的handle來響應數據(狀態)的變動,因此Function Component不能脫離Class Comnent來存在!
function Child(props) {
  const handleClick = () => {
    this.props.setCounts(this.props.counts);
  };

  // UI的變動只能經過Parent Component更新props來作到!!
  return (
    <>
      <div>{this.props.counts}</div>
      <button onClick={handleClick}>increase counts</button>
    </>
  );
}

class Parent extends Component() {
  // 狀態管理仍是得依賴Class Component
  counts = 0

  render () {
    const counts = this.state.counts;
    return (
      <>
        <div>sth...</div>
        <Child counts={counts} setCounts={(x) => this.setState({counts: counts++})} />
      </>
    );
  }
}
複製代碼

因此,Function Comonent是否能脫離Class Component獨立存在,關鍵在於讓Function Comonent自身具有狀態處理能力,即在組件首次render以後,「組件自身可以經過某種機制再觸發狀態的變動而且引發re-render」,而這種「機制」就是Hooks!

Hooks的出現彌補了Function Component相對於Class Component的不足,讓Function Component取代Class Component成爲可能。

1.4 Function Component + Hooks組合

一、功能相對獨立、和render無關的部分,能夠直接抽離到hook實現,好比請求庫、登陸態、用戶核身、埋點等等,理論上裝飾器均可以改用hook實現(如react-use,提供了大量從UI、動畫、事件等經常使用功能的hook實現)。

case:Popup組件依賴視窗寬度適配自身顯示寬度、相冊組件依賴視窗寬度作單/多欄佈局適配

🤔:請自行腦補使用Class Component來如何實現
function useWinSize() {
  const html = document.documentElement;
  const [ size, setSize ] = useState({ width: html.clientWidth, height: html.clientHeight });

  useEffect(() => {
    const onSize = e => {
      setSize({ width: html.clientWidth, height: html.clientHeight });
    };

    window.addEventListener('resize', onSize);

    return () => {
      window.removeEventListener('resize', onSize);
    };
  }, [ html ]);

  return size;
}

// 依賴win寬度,適配圖片佈局
function Article(props) {
  const { width } = useWinSize();
  const cls = `layout-${width >= 540 ? 'muti' : 'single'}`;

  return (
    <>
      <article>{props.content}<article>
      <div className={cls}>recommended thumb list</div>
    </>
  );
}

// 彈層寬度根據win寬高作適配
function Popup(props) {
  const { width, height } = useWinSize();
  const style = {
    width: width - 200,
    height: height - 300,
  };
  return (<div style={style}>{props.content}</div>);
}
複製代碼

二、有render相關的也能夠對UI和功能(狀態)作分離,將功能放到hook實現,將狀態和UI分離

case:表單驗證

function App() {
  const { waiting, errText, name, onChange } = useName();
  const handleSubmit = e => {
    console.log(`current name: ${name}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <>
        Name: <input onChange={onChange} />
        <span>{waiting ? "waiting..." : errText || ""}</span>
      </>
      <p>
        <button>submit</button>
      </p>
    </form>
  );
}
複製代碼

2 Hooks的實現與使用

2.1 useState

useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>]
複製代碼

做用:返回一個狀態以及能修改這個狀態的setter,在其餘語言稱爲元組(tuple),一旦mount以後只能經過這個setter修改這個狀態。

思考🤔:useState爲啥不返回object而是返回tuple?
useState函數申明
  • 使用了Hooks API的函數組件,返回的setter能夠改變組件的狀態,而且引發組件re-render
  • 和通常意義上的hook(鉤子)不同,這裏的hook能夠屢次調用且產生不一樣的效果,且hook隨Fiber Node一塊兒生滅

2.1.1 爲何只能在Function Component裏調用Hooks API?

Hooks API的默認實現:

function throwInvalidHookError() {
  invariant(false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.');
}

var ContextOnlyDispatcher = {
    ...
  useEffect: throwInvalidHookError,
  useState: throwInvalidHookError,
  ...
};
複製代碼

當在Function Component調用Hook:

function renderWithHooks(current, workInProgress, Component, props, refOrContext, nextRenderExpirationTime) {
  currentlyRenderingFiber$1 = workInProgress; // 指針指向當前正在render的fiber節點
  ....
  if (nextCurrentHook !== null) {
    // 數據更新
    ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
  } else {
    // 首次render
    ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
  }
}

/// hook api的實現
HooksDispatcherOnMountInDEV = {
    ...
  useState: function (initialState) {
    currentHookNameInDev = 'useState';
    ...
    return mountState(initialState);
  },
};
複製代碼

2.1.2 爲何必須在函數組件頂部做用域調用Hooks API?

在類組件中,state就是一個對象,對應FiberNode的memoizedState屬性,在類組件中當調用setState()時更新memoizedState便可。可是在函數組件中,memoizedState被設計成一個鏈表(Hook對象):

// Hook類型定義
type Hook = {
  memoizedState: any, // 存儲最新的state
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null, // 更新隊列
  next: Hook | null, // 下一個hook
}

// 定義一次更新
type Update<S, A> = {
  ...
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null, // 待更新狀態值
  next: Update<S, A> | null,
  ...
};

// 待更新隊列定義
type UpdateQueue<S, A> = {
  last: Update<S, A> | null, // 最後一次更新操做
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null, // 最新處理處理state的reducer
  lastRenderedState: S | null, // 最新渲染後狀態
};
複製代碼

示例:

function App() {
  const [ n1, setN1 ] = useState(1);
  const [ n2, setN2 ] = useState(2);

  // if (sth) {
  //    const [ n4, setN4 ] = useState(4);
  // } else {
  //    const [ n5, setN5 ] = useState(5);
  // }

  const [ n3, setN3 ] = useState(3);
}
複製代碼

Hook存儲(鏈表)結構:

Fiber(Hook)鏈表結構
  • Hook API調用會產生一個對應的Hook實例(並追加到Hooks鏈),可是返回給組件的是state和對應的setter,re-render時框架並不知道這個setter對應哪一個Hooks實例(除非用HashMap來存儲Hooks,但這就要求調用的時候把相應的key傳給React,會增長Hooks使用的複雜度)。
  • re-render時會從第一行代碼開始從新執行整個組件,即會按順序執行整個Hooks鏈,若是re-render時sth不知足,則會執行useState(5)分支,相反useState(4)則不會執行到,致使useState(5)返回的值實際上是4,由於首次render以後,只能經過useState返回的dispatch修改對應Hook的memoizedState,所以必需要保證Hooks的順序不變,因此不能在分支調用Hooks,只有在頂層調用才能保證各個Hooks的執行順序!

2.1.3 useState hook如何更新數據?

useState() mount階段(部分)源碼實現:

// useState() 首次render時執行mountState
function mountState(initialState) {
  // 從當前Fiber生成一個新的hook對象,將此hook掛載到Fiber的hook鏈尾,並返回這個hook
  var hook = mountWorkInProgressHook();

  hook.memoizedState = hook.baseState = initialState;

  var queue = hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: (state, action) => isFn(state) ? action(state) : action,
    lastRenderedState: initialState
  };
  // currentlyRenderingFiber$1保存當前正在渲染的Fiber節點
  // 將返回的dispatch和調用hook的節點創建起了鏈接,同時在dispatch裏邊能夠訪問queue對象
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

//// 功能至關於setState!
function dispatchAction(fiber, queue, action) {
  ...
  var update = {
    action, // 接受普通值,也能夠是函數
    next: null,
  };
  var last = queue.last;

  if (last === null) {
    update.next = update;
  } else {
    last.next = update;
  }

  // 略去計算update的state過程
  queue.last = update;
  ...
  // 觸發React的更新調度,scheduleWork是schedule階段的起點
  scheduleWork(fiber, expirationTime);
}
複製代碼
  • dispatchAction函數是更新state的關鍵,它會生成一個update掛載到Hooks隊列上面,並提交一個React更新調度,後續的工做和類組件一致。
  • 理論上能夠同時調用屢次dispatch,但只有最後一次會生效(queue的last指針指向最後一次update的state)
  • 注意useState更新數據和setState不一樣的是,前者會與old state作merge,咱們只需把更改的部分傳進去,可是useState則是直接覆蓋!
schedule階段介於reconcile和commit階段之間,schedule的起點方法是scheduleWork。 ReactDOM.render, setState,forceUpdate, React Hooks的dispatchAction都要通過scheduleWork。 Ref: zhuanlan.zhihu.com/p/54042084

update階段(state改變、父組件re-render等都會引發組件狀態更新)useState()更新狀態:

function updateState(initialState) {
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;
  var newState;
  var update;

  if (numberOfReRenders > 0) {
    // 組件本身re-render
    newState = hook.memoizedState;
    // renderPhaseUpdates是一個全局變量,是一個的HashMap結構:HashMap<(Queue: Update)>
    update = renderPhaseUpdates.get(queue);
  } else {
    // update
    newState = hook.baseState;
    update = hook.baseUpdate || queue.last;
  }

  do {
    newState = update.action; // action多是函數,這裏略去了細節
    update = update.next;
  } while(update !== null)

  hook.memoizedState = newState;
  return [hook.memoizedState, queue.dispatch];
}
複製代碼
  • React會依次執行hook對象上的整個update queue以獲取最新的state,因此useState()返回的tuple[0]始終會是最新的state!
  • 能夠看到,在update階段,initialState根本沒有用到的!

2.1.4 useState hook更新過程

function App() {
  const [n1, setN1] = useState(1);
  const [n2, setN2] = useState(2);
  const [n3, setN3] = useState(3);

  useEffect(() => {
    setN1(10);
    setN1(100);
  }, []);

  return (<button onClick={() => setN2(20)}>click</button>);
}
複製代碼

圖解更新過程:

useState更新過程
  • setState返回的setter執行會致使re-render
  • 框架內部會對屢次setter操做進行合併(循環執行傳入的setter,目的是保證useState拿到最新的狀態)

2.2 useEffect

useEffect(effect: React.EffectCallback, deps?: ReadonlyArray<any> | undefined)
複製代碼

做用:處理函數組件中的反作用,如異步操做、延遲操做等,能夠替代Class Component的componentDidMountcomponentDidUpdatecomponentWillUnmount等生命週期。

2.2.1 useEffect實現剖析

HooksDispatcherOnMountInDEV = {
    useEffect: function() {
    currentHookNameInDev = 'useEffect';
    ...
    return mountEffectImpl(Update | Passive, UnmountPassive | MountPassive, create, deps);
  },
};

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  return hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}

function pushEffect(tag, create, destroy, deps) {
  var effect = {
    tag: tag,
    create: create, // 存儲useEffect傳入的callback
    destroy: destroy, // 存儲useEffect傳入的callback的返回函數,用於effect清理
    deps: deps,
    next: null
  };
  .....
  componentUpdateQueue = createFunctionComponentUpdateQueue();
  componentUpdateQueue.lastEffect = effect.next = effect;
  ....
  return effect;
}

function renderWithHooks() {
    ....
  currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
  ....
}
複製代碼
  • 與useState傳入的是具體state不一樣,useEffect傳入的是一個callback函數,與useState最大的不一樣是執行時機,useEffect callback是在組件被渲染爲真實DOM後執行(因此能夠用於DOM操做)
  • useEffect調用也會在當前Fiber節點的Hooks鏈追加一個hook並返回,它的memoizedState存放一個effect對象,effect對象最終會被掛載到Fiber節點的updateQueue隊列(當Fiber節點都渲染到頁面上後,就會開始執行Fiber節點中的updateQueue中所保存的函數)

2.2.2 deps參數很重要

下面一段很常見的代碼,🤔有什麼問題?運行demo

// 用Hook寫
function App() {
  const [data, setData] = useState('');

  useEffect(() => {
    setTimeout(() => {
      setData(`current data: ${Date.now()}`);
    }, 3000);
  });

  return <div>{data}</div>;
}
// 等價代碼
class App extends Component {
  state = {data = ''}

  componentDidMount() {
    setTimeout(() => {
      this.setState({ data: `current data: ${Date.now()}` });
    }, 3000);
  }

  render() {
    return <div>{this.state.data}</div>;
  }
}
複製代碼
  • 組件re-render時,函數組件是從新執行整個函數,其中也包括全部「註冊」過的hooks,默認狀況下useEffect callback也會被從新執行!
  • useEffect能夠接受第二個參數deps,用於在re-render時判斷是否從新執行callback,因此deps必需要按照實際依賴傳入,不能少傳也不要多傳!
  • deps數組項必須是mutable的,好比不能也沒必要傳useRef、dispatch等進去
  • deps的比較實際上是淺比較(參閱源碼),傳入對象、函數進去是無心義
  • 做爲最佳實踐,使用useEffect時請儘量都傳deps(不傳入deps的場景筆者暫時沒找到)

2.2.3 清理反作用

Hook接受useEffect傳入的callback返回一個函數,在Fiber的清理階段將會執行這個函數,從而達到清理effect的效果:

function App() {
  useEffect(() => {
    const timer = setTimeout(() => {
        console.log('print log after 1s!');
    }, 1000);
    window.addEventListener('load', loadHandle);

    return () => window.removeEventListener('load', loadHandle); // 執行清理
  }, []);
}

// 同等實現
class App extends Component {
  componentDidMount() {
    const timer = setTimeout(() => {
        console.log('print log after 1s!');
    }, 1000);
    window.addEventListener('load', loadHandle);
  }

  componentDidUnmount() {
    window.removeEventListener('load', loadHandle);
  }
}
複製代碼

2.3 useContext

對於組件之間的狀態共享,在類組件裏邊官方提供了Context相關的API:

  • 使用React.createContext API建立Context,因爲支持在組件外部調用,所以能夠實現狀態共享
  • 使用Context.Provider API在上層組件掛載狀態
  • 使用Context.Consumer API爲具體的組件提供狀態或者經過contextType屬性指定組件對Context的引用

在消費context提供的狀態時必需要使用contextType屬性指定Context引用或者用<Context.Consumer>包裹組件,在使用起來很不方便(參見React Context官方示例)。

React團隊爲函數組件提供了useContext Hook API,用於在函數組件內部獲取Context存儲的狀態:

useContext<T>(Context: ReactContext<T>, unstable_observedBits: void | number | boolean): T
複製代碼

useContext的實現比較簡單,只是讀取掛載在context對象上的_currentValue值並返回:

function useContext(content, observedBits) {
  // 處理observedBits,暫時
  // 只有在React Native裏邊isPrimaryRenderer纔會是false
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
複製代碼
理解useContext的實現,首先要對Context源碼實現有所瞭解,推薦《 React 源碼系列 | React Context 詳解

useContext極大地簡化了消費Context的過程,爲組件之間狀態共享提供了一種可能,事實上,社區目前基於Hooks的狀態管理方案很大一部分是基於useContext來實現的(另外一種是useState),關於狀態管理方案的探索咱們放在後面的文章介紹。

2.4 useReducer

useReducer<S, I, A>(reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>]
複製代碼

做用:用於管理複雜的數據結構(useState通常用於管理扁平結構的狀態),基本實現了redux的核心功能,事實上,基於Hooks Api能夠很容易地實現一個useReducer Hook:

const useReducer = (reducer, initialArg, init) => {
  const [state, setState] = useState(
    init ? () => init(initialArg) : initialArg,
  );
  const dispatch = useCallback(
    action => setState(prev => reducer(prev, action)),
    [reducer],
  );
  return useMemo(() => [state, dispatch], [state, dispatch]);
};
複製代碼

reducer提供了一種能夠在組件外從新編排state的能力,而useReducer返回的dispatch對象又是「性能安全的」,能夠直接放心地傳遞給子組件而不會引發子組件re-render。

function reducer(state, action) {
  // 這裏可以拿到組件的所有state!!
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + state.step,
      };
    ...
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, {count: initialCount, step: 10});

  return (
    <>
      <div>{state.count}</div>
      // redux like diaptch
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <ChildComponent dispatch={dispatch} />
    </>
  );
}
複製代碼

2.5 性能優化(Memoization)相關Hooks API

2.5.1 useCallback

useCallback<T>(callback: T, deps: Array<mixed> | void | null): T
複製代碼

因爲javascript函數的特殊性,當函數簽名被做爲deps傳入useEffect時,仍是會引發re-render(即便函數體沒有改變),這種現象在類組件裏邊也存在:

// 當Parent組件re-render時,Child組件也會re-render
class Parent extends Component {
  render() {
    const someFn = () => {}; // re-render時,someFn函數會從新實例化

    return (
      <>
        <Child someFn={someFn} />
        <Other />
      </>
    );
  }
}

class Child extends Component {
  componentShouldUpdate(prevProps, nextProps) {
    return prevProps.someFn !== nextProps.someFn; // 函數比較將永遠返回false
  }
}
複製代碼

Function Component(查看demo):

function App() {
  const [count, setCount] = useState(0);
  const [list, setList] = useState([]);
  const fetchData = async () => {
    setTimeout(() => {
      setList(initList);
    }, 3000);
  };

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return (
    <>
      <div>click {count} times</div>
      <button onClick={() => setCount(count + 1)}>Add count</button>
      <List list={list} />
    </>
  );
}
複製代碼

解決方案:

  • 將函數移到組件外部(缺點是沒法讀取組件的狀態了)
  • 條件容許的話,把函數體移到useEffect內部
  • 若是函數的調用不止是useEffect內部(如須要傳遞給子組件),可使用useCallback API包裹函數,useCallback的本質是對函數進行依賴分析,依賴變動時才從新執行

2.5.2 useMemo & memo

useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T
複製代碼

useMemo用於緩存一些耗時的計算結果,只有當依賴參數改變時才從新執行計算:

function App(props) {
  const start = props.start;
  const list = props.list;
  const fibValue = useMemo(() => fibonacci(start), [start]); // 緩存耗時操做
  const MemoList = useMemo(() => <List list={list} />, [list]);

  return (
    <>
      <div>Do some expensive calculation: {fibValue}</div>
      {MemoList}
      <Other />
    </>
  );
}
複製代碼
簡單理解: useCallback(fn, deps) === useMemo(() => fn, deps)

在函數組件中,React提供了一個和類組件中和PureComponent相同功能的API React.memo,會在自身re-render時,對每個 props 項進行淺對比,若是引用沒有變化,就不會觸發重渲染。

// 只有列表項改變時組件纔會re-render
const MemoList = React.memo(({ list }) => {
  return (
    <ul>
      {list.map(item => (
        <li key={item.id}>{item.content}</li>
      ))}
    </ul>
  );
});
複製代碼

相比React.memouseMemo在組件內部調用,能夠訪問組件的props和state,因此它擁有更細粒度的依賴控制。

2.6 useRef

關於useRef其實官方文檔已經說得很詳細了,useRef Hook返回一個ref對象的可變引用,但useRef的用途比ref更普遍,它能夠存儲任意javascript值而不只僅是DOM引用。

useRef的實現比較簡單:

// mount階段
function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();
  var ref = { current: initialValue };
  {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

// update階段
function updateRef(initialValue) {
  var hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
複製代碼

useRef是比較特殊:

  • useRef是全部Hooks API裏邊惟一一個返回mutable數據的
  • 修改useRef值的惟一方法是修改其current的值,且值的變動不會引發re-render
  • 每一次組件render時useRef都返回固定不變的值,不具備下文所說的Capture Values特性

2.7 其餘Hooks API

  • useLayoutEffect:用法和useEffect一致,與useEffect的差異是執行時機,useLayoutEffect是在瀏覽器繪製節點以前執行(和componentDidMount以及componentDidUpdate執行時機相同)
  • useDebugValue:用於開發者工具調試
  • useImperativeHandle:配合forwardRef使用,用於自定義經過ref給父組件暴露的值

2.8 Capture Values特性

一、useState具備capture values,查看demo

二、useEffect具備capture values

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
    // 連續點擊三次button,頁面的title將依次改成一、二、3,而不是三、三、3
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
複製代碼

三、event handle具備capture values,查看demo

四、。。。全部的Hooks API都具備capture values特性,除了useRef,查看demo(setTimeout始終能拿到state最新值),state是Immutable的,ref是mutable的。

function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();
  var ref = { current: initialValue }; // ref就是一個普通object的引用,沒有閉包
  {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}
複製代碼

非useRef相關的Hooks API,本質上都造成了閉包,閉包有本身獨立的狀態,這就是Capture Values的本質

2.9 自定義組件:模擬一些經常使用的生命週期

  • componentDidMount:當deps爲空時,re-render時再也不執行callback
// mount結束,已經更新到DOM
const onMount = function useDidMount(effect) => {
    useEffect(effect, []);
};
複製代碼
  • componentDidUpdate
// layout結束,render DOM以前(會block rendering)
const onUpdate = function useUpdate(effect) => {
  useLayoutEffect(effect, []);
};
複製代碼
  • componentWillUnMount
const unMount = function useWillUnMount(effect, deps = []) => {
  useEffect(() => effect, deps);
};
複製代碼
  • shouldComponentUpdate(或React.PureComponent)
// 使用React.memo包裹組件
const MyComponent = React.memo(() => {
  return <Child prop={prop} />
}, [prop]);

// or
function A({ a, b }) {
  const B = useMemo(() => <B1 a={a} />, [a]);
  const C = useMemo(() => <C1 b={b} />, [b]);
  return (
    <>
      {B}
      {C}
    </>
  );
}
複製代碼

3 Hooks的問題

一、Hooks能解決組件功能複用,但沒有很好地解決JSX的複用問題,好比(1.4)表單驗證的case:

function App() {
  const { waiting, errText, name, onChange } = useName();
  // ...

  return (
    <form>
      <div>{name}</div>
      <input onChange={onChange} />
      {waiting && <div>waiting<div>}
      {errText && <div>{errText}<div>}
    </form>
  );
}
複製代碼

雖可以將用戶的輸入、校驗等邏輯封裝到useName hook,但DOM部分仍是有耦合,這不利於組件的複用,期待React團隊拿出有效的解決方案來。

二、React Hooks模糊了(或者說是拋棄了)生命週期的概念,但也帶來了更高門檻的學習心智(如Hooks生命週期的理解、Hooks Rules的理解、useEffect依賴項的判斷等),相比Vue3.0即將推出的Hooks有較高的使用門檻。

三、類擁有比函數更豐富的表達能力(OOP),React採用Hooks+Function Component(函數式)的方式實際上是一種無奈的選擇,試想一個掛載了十幾個方法或屬性的Class Component,用Function Component來寫如何組織代碼使得邏輯清晰?這背後實際上是函數式編程與面向對象編程兩種編程範式的權衡。

4 Ref


最後,感謝你認真閱讀這麼長的一篇文章~

螞蟻 RichLab 前端團隊」致力於與你共享高質量的技術文章

歡迎關注咱們的專欄,將文章分享給你的好友,共同成長 :-)

咱們團隊正在急招:互動圖形技術、前端/全棧開發、前端架構、算法、大數據開發等方向任選,指望層級 P6+~P7,團隊技術氛圍好,上升空間大,簡歷能夠直接砸給我哈 shudai.lyy@alibaba-inc.com

相關文章
相關標籤/搜索