函數組件與類有什麼不一樣?

原文: How Are Function Components Different from Classes?javascript

原譯文: 函數組件與類有什麼不一樣?html

函數組件與類有什麼不一樣?

React函數組件與React類組件有何不一樣?java

有一段時間,規範的答案是: 類能夠訪問更多功能(如狀態)。有了Hooks,就再也不是這樣了。react

也許你據說其中一個在性能上會更好。那麼是哪個? 許多這樣的benchmarks是有缺陷的,因此我會當心地從中得出結論。性能主要取決於代碼在作什麼,而不是你選擇的是函數仍是類。在咱們的觀察中,雖然優化策略有點不一樣,但性能差別能夠忽略不計。git

在任何一種狀況下,咱們都不建議重寫現有組件,除非你有其餘緣由,而且你也不介意成爲早期實踐者。Hooks仍然是新的(就像2014年的React同樣),而且一些「最佳實踐」還沒有進入教程。github

這給咱們帶來了什麼? React函數和類之間有什麼根本的區別嗎?固然,在心智模型中存在。 在這篇文章中,我將看看它們之間的最大區別。 自從2015年引入函數組件以來,它就一直存在,但它常常被忽視:編程

函數組件捕獲渲染的值。數組

讓咱們來解釋下這意味着什麼。瀏覽器


注意: 這篇文章不是對類或函數的價值判斷。我只描述了React中這兩種編程模型之間的區別。有關更普遍地採用功能的問題,請參閱Hooks FAQ網絡


考慮這個組件:

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 ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

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

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

一般認爲這兩段代碼是等價的。人們常常在這些模式之間自由地重構,而不注意它們的含義:

image

可是,這兩個代碼片斷略有不一樣。 好好看看他們。你看到區別了嗎? 就我我的而言,我花了一段時間才明白這一點。

前面有劇透,若是你想本身弄明白,這裏有一個在線演示 本文的其他部分解釋了差別及其重要性。


在咱們繼續以前,我想強調一點,我所描述的差別與React Hooks自己無關。以上示例甚至沒有使用Hooks!

這都是React中函數和類之間的區別。若是你計劃在React應用程序中更頻繁地使用函數,則可能須要瞭解它。


咱們將經過React應用程序中常見的bug說明其差別。

打開此示例沙箱並使用選擇的當前配置文件和上面的兩個ProfilePage實現 -- 每一個都渲染一個Follow按鈕。

使用兩個按鈕嘗試此操做序列:

  1. 點擊 其中一個"follow"按鈕
  2. 在3秒以前 更改 所選的我的資料(筆:就是那個下拉框)。
  3. 查看 彈出的文字。

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

  • 使用上面的ProfilePage 函數 ,單擊Follow Dan的我的資料,而後導航到Sophie's仍然會彈框'Followed Dan'
  • 使用上面的ProfilePage ,他將會彈出'Followed Sophie'

image


在此示例中,第一個行爲是正確的行爲。若是我follow一我的而後導航到另外一我的的我的資料,個人組件不該該對我follow的人感到困惑。 類的實現顯然是錯誤的。

(你應該關注Sophie)


那麼爲何咱們的類示例會以這種方式運行?

讓咱們仔細看看咱們類中的showMessage方法:

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

這個類的方法從this.props.user讀取。Props在React中是不可變的,所以它們永遠不會改變。 然而,this是,而且一直是可變的。

實際上,這就是this在一個類中的所有的目的。React自己會隨着時間的推移而改變,以便你能夠在render和生命週期方法中讀取新版本。

所以,若是咱們的組件在請求處於運行狀態時從新呈現,則this.props將會更改。showMessage方法從「過新」的props中讀取user

這就暴露了一個關於用戶界面性質的有趣觀察。若是咱們說UI在概念上是當前應用程序狀態的函數, 那麼事件處理程序就是呈現結果的一部分——就像可視化輸出同樣。 咱們的事件處理程序「屬於」具備特定props和狀態的特定渲染。

可是,調用其回調讀取this.props的超時會中斷該關聯。咱們的showMessage回調沒有「綁定」到任何特定的渲染,所以它「失去」正確的props。從this讀取切斷了這種聯繫。


假設函數組件不存在。 咱們如何解決這個問題?

咱們但願以某種方式「修復」具備正確props的render和讀取它們的showMessage回調之間的鏈接。沿途的某個地方props丟失了。

一種方法是在事件早期讀取this.props,而後將它們顯式傳遞到超時完成處理程序:

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怎麼辦?若是咱們還須要訪問該狀態怎麼辦?若是showMessage調用另外一個方法,而且該方法讀取this.props.somethingthis.state.something,咱們將再次遇到徹底相同的問題。 因此咱們必須經過從showMessage調用的每一個方法將this.propsthis.state做爲參數傳遞。

這樣作會破壞一般由類提供的人體工程學。這也很難記住或執行, 這就是人們常常解決問題的緣由。

一樣,在handleClick中嵌入alert代碼並不能解決更大的問題。咱們但願以一種容許將代碼拆分爲更多方法的方式來構造代碼,同時還能夠讀取與這個調用相關的呈現所對應的props和狀態。這個問題甚至不是React獨有的 - 你能夠在任何UI庫中重現它,你只須要將數據放入可變的對象,好比this

