Function 與 Classes 組件的區別在哪?

React function 組件和 React classes 有什麼不一樣?javascript

之前,一個標準答案是說 classes 提供更多的功能(例如 state)。有了 Hooks,便不是這樣了。html

可能你聽過其中一個性能更好。哪個?許多這樣的性能基準都存在缺陷,因此我會當心地從中得出結論。性能主要取決於代碼而不是選擇一個 function 或者 一個 class。在咱們觀察中,即便優化策略有所不一樣,但性能的差距其實微乎其微。java

另外一方面咱們不推薦重寫你寫好的組件,除非你有其餘緣由且不介意成爲早期試驗者。Hooks 仍然很新(就像 2014 年的 React),而且一些「最佳實踐」還未寫進教程。react

React function 和 classes 是否存在本質上的區別?固然,它們 —— 在心智模型中。在這篇文章裏,我會看看它們之間的最大區別。這在2015年的 function components 中介紹過,但它常常被忽視了:git

Function 組件捕獲 render 後的值github

讓咱們來分析下這是什麼意思。編程


注意:這片文章不作 classes 或者 functions 的價值衡量,我只描述兩種編程模型在 React 中的區別。更多關於採用 functions 的問題,請參閱 Hooks 常見問題解答數組


思考這個組件:瀏覽器

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

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

  return (
    <button onClick={handleClick}>Follow</button>
  );
}
複製代碼

它展現一個帶有模擬網絡請求的 setTimeout 且以後會在確認彈窗中現實出來的按鈕。例如,若是 props.user'Dan',它會在三秒後顯示'Followed Dan',很是簡單。網絡

(請注意,上面例子中不管我是否使用箭頭仍是普通函數,function handleClick() 確定是同樣效果的。)

那咱們把它寫成 class 會怎麼樣呢?直接翻譯後可能看起來像這樣:

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

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

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

通常會認爲這兩段代碼是等效的,你們常常在這些模式中隨意的重構,而不會注意到它們的含義:

Spot the difference between two versions

可是,這兩段代碼略微不一樣。 好好看看它們,有看出不一樣了嗎?就我的而言,我花了好一會才發現。

前面有劇透,若是你想本身找到的話,這是一個在線demo。文章接下來的部分來分析這個差別及爲何會這樣。


在咱們繼續以前,我想強調下,我所描述的差別與 React Hooks 自身無關,上面的例子甚至不須要用 Hooks!

這徹底是關於 functions 和 classes 在 React 中的區別的,若是你打算在 React 應用中更經常使用 functions,你可能想去弄懂它。


咱們將經過 React 應用中常見的一個 bug 來講明這區別

使用當前的條目選擇器和以前兩個 ProfilePage 實現來打開這個 sandbox 例子 —— 每一個渲染一個 Follow 按鈕。

按照這種操做順序使用兩個按鈕:

  1. 點擊 其中一個按鈕。
  2. 在 3 秒中內改變選擇條目。
  3. 看下彈出的文本。

你會注意到一個特殊的區別:

  • 當爲 functionProfilePage 時,點擊 Follow Dan 的條目而後切換成 Sophie 的,仍然彈出 'Followed Dan'

  • 當爲 classProfilePage 時,它會彈出 'Followed Sophie'

Demonstration of the steps


這個例子中,第一種行爲是正確的。若是你關注一我的,而後切換到另一我的的條目,個人組件不該該困惑於我要關注的是誰。class 的實現明顯是個錯誤。


因此爲何咱們的 class 例子會以這種方式運行?

讓咱們仔細看看 class 中 showMessage 方法:

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

這個 class 方法讀取了 this.props.user,Props 在 React 中是不可變的。 可是,this ,且已經改變了

實際上,這就是在 class 中有 this 的目的,React 自己會隨着時間的推移而變異,以便你在能夠渲染和生命週期中獲取到新版本。

因此若是咱們組件在處於請求狀態時重渲染,this.props 會發生改變。 showMessage 方法從「太新」的 props 中獲取 user

這暴露了一個 UI 層性質上的有趣現象。若是咱們說 UI 在概念上是當前應用程序狀態的函數,則事件處理程序是渲染結果的一部分 —— 就像視覺輸出同樣。咱們的事件處理程序「屬於」具備特定 props 和 state 的特定 render。

