[譯]React函數組件和類組件的差別

[譯]React函數組件和類組件的差別

原文overreacted.io/how-are-fun…javascript

React.js 開發中,函數組件(function component) 和 類組件(class component) 有什麼差別呢?html

在之前,一般認爲區別是,類組件提供了更多的特性(好比state)。隨着 React Hooks 的到來,這個說法也不成立了(經過hooks,函數組件也能夠有state和類生命週期回調了)。java

或許你也據說過,這兩類組件中,有一類的性能更好。哪一類呢?不少這方面的性能測試,都是 有缺陷的 ,所以要從這些測試中 得出結論 ,不得不謹慎一點。性能主要取決於你代碼要實現的功能(以及你的具體實現邏輯),和使用函數組件仍是類組件,沒什麼關係。咱們觀察發現,儘管函數組件和類組件的性能優化策略 有些不一樣 ,可是他們性能上的差別是很微小的。react

無論是上述哪一個緣由,咱們都 不建議 你使用函數組件重寫已有的類組件,除非你有別的緣由,或者你喜歡當第一個吃螃蟹的人。React Hooks 還很新(就像2014年的React同樣),目前尚未使用hooks相關的最佳實踐。git

除了上面這些,還有什麼別的差別麼?在函數組件和類組件之間,真的存在根本性的不一樣麼?"Of course, there are — in the mental model" (這個實在不知道咋表達,貼下做者原文吧😅) 在這篇文章裏,咱們將一塊兒看下,這兩類組件最大的不一樣。這個不一樣點,在2015年函數組件函數組件 被引入React 時就存在了,可是被大多數人忽略了。github

函數組件和類組件的差別

函數組件會捕獲render內部的狀態編程

讓咱們一步步來看下,這表明什麼意思。數組

**注意,本文並非對函數組件和類組件進行價值判斷。我只是展現下React生態裏,這兩種組件編程模型的不一樣點。要學習如何更好的採用函數組件,推薦官方文檔 Hooks FAQ ** 。性能優化

假設咱們有以下的函數組件:網絡

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

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

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

組件會渲染一個按鈕,當點擊按鈕的時候,模擬了一個異步請求,而且在請求的回調函數裏,顯示一個彈窗。好比,若是 props.user 的值是 Dan,點擊按鈕3秒以後,咱們將看到 Followed Dan 這個提示彈窗。很是簡單。

