React Hooks 實現和由來以及解決的問題

與React類組件相比,React函數式組件究竟有何不一樣?html

通常的回答都是:前端

  1. 類組件比函數式組件多了更多的特性,好比 state,那若是有 Hooks 以後呢?
  2. 函數組件性能比類組件好,可是在現代瀏覽器中,閉包和類的原始性能只有在極端場景下才會有明顯的差異。react

    1. 性能主要取決於代碼的做用,而不是選擇函數式仍是類組件。儘管優化策略有差異,但性能差別能夠忽略不計。
    2. 參考官網:(https://zh-hans.reactjs.org/d...
    3. 參考做者github:(https://github.com/ryardley/h...

而下面會重點講述:React的函數式組件和類組件之間根本的區別: 在心智模型上。git

簡單的案例

函數式組件以來,它一直存在,可是常常被忽略:函數式組件捕獲了渲染所用的值。(Function components capture the rendered values.)github

思考這個組件:數組

function ProfilePage(props) {
  const showMessage = () => alert('你好 ' + props.user);

  const handleClick = () => setTimeout(showMessage, 3000);

  return <button onClick={handleClick}>Follow</button>
}

上述組件:若是 props.userDan,它會在三秒後顯示 你好 Dan瀏覽器

若是是類組件咱們怎麼寫?一個簡單的重構可能就象這樣:閉包

class ProfilePage extends React.Component {
  showMessage = () => alert('Followed ' + this.props.user);

  handleClick = () => setTimeout(this.showMessage, 3000);

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

一般咱們認爲,這兩個代碼片斷是等效的。人們常常在這兩種模式中自由的重構代碼,可是不多注意到它們的含義:併發

咱們經過 React 應用程序中的一個常見錯誤來講明其中的不一樣。函數

咱們添加一個父組件,用一個下拉框來更改傳遞給子組件(ProfilePage),的 props.user,實例地址:(https://codesandbox.io/s/pjqn...

按步驟完成如下操做:

  1. 點擊 其中某一個 Follow 按鈕。
  2. 在3秒內 切換 選中的帳號。
  3. 查看 彈出的文本。

這時會獲得一個奇怪的結果:

  • 當使用 函數式組件 實現的 ProfilePage, 當前帳號是 Dan 時點擊 Follow 按鈕,而後立馬切換當前帳號到 Sophie,彈出的文本將依舊是 'Followed Dan'
  • 當使用 類組件 實現的 ProfilePage, 彈出的文本將是 'Followed Sophie'

在這個例子中,函數組件是正確的。 若是我關注一我的,而後導航到另外一我的的帳號,個人組件不該該混淆我關注了誰。 ,而類組件的實現很明顯是錯誤的。

案例解析

因此爲何咱們的例子中類組件會有這樣的表現? 讓咱們仔細看看類組件中的 showMessage 方法:

showMessage = () => {
    alert('Followed ' + this.props.user);
  };

這個類方法從 this.props.user 中讀取數據。

  1. 在 React 中 Props 是 不可變(immutable)的,因此他們永遠不會改變。
  2. this 是並且永遠是 可變(mutable)的。**

這也是類組件 this 存在的意義:能在渲染方法以及生命週期方法中獲得最新的實例。

因此若是在請求已經發出的狀況下咱們的組件進行了從新渲染, this.props將會改變。 showMessage方法從一個"過於新"的 props中獲得了 user

從 this 中讀取數據的這種行爲,調用一個回調函數讀取 this.props 的 timeout 會讓 showMessage 回調並無與任何一個特定的渲染"綁定"在一塊兒,因此它"失去"了正確的 props。。

如何用類組件解決上述BUG?(假設函數式組件不存在)

咱們想要以某種方式"修復"擁有正確 props 的渲染與讀取這些 props 的 showMessage回調之間的聯繫。在某個地方 props被弄丟了。

方法一:在調用事件以前讀取 this.props,而後顯式地傳遞到timeout回調函數中:
class ProfilePage extends React.Component {
  showMessage = (user) => alert('Followed ' + user);

  handleClick = () => {
    const {user} = this.props;
    setTimeout(() => this.showMessage(user), 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Followbutton>;
  }
}

然而,這種方法使得代碼明顯變得更加冗長。若是咱們須要的不止是一個props 該怎麼辦? 若是咱們還須要訪問state 又該怎麼辦? 若是 showMessage 調用了另外一個方法,而後那個方法中讀取了 this.props.something 或者 this.state.something ,咱們又將遇到一樣的問題。而後咱們不得不將 this.propsthis.state以函數參數的形式在被 showMessage調用的每一個方法中一路傳遞下去。

這樣的作法破壞了類提供的工程學。同時這也很難讓人去記住傳遞的變量或者強制執行,這也是爲何人們老是在解決bugs。

這個問題能夠在任何一個將數據放入相似 this 這樣的可變對象中的UI庫中重現它(不只只存在 React 中)

方法二:若是咱們能利用JavaScript閉包的話問題將迎刃而解。*

若是你在一次特定的渲染中捕獲那一次渲染所用的props或者state,你會發現他們老是會保持一致,就如同你的預期那樣:

class ProfilePage extends React.Component {
  render() {
    const props = this.props;

    const showMessage = () => {
      alert('Followed ' + props.user);
    };

    const handleClick = () => {
      setTimeout(showMessage, 3000);
    };

    return <button onClick={handleClick}>Follow</button>;
  }
}

你在渲染的時候就已經"捕獲"了props:。這樣,在它內部的任何代碼(包括 showMessage)都保證能夠獲得這一次特定渲染所使用的props。

Hooks 的由來

可是:若是你在 render方法中定義各類函數,而不是使用class的方法,那麼使用類的意義在哪裏?

事實上,咱們能夠經過刪除類的"包裹"來簡化代碼:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

就像上面這樣, props仍舊被捕獲了 —— React將它們做爲參數傳遞。 不一樣於 thisprops 對象自己永遠不會被React改變。

當父組件使用不一樣的props來渲染 ProfilePage時,React會再次調用 ProfilePage函數。可是咱們點擊的事件處理函數,"屬於"具備本身的 user值的上一次渲染,而且 showMessage回調函數也能讀取到這個值。它們都保持無缺無損。

這就是爲何,在上面那個的函數式版本中,點擊關注帳號1,而後改變選擇爲帳號2,仍舊會彈出 'Followed 帳號1'

函數式組件捕獲了渲染所使用的值。

使用Hooks,一樣的原則也適用於state。 看這個例子:

function MessageThread() {
  const [message, setMessage] = useState('');

  const showMessage = () => {
    alert('You said: ' + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
  };

  return <>
    <input value={message} onChange={handleMessageChange} />
    <button onClick={handleSendClick}>Send</button>
  </>;
}

若是我發送一條特定的消息,組件不該該對實際發送的是哪條消息感到困惑。這個函數組件的 message變量捕獲了"屬於"返回了被瀏覽器調用的單擊處理函數的那一次渲染。因此當我點擊"發送"時 message被設置爲那一刻在input中輸入的內容。

讀取最新的狀態

所以咱們知道,在默認狀況下React中的函數會捕獲props和state。 可是若是咱們想要讀取並不屬於這一次特定渲染的,最新的props和state呢?若是咱們想要["從將來讀取他們"]呢?

在類中,你經過讀取 this.props或者 this.state來實現,由於 this自己時可變的。React改變了它。在函數式組件中,你也能夠擁有一個在全部的組件渲染幀中共享的可變變量。它被成爲"ref":

function MyComponent() {
  const ref = useRef(null);

}

可是,你必須本身管理它。

一個ref與一個實例字段扮演一樣的角色。這是進入可變的命令式的世界的後門。你可能熟悉'DOM refs',可是ref在概念上更爲普遍通用。它只是一個你能夠放東西進去的盒子。

甚至在視覺上, this.something就像是 something.current的一個鏡像。他們表明了一樣的概念。

默認狀況下,React不會在函數式組件中爲最新的props和state創造refs。在不少狀況下,你並不須要它們,而且分配它們將是一種浪費。可是,若是你願意,你能夠這樣手動地來追蹤這些值:

function MessageThread() {
  const [message, setMessage] = useState('');
  const latestMessage = useRef('');
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
    latestMessage.current = e.target.value;  };

若是咱們在 showMessage中讀取 message,咱們將獲得在咱們按下發送按鈕那一刻的信息。可是當咱們讀取 latestMessage.current,咱們將獲得最新的值 —— 即便咱們在按下發送按鈕後繼續輸入。

ref是一種"選擇退出"渲染一致性的方法,在某些狀況下會十分方便。

一般狀況下,你應該避免在渲染期間讀取或者設置refs,由於它們是可變得。咱們但願保持渲染的可預測性。 然而,若是咱們想要特定props或者state的最新值,那麼手動更新ref會有些煩人。咱們能夠經過使用一個effect來自動化實現它:

function MessageThread() {
  const [message, setMessage] = useState('');

  const latestMessage = useRef('');
  useEffect(() => {
    latestMessage.current = message;
  });
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };

咱們在一個effect 內部執行賦值操做以便讓ref的值只會在DOM被更新後纔會改變。這確保了咱們的變量突變不會破壞依賴於可中斷渲染的時間切片和 Suspense 等特性。

一般來講使用這樣的ref並非很是地必要。 捕獲props和state一般是更好的默認值。 然而,在處理相似於intervals和訂閱這樣的命令式API時,ref會十分便利。你能夠像這樣跟蹤 任何值 —— 一個prop,一個state變量,整個props對象,或者甚至一個函數。

這種模式對於優化來講也很方便 —— 例如當 useCallback自己常常改變時。然而,使用一個reducer 一般是一個更好的解決方式

閉包幫咱們解決了很難注意到的細微問題。一樣,它們也使得在併發模式下能更輕鬆地編寫可以正確運行的代碼。這是可行的,由於組件內部的邏輯在渲染它時捕獲幷包含了正確的props和state。

函數捕獲了他們的props和state —— 所以它們的標識也一樣重要。這不是一個bug,而是一個函數式組件的特性。例如,對於 useEffect或者 useCallback來講,函數不該該被排除在"依賴數組"以外。(正確的解決方案一般是使用上面說過的 useReducer或者 useRef

當咱們用函數來編寫大部分的React代碼時,咱們須要調整關於優化代碼什麼變量會隨着時間改變的認知與直覺。

到目前爲止,我發現的有關於hooks的最好的內心規則是"寫代碼時要認爲任何值均可以隨時更改"。

React函數老是捕獲他們的值 —— 如今咱們也知道這是爲何了。

文章參考:React做者 Dan Abramov 的github

最後

  1. 譯者寫了一個 React + Hooks 的 UI 庫,方便你們學習和使用, (https://github.com/zhongmeizh...
  2. 歡迎關注公衆號「前端進階課」認真學前端,一塊兒進階。

相關文章
相關標籤/搜索