[譯]函數組件和類組件到底哪裏不一樣

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

譯者: github.com/joe06102html

React 裏的函數組件和類組件有哪些不一樣?java

一度,二者的區別在於類組件能提供更多的能力(好比局部的狀態)。可是有了Hooks以後,狀況卻有所不一樣了。react

也許以前你據說性能也是二者的差異。可是哪一個性能更好?很差說。我一直很謹慎地對待這類結論,由於不少性能測試都是不全面的。性能主要仍是在於代碼的邏輯而非你選擇了函數組件或者類組件。據咱們觀察,雖然二者的優化策略有所差異,可是總體來講性能區別是微不足道的。git

不管哪一種狀況下,咱們都不推薦用 Hooks 重寫你現有的組件。除非你有其餘緣由而且不介意作第一個吃螃蟹的人。由於 Hooks 還不是很成熟(就像 2014 年的 React),有一些「最佳實踐」尚待發掘。github

因此咱們該怎麼辦?函數組件和類組件本質上難道沒有區別嗎?固然有,二者在心智模型上有區別,也就是接下來我要說的二者之間最大的區別。早在 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,3 秒後彈窗就會顯示‘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>;
  }
}
複製代碼

咱們通常會以爲這兩段代碼功能是同樣的。人們可能在這兩種模式之間來回重構,可是卻並未注意到這兩種模式分別意味着什麼。

事實上,這兩段代碼之間的差異是很精妙的。 仔細看看,你能察覺它們之間的差異嗎?反正我是花了好久纔看出來。

前方劇透,若是你想本身琢磨,這裏有一個例子 接下來我將解釋二者的差異以及爲何它這麼重要。


在解釋以前,我想強調一點:我描述的差異和 React Hooks 自己無關。由於剛纔的例子都沒用到 Hooks!

這個差異只關乎函數組件和類組件。若是你常常用到函數組件的話, 你應該要理解它。


咱們會經過一個 React 應用裏常見的 Bug 來講明這個差異。

打開這個示例,裏面有一個表示當前用戶的下拉框以及上面實現的兩個關注組件 -- 每一個都渲染了一個關注按鈕。

按照下面的順序,分別操做兩個的按鈕:

  1. 點擊其中一個 Follow 按鈕
  2. 在 3 秒內改變下拉框中選擇的用戶
  3. 觀察彈窗中出現的文字

你會看到一個奇怪的現象:

  • 函數組件中,點擊 Follow Dan,而後切換到 Sophie,彈窗仍然顯示‘Followed Dan’
  • 類組件中,彈窗卻顯示‘Followed Sophie’:

Demonstration of the steps

在這個例子裏,表現正確的應該是第一個。若是我點擊關注了一我的,而後切換到另外一我的,個人組件應該知道我最後關注了誰。 類組件的表現很明顯不正確。

(可是你真的應該關注Sophie。)


因此類組件的表現爲何是這樣的?

讓咱們仔細看下showMessage這個方法:

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

這個方法從this.props.user中讀取用戶。在 React 中,props 是不可變的,因此它們不會改變。可是,this一直都是可變的。

事實上,這就是this在 class 中的目的。React 經過常常的更新 this 來保證你在 render 和生命週期方法中總能讀取到最新版本的數據。

因此,當咱們請求期間,this.props 改變了,showMessage方法就會從’太新‘的 props 中讀取用戶信息。

這其實展露了一個關於 UI 本質的現象。若是概念上來講 UI 是應用當前狀態的一種映射,那處理事件其實也是渲染的一部分結果 - 就像視覺上的渲染輸出同樣。咱們的處理事件其實」屬於「某個特定 props 和 state 生成的特定渲染。

可是,延時任務打破了這種關聯。咱們的showMessage回調再也不和特定的渲染綁定,因此就」失去「了其正確的 props。正是從 this 中讀取這個行爲,切斷了二者之間的關聯。


假設函數組件不存在,咱們會怎麼解決這類問題?

咱們想沿着 props 丟失的地方,以某種方式修復正確的 props 生成的渲染和 showMessage 回調之間的關聯。

其中一種方法就是儘早地從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>;
  }
}
複製代碼

上面這種方法能夠奏效。可是,隨着時間的推移,這樣的代碼會變得很囉嗦,而且容易出錯。若是須要更多屬性怎麼辦?若是還要訪問 state 呢?若是showMessage又調用了別的函數,而這個函數又從 props 或者 state 讀取了某些屬性,咱們又會遇到一樣的問題。因此咱們仍是得傳遞 this.props 和 this.state 給每一個在 showMessage 中被調用的函數。

這麼作很容易下降效率。並且開發人員很容易忘記,也很難保證必定會按照上面的方法來避免問題,因此常常要去解決這類 bug。

一樣的,把alert代碼內聯到handleClick也解決不了這個問題。咱們但願以更細的粒度來組織代碼,同時也但願能讀取和特定渲染對應的 props 和 state 值。這個問題其實非 React 纔有,-- 任何一個存在相似this這類可變數據對象的 UI 框架都會有

可能有人會說,咱們是否是能夠在構造函數中綁定這些方法?

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 都是不可變的(至少咱們是這麼推薦的!)。這樣就避免了搬起石頭砸本身的腳。

這意味着若是你將每次渲染的 props 或者 state 關住,你就能夠保證他們對應的都是特定渲染的結果:

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:

Capturing Pokemon

這樣一來,任何內部的代碼(包括showMessage)就能保證讀取到的都是此次渲染的 props。React 也就」動不了咱們的奶酪」了。

而後咱們想加多少函數就能加多少,而且它們都能讀取到正確的 props 或者 state。閉包簡直是救星!

可是上面的例子看起來有些奇怪。既然都有類了,咱們爲何還要在 render 函數裏定義函數,而不直接定義類的方法?

事實上,咱們能夠經過拿掉「class」的外殼來簡化代碼:

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

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

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

和以前的例子同樣,這裏的props也能被捕獲 - 由於 React 將它做爲參數傳遞。不一樣於this,React 不會對這裏的props對象對任何修改。

若是你在函數定義中將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回調也「屬於」上一次。一切都是原封不動。

這也就是爲何,在這個demo的函數式版本中,點擊 Follow Sophie,而後切換到 Sunil,卻仍然提示Followed Sophie:

Demo of correct behavior


如今,咱們理解了 React 中函數式組件和類組件之間最大的區別就是:

函數式組件能夠捕獲渲染的值。

在 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)

儘管這不是一個很好的聊天應用界面,可是它也能說明一樣的問題:若是我發送了一條消息,應用應該清楚地知道我發送了哪條消息。這個函數組件中的message捕獲了特定渲染的 state,而瀏覽器所點擊的事件處理函數也是渲染結果的一部分。因此message就是我點擊「Send」時輸入的值。

如今咱們知道了 React 中的函數默承認以捕獲 props 和 state。可是若是咱們想訪問不屬於此次渲染的最新的 props 或 state 呢?或者是想要稍後再訪問它們呢?

在類組件中,你會經過讀取this.props或者this.state來實現由於 this 自己是可變的。React 會修改它。在函數組件中,你能夠擁有一個可變的值,而且它在每次組件渲染的時候都是能夠被共享的。這就是ref

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

可是,你必需要本身管理它。 ref扮演了一個實例字段的角色。它就像是逃往可變的、命令式世界的出口。你可能對」Dom Refs「比較熟悉,可是 ref 的概念更加通用一些。它就是一個你能夠往裏面裝些東西的盒子。

甚至在視覺上,this.somthing看起來就像是另外一個something.current。它們有着一樣的含義。

默認狀況下,React 不會在函數式組件中爲最新的 props 或者 state 建立 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,由於它們是可變的。咱們但願保持渲染可預測。可是,若是你想讀取某個 prop 或 state 的最新值,手動更新 ref 是比較麻煩的。咱們能夠藉助反作用來實現自動同步:

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

這是示例

咱們經過在反作用中給 ref 賦值來保證 ref 的值只在每次 DOM 更新以後纔會改變。這樣能夠確保咱們的改變不會影響別的功能例如Time SlicingSuspense,由於它們依賴於非連續的渲染。

一般咱們不須要這樣使用 Ref。大部分狀況下,捕獲 props 或 state 是更好的方式。可是,當處理一些命令式的 APIs 像intervalsubscriptions會比較方便。記住你能夠像使用 this 同樣來追蹤任何值 -- 一個屬性,一個狀態變量,整個 props 對象,甚至是一個函數。

這個模式能夠方便地優化代碼 -- 例如useCallback的依賴改變的過於頻繁時。可是,使用 reducer 一般是一個更好地選擇(又是一個能夠用一篇文章來討論的新特性!)


在這片文章裏,咱們瞭解了類中常見的問題,以及閉包是如何幫咱們解決的。可是,當你想要指定依賴來優化 Hooks 的時候,可能會遇到過時的閉包問題。這是否意味着閉包纔是問題所在呢?我不這麼認爲。

就像咱們上面看到的,閉包幫助咱們解決了一些平時很難注意到的細微的問題。而且,它也能幫咱們方便地寫出在併發模式下也能夠正常工做的代碼。這是由於組件內部邏輯綁定了它渲染時所對應的的 props 和 state。

在我所瞭解的案例中,「過時的閉包」問題都是由於開發者錯誤的認爲「函數不會改變」或者「props 一直都是相同的」。但事實並不是如此,但願我這篇文章裏已經解釋清楚了。

函數組件會和 props 和 state 綁定,因此確認它們的對應性很重要。這不是 bug,而是函數組件的特色。例如,函數也不該該從 useEffect 或者 useCallback 的依賴中被移除。(正確的作法是使用 useReducer 或者 useRef 解決,咱們很快就會出一份關於 2 者如何選擇的文檔)。

當咱們在咱們的 React 項目中大量使用函數組件時,咱們須要調整對代碼優化以及哪些值會隨着時間改變的見解。

就像Fredrik 說得

到目前爲止,我發現使用 hooks 最好的心智規則就是:編碼的時候當作任何值在任什麼時候候都會改變。

函數也不該該排除在這條規則以外。可是把它當作 React 學習中的常識還須要一段時間。由於它要求開發者從類組件的心智模型中作一些調整。可是我但願這篇文章能幫你從一個全新的視角看待這個問題。

React 函數組件老是能捕獲它們的值 -- 如今咱們知道了爲何。

Smiling Pikachu

由於它們是各類不一樣的神奇寶貝。

Discuss on TwitterEdit on GitHub

相關文章
相關標籤/搜索