(注意,上面代碼裏,使用箭頭函數仍是普通的函數,沒有什麼區別。由於沒有 this 問題。把箭頭函數換成普通函數 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生態裏,函數組件和類組件的差別性。若是你打算在react開發中,大規模的使用函數組件,那麼你可能須要瞭解這個差別。

咱們將使用在react平常開發中,常常遇到的一個bug,來展現這個差別

接下來,咱們就來複現下這個bug。打開 這個在線例子 ,頁面上有一個名字下拉框,下面是兩個關注組件,一個是前文的函數組件,另外一個是類組件。

對每個關注按鈕,分別進行以下操做:

  1. 點擊其中1個關注按鈕
  2. 在3秒以內,從新選擇下拉框中的名字
  3. 3秒以後,注意看alert彈窗中的文字差別

你應該注意到了兩次alert彈窗的差異:

  • 在函數組件的測試狀況下,下拉框中選中 Dan,點擊關注按鈕,迅速將下拉框切換到Sophie,3秒以後,alert彈窗內容仍然是 Followed Dan
  • 在類組件的測試狀況下,重複相同的動做,3秒以後,alert彈窗將會顯示 Followed Sophie

在這個例子裏,使用函數組件的實現是正確的,類組件的實現明顯有bug。若是我先關注了一我的,而後切換到了另外一我的的頁面,關注按鈕不該該混淆我實際關注的是哪個

(PS,我也推薦你真的關注下 Sophie)

那麼,爲何咱們的類組件,會存在問題呢?

讓咱們再仔細看看類組件的 showMessage 實現:

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

這個方法會讀取 this.props.user 。在React生態裏,props是不可變數據,永遠不會改變。可是,this卻始終是可變的

確實,this存在的意義,就是可變的。react在執行過程當中,會修改this上的數據,保證你可以在 render和其餘的生命週期方法裏,讀取到最新的數據(props, state)。

所以,若是在網絡請求處理過程當中,咱們的組件從新渲染,this.props改變了。在這以後,showMessage方法會讀取到改變以後的 this.props

這揭示了用戶界面渲染的一個有趣的事實。若是咱們認爲,用戶界面(UI)是對當前應用狀態的一個可視化表達(UI=render(state)),那麼事件處理函數,一樣屬於render結果的一部分,正如用戶界面同樣 。咱們的事件處理函數,是屬於事件觸發時的render,以及那次render相關聯的 propsstate

然而,咱們在按鈕點擊事件處理函數裏,使用定時器(setTimeout)延遲調用 showMessage,打破了showMessagethis.props的關聯。showMessage回調再也不和任何的render綁定,一樣丟失了原本關聯的props。從 this 上讀取數據,切斷了這種關聯。

假如函數組件不存在,那咱們怎麼來解決這個問題呢?

咱們須要經過某種方式,修復showMessage和它所屬的 render以及對應props的關聯。

一種方式,咱們能夠在按鈕點擊處理函數中,讀取當前的 props,而後顯式的傳給 showMessage,就像下面這樣:

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

這種方式 能夠解決這個問題 。然而,這個解決方式讓咱們引入了冗餘的代碼,隨着時間推移,容易引入別的問題。若是咱們的 showMessage方法要讀取更多的props呢?若是showMessage還要訪問state呢?若是 showMessage 調用了其餘的方法,而那個方法讀取了別的狀態,好比this.props.somethingthis.state.something ,咱們會再次面臨一樣的問題。 咱們可能須要在 showMessage 裏顯式的傳遞 this.props this.state 給其餘調用到的方法。

這樣作,可能會破壞類組件帶給咱們的好處。這也很難判斷,何時須要傳遞,何時不須要,進一步增長了引入bug的風險。

一樣,簡單地把全部代碼都放在 onClick 處理函數裏,會帶給咱們其餘的問題。爲了代碼可讀性、可維護性等緣由,咱們一般會把大的函數拆分爲一些獨立的小的函數。這個問題不只僅是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做爲不可變數據)。props和state的不可變特性,完美解決了使用閉包帶來的問題。

這意味着,若是在 render方法裏,經過閉包來訪問props和state,咱們就能確保,在showMessage執行時,訪問到的props和state就是render執行時的那份數據:

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

在render執行時,你成功的捕獲了當時的props

經過這種方式,render方法裏的任何代碼,都能訪問到render執行時的props,而不是後面被修改過的值。react不會再偷偷挪動咱們的奶酪了。

像上面這樣,咱們能夠在render方法裏,根據須要添加任何幫助函數,這些函數都可以正確的訪問到render執行時的props。閉包,這一切的救世主。

上面的代碼,功能上沒問題,可是看起來有點怪。若是組件邏輯都做爲函數定義在render內部,而不是做爲類的實例方法,那爲何還要用類呢?

確實,咱們剝離掉類的外衣,剩下的就是一個函數組件:

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

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

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

這個函數組件和上面的類組件同樣,內部函數捕獲了props,react會把props做爲函數參數傳進去。和this不一樣的是,props是不可變的,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。可是在這以前,咱們點擊關注按鈕的事件處理函數,已經捕獲了上一次render時的props。

這就是爲何,在 這個例子 的函數組件中,沒有問題。

能夠看到,功能徹底是正確的。(再次PS,建議你也關注下 Sunil)

如今咱們理解了,在函數組件和類組件之間的這個差別:

函數組件會捕獲render內部的狀態

函數組件配合React Hooks

在有 Hooks 的狀況下,函數組件一樣會捕獲render內部的 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,點擊這裏)