也許,咱們能夠在構造函數中 綁定 方法?

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和狀態是不可改變的!(或者至少,這是一個強烈的推薦。)這就消除了閉包的主要障礙。

這意味着,若是結束某個特定渲染中的props或狀態,則始終能夠期望它們保持徹底相同:

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

    // Note: we are *inside render*.
    // These aren't class methods.
    const showMessage = () => {
      alert('Followed ' + props.user);
    };

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

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

你在渲染時「捕獲」了props:

image

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

而後咱們能夠在裏面添加任意數量的輔助函數,它們都會使用捕獲的props和狀態。 閉包來救場了!


上面的例子是正確的,但看起來很奇怪。若是在render中定義函數而不是使用類方法,那麼擁有一個類有什麼意義呢?

實際上,咱們能夠經過刪除它周圍類的「外殼」來簡化代碼:

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

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

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

就像上面同樣,props仍然被捕獲 - React將它們做爲參數傳遞。this不一樣,props對象自己永遠不會被React改變。

若是你在函數定義中對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值和讀取它的showMessage回調。它們都無缺無損。

這就是爲何,在這個演示的功能版本中,單擊關注Sophie的我的資料,而後將選擇更改成Sunil會彈出'Followed Sophie'

image

這個行爲是正確的。(雖然你可能也想關注Sunil!)


如今咱們瞭解React中函數和類之間的巨大差別:

函數組件捕獲呈現的值。

使用Hooks,一樣的原則也適用於狀態。 考慮這個例子:

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應用的UI,但它說明了一樣的觀點:若是我發送特定消息,組件不該該對實際發送的消息感到困惑。這個函數組件的message捕獲了「屬於」呈現的狀態,呈現返回了瀏覽器調用的單擊處理程序。所以,當我單擊"send"時,消息將設置爲輸入中的內容。


所以,默認狀況下,咱們知道React中的函數捕獲props和狀態。可是,若是咱們想要閱讀不屬於這個特定渲染的最新props或狀態,該怎麼辦? 若是咱們想「從將來讀取它們」怎麼辦?

在類中,你能夠經過閱讀this.propsthis.state來實現它,由於this自己是可變的。React改變了它。在函數組件中,還能夠有一個可變值,該值由全部組件呈現共享。它被稱爲「ref」:

function MyComponent() {
  const ref = useRef(null);
  // You can read or write `ref.current`.
  // ...
}
複製代碼

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

ref與實例字段扮演相同的角色。這是進入可變命令式世界的出口。你可能熟悉「DOM refs」,但概念更爲通用。它只是一個盒子,你能夠把東西放進去。

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

默認狀況下,React不會爲函數組件中的最新props或狀態建立ref。在許多狀況下,你不須要它們,分配它們將是浪費工做。可是,若是你願意,能夠手動跟蹤值:

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或狀態的最新值,那麼手動更新ref會很煩人。 咱們能夠經過使用effect來自動處理它:

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

  // Keep track of the latest value.
  const latestMessage = useRef('');
  useEffect(() => {
    latestMessage.current = message;
  });

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

(這裏是一個demo。)

咱們在effect 進行賦值,以便ref值僅在DOM更新後更改。這確保了咱們的突變不會破壞依賴於可中斷呈現的Time Slicing and Suspense等特性。

使用像這樣的ref並非常常須要的。捕獲props或狀態一般是更好的默認配置。 可是,在處理間隔和訂閱等命令式API時,它能夠很方便。請記住,你能夠跟蹤 任何 這樣的值 - 一個prop,一個狀態變量,整個props對象,甚至是函數。

這種模式對於優化也很方便,例如當useCallback標識更改太頻繁時。可是,using a reducer 一般是一個 更好的解決方案。(後續的博客文章的主題!)


在這篇文章中,咱們研究了類中常見的破壞模式,以及閉包如何幫助咱們修復它。然而,你可能已經注意到,當你試圖經過指定依賴項數組來優化Hooks時,可能會遇到使用過期閉包的bug。是否意味着閉包是問題?我不這麼認爲。

正如咱們上面所看到的,閉包實際上幫助咱們解決了很難注意到的細微問題。相似地,它們使在併發模式下編寫正確工做的代碼更加容易。這是可能的,由於組件內部的邏輯結束了正確的props和狀態。

到目前爲止,我所看到的全部狀況下,「過期的閉包」問題都是因爲錯誤地假設「函數不會更改」或「props老是相同」而發生的。 事實並不是如此,由於我但願這篇文章有助於澄清這個問題。

函數與它們的props和狀態密切相關,所以它們的身份也一樣重要。這不是bug,而是函數組件的一個特性。例如,函數不該該從useeffectusecallback的「依賴項數組」中排除。(正確的修復一般是useReducer或上面的useRef解決方案 - 咱們很快就會記錄如何在它們之間進行選擇。)

當咱們編寫大多數帶有函數的React代碼時,咱們須要調整優化代碼,以及哪些值會隨時間變化

正如 Fredrik所說 :

到目前爲止,我在hook中發現的最好的規則是「編寫代碼時,就好像任何值均可以隨時更改」。

函數也不例外。這將須要一段時間才能成爲react學習材料中的常識。這須要從階級觀念上作一些調整。但我但願這篇文章能夠幫助你以新的眼光看待它。

React函數老是捕獲它們的值 - 如今咱們知道緣由了。

image

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

相關文章
相關標籤/搜索