可是,調度一個回掉讀取 this.props 的 timeout 會中斷該聯繫。咱們的 showMessage 回調沒有「綁定」到任何特定 render 上,所以它「丟失」了正確的 props,而讀取了 this 切斷這種聯繫。


能夠說 function 組件不存在這個問題。咱們要這麼解決這個問題?

咱們想以某種方式 「修復」 有正確 props 的 render 與獲取它們的 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}>Follow</button>;
  }
}
複製代碼

這樣可行。可是,這種方法使代碼明顯更加冗餘,且隨着時間推移容易出錯。若是咱們須要超過一個 prop 怎麼辦?若是咱們也須要獲取 state 怎麼辦?若是 showMessage 調用其餘方法,且這個方法讀取 this.props.somethingthis.state.something,咱們會再次遇到一樣的問題。因此咱們不得不將 this.propsthis.state 作爲參數傳給每一個調用了 showMessage 的方法。

這樣作一般會破壞一般由 class 提供的人體工程學,也難以記住或強制執行,這就是你們常常出現 bugs 的緣由。

相似的,把 alert 放入 handleClick 中也沒法解決這個難題。咱們但願以容許拆分更多方法的方式構造代碼,同時咱們還要讀取與該調用相關 render 的對應 props 和 state。這個問題甚至不是 React 獨有的 —— 你能夠在任何將數據放入像 this 可變對象的 UI 庫中重現它

或許,咱們能夠在 constructor 裏 bind 方法?

class ProfilePage extends React.Component {
  constructor(props) {
    super(props);
    this.showMessage = this.showMessage.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }

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

  handleClick() {
    setTimeout(this.showMessage, 3000);
  }

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

不, 這樣沒法修復任何東西。記住,這個問題是因爲咱們太遲去讀取 this.props 了 —— 不是咱們這麼用語法的問題!然而,這個問題若是咱們徹底依靠 JavaScript 閉包就能夠解決

一般會避開使用閉包,由於很知道隨着時間推移可能會發生變異的值。但在 React,props 和 state 是不能夠變的!(或者至少,這是一個強烈推薦。)這去除了閉包的一個殺手鐗。

這意味着若是你封鎖一個特定 render 的 props 或 state,你老是能夠獲取相同的它們:

class ProfilePage extends React.Component {
  render() {
    // 捕獲 props!
    const props = this.props;

    // 注意: 咱們在 *render 裏面*
    // 這不是 class 方法。
    const showMessage = () => {
      alert('Followed ' + props.user);
    };

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

    return <button onClick={handleClick}>Follow</button>;
  }
}
複製代碼

你在 render 時已經「捕獲」到 props 了

這樣,它內部的任何代碼(包括 showMessage)均可以保證看到這個特定 render 的 props,React 不會再「動咱們的奶酪」了。

咱們在裏邊添加多少個輔助方法均可以,而且它們全都使用被捕獲的 props 和 state,救回了閉包。


上面的例子沒有錯但看起來奇怪。若是在 render 中定義函數而不是使用 class 的方法,那還要 class 作什麼?

事實上,咱們能夠去掉 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 改變

若是在 function 定義時解構 props 就更明顯的:

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

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

  return (
    <button onClick={handleClick}>Follow</button>
  );
}
複製代碼

當父組件用不一樣 props 渲染 ProfilePage 時,React 會再次調用 ProfilePage 方法。但咱們點擊了的事件程序「屬於」具備本身的 user 值的上一個 render 和讀取它的 showMessage 回調,它們都無缺無損。

這就是爲何,在這個 demo 的 function 版本中,在 Sophie 的條目時點擊 Follow 以後切換成 Sunil 會彈出 'Followed Sophie'

這反應是正確的。(雖然你也可能想關注 Sunil!)


如今咱們明白了 functions 與 classes 在 React 中的最大不一樣了:

Function 組件捕獲 渲染後的值

使用 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>
    </>
  );
}
複製代碼

([這是一個 線上 demo。])

雖然這不是一個好的消息應用 UI,它實現了一樣的東西:若是我發送一個特定的信息,這個組件不該該困惑於發送哪一個消息。這個 function 組件的消息捕獲了 state 且「屬於」返回被瀏覽器點擊事件調用的 render。因此這個消息被設定爲當我點擊」發送「時 input 裏的值。


因此咱們知道 React 中的 functions 會默認捕獲 props 和 state。但若是咱們但願讀取的是最新的 props 或者 state,它們不屬於特定的 render 要怎麼辦?若是咱們想在將來裏讀取到它們怎麼辦?

在 classes 中,你能夠讀取 this.propsthis.state,由於 this 自己是可變的,React 會改變它。在 function 組件中,你也能夠有一個共享於全部組件 renders 的可變值,它叫作 「ref」:

function MyComponent() {
  const ref = useRef(null);
  // 你能夠讀寫 `ref.current`。
  // ...
}
複製代碼

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

ref 和實例字段扮演相同的角色,它是進入可變命令世界的逃脫倉。你可能熟悉 「DOM refs」,但這個原理要通俗的多,它只是一個你能夠往裏面放東西的箱子。

即使在視覺上,this.someting 看起來像 something.current 的鏡像。它們表明了相同的概念。

默認狀況下,在 function 組件中 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;
  };
複製代碼

若是咱們讀取 showMessagemessage,咱們會看到咱們第幾發送按鈕時的消息。但當咱們讀取 latestMessage.current,咱們獲取到的是最新的值 —— 即便咱們在按下發送按鈕後繼續輸入。

你能夠比較這兩個 demos 看看區別。ref 是一種「選擇退出」渲染一致的方法,在某些狀況下能夠很方便。

一般你應該避免在渲染期間讀取或設置 refs,由於它們是可變的。咱們想保持渲染的可預測性。可是,若是咱們想獲取到特定 prop 或 state 最新的值,手動更新 ref 會很麻煩。咱們能夠用 effect 自動化它:

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

  // 保持 track 是最新值
  const latestMessage = useRef('');
  useEffect(() => {
    latestMessage.current = message;
  });

  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };
複製代碼

(這是一個demo。)

咱們在 effect 裏面賦值來實現 ref 值只在 DOM 被更新時才改變。這確保咱們的變異不會破壞像 Time Slicing 和 Suspense 等中斷渲染的功能。

不多會像這樣去使用 ref,捕獲 props 或 state 默認下是更好的。然而,在處理像定時器和訂閱這樣的棘手地 APIs 時是很方便的。記住你能夠跟蹤任何這樣的值 —— prop、state 變量、整個 props 對象,甚至是一個 function。

這種模式也能夠用來作優化 —— 例如在 useCallback 標示頻繁改變時。可是,使用一個 reducer 一般是一個更好的解決方案。(這個會在之後的博客文章中寫!)


這片文章裏,咱們看到在 classes 中的廣泛破壞模式,及閉包是如何幫助咱們修復它的。可是,你可能注意到了當你試着經過指定依賴數組來優化 Hooks 時,你可能會遇到過期閉包帶來的 bugs。這意味着閉包是問題?我也不這麼認爲。

正如咱們以前所見,閉包確實幫助咱們修復了難以注意到的細微問題。一樣地,它們使編寫在併發模式下的代碼正常工做變得更簡單。這多是由於組件內部的邏輯在渲染後封鎖正確的 props 和 state。

在目前爲止看到的全部狀況中,「過期閉包」問題發生是因爲 「functions 不發生變化」 或 「props 老是相同」的錯誤假設。事實並不是如此,我但願這篇文章有助於澄清。

Functions 鎖住它們的 props 和 state —— 全部它們是什麼很重要。這不是一個 bug,而是一個 function 組件的特性。例如,Functions 不該該從 userEffectuseCallback 的「依賴數組」中被排除。(上面提到經常使用的適當修復無論是 useReducer 或是 useRef 的解決方案 —— 咱們很快會在文檔中說明如何在它們之間作選擇)

在咱們用 functions 寫大多數 React 代碼時,咱們須要適配咱們的關於 優化代碼什麼值會一直改變的狀況。

到目前爲止我用 hooks 找到的最好的心理規則是 「代碼的任何值彷佛能夠在任意時間改變」。

Functions 也不例外。這須要一些時間才能在 React 學習材料裏面變成廣泛的知識,從 class 心態過來的須要一些適應,但我但願這篇文章能夠幫助你用新的眼光看待它。

React functions 總會捕獲它們的值 —— 且如今咱們知道爲何了。

它們是一個徹底不一樣的神奇寶貝。

翻譯原文How Are Function Components Different from Classes?(2019-03-03)

相關文章
相關標籤/搜索