儘管這是一個很簡陋的消息發送組件,它一樣展現了和前一個例子相同的問題:若是我點擊了發送按鈕,這個組件應該發送的是,我點擊按鈕那一刻輸入的信息。

OK,咱們如今知道,函數組件會默認捕獲props和state。可是,若是你想讀取最新的props、state呢,而不是某一時刻render時捕獲的數據? 甚至咱們想在 未來某個時刻讀取舊的props、state 呢?

在類組件裏,咱們只須要簡單的讀取 this.props this.state 就能訪問到最新的數據,由於react會修改this。在函數組件裏,咱們一樣能夠擁有一個可變數據,它能夠在每次render裏共享同一份數據。這就是hooks裏的 useRef

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

可是,你須要本身維護 ref 對應的值。

函數組件裏的 ref 和類組件中的實例屬性 扮演了相同的角色 。你或許已經熟悉 DOM refs,可是hooks裏的 ref 更加通用。hooks裏的 ref 只是一個容器,你能夠往容器裏放置任何你想放的東東。

甚至看起來,類組件裏的 this.something 也和hooks裏的 something.current 類似,他們確實表明了同一個概念。

默認狀況下,react不會給函數組件裏的props、state創造refs。大多數場景下,你也不須要這樣作,這也須要額外的工做來給refs賦值。固然了,你能夠手動的實現代碼來跟蹤最新的state:

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,咱們會獲得輸入框裏最新的值——甚至咱們在點擊發送按鈕後,不斷的輸入新的內容。

你能夠對比 這兩個demo 來看看其中的不一樣。

一般來說,你應該避免在render函數中,讀取或修改 refs ,由於 refs 是可變的。咱們但願能保證render的結果可預測。可是,若是咱們想要獲取某個props或者state的最新值,每次都手動更新refs的值顯得很枯燥 。這種狀況下,咱們可使用 useEffect 這個hook:

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

咱們在 useEffect 裏去更新 ref 的值,這保證只有在DOM更新以後,ref纔會被更新。這確保咱們對 ref 的修改,不會破壞react中的一些新特性,好比 時間切分和中斷 ,這些特性都依賴 可被中斷的render。

像上面這樣使用 ref 不會太常見。大多數狀況下,咱們須要捕獲props和state 。可是,在處理命令式API的狀況下,使用 ref 會很是簡便,好比設置定時器,訂閱事件等。記住,你可使用 ref 來跟蹤任何值——一個prop,一個state,整個props,或者是某個函數。

使用 ref 在某些性能優化的場景下,一樣適用。好比在使用 useCallback 時。可是,使用useReducer 在大多數場景下是一個 更好的解決方案

總結

在這篇文章裏,咱們回顧了類組件中常見的一個問題,以及怎樣使用閉包來解決這個問題。可是,你可能已經經歷過了,若是你嘗試經過指定hooks的依賴項,來優化hooks的性能,那麼你極可能會在hooks裏,訪問到舊的props或state。這是否意味着閉包會帶來問題呢?我想不是的。

正如咱們上面看到的,在一些不易察覺的場景下,閉包幫助咱們解決掉這些微妙的問題。不只如此,閉包也讓咱們在 並行模式 下更加容易寫出沒有bug的代碼。由於閉包捕獲了咱們render函數運行時的props和state,使得並行模式成爲可能。

到目前爲止,我經歷的全部狀況下,訪問到舊的props、state,一般是因爲咱們錯誤的認爲"函數不會變",或者"props始終是同樣的"。實時上不是這樣的,我但願在本文裏,可以幫助你瞭解到這一點。

在咱們使用函數來開發大部分react組件時,須要更正咱們對於 代碼優化哪些狀態會改變 的認知。

正如 Fredrik說的:

在使用react hooks過程當中,我學習到的最重要規則就是,"任何變量,均可以在任什麼時候間被改變"

函數一樣遵照這條規則。

react函數始終會捕獲props、state,最後再強調一下。

譯註: 有些地方不明白怎麼翻譯,有刪減,建議閱讀原文!

相關文章
相關標籤/搜索