React函式式元件和類元件[Dan]

xuweiblog發表於2021-03-05

一篇對DanHow Are Function Components Different from Classes? 一文的個人閱讀總結,內容來自於此。強烈推薦閱讀 Dan Abramov.的部落格

函式式元件和Class元件有什麼不同?

Dan很直接的給出了答案:

函式式元件捕獲了渲染所用的值。(Function components capture the rendered values.)

直接看結論可能有點不知所云。

class元件可能引發的"錯誤"


看一個元件,使用setTimeout模擬網路請求,點選button之後警告提示關注某人(user),userprops中讀取。

該元件的function版本:

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

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

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

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>;
  }
}

頁面元件程式碼:

class App extends React.Component {
  state = {
    user: 'Dan',
  };
  render() {
    return (
      <>
        <label>
          <b>Choose profile to view: </b>
          <select
            value={this.state.user}
            onChange={e => this.setState({ user: e.target.value })}
          >
            <option value="Dan">Dan</option>
            <option value="Sophie">Sophie</option>
            <option value="Sunil">Sunil</option>
          </select>
        </label>
        <h1>Welcome to {this.state.user}’s profile!</h1>
        <p>
          <ProfilePageFunction user={this.state.user} />
          <b> (function)</b>
        </p>
        <p>
          <ProfilePageClass user={this.state.user} />
          <b> (class)</b>
        </p>
        <p>
          Can you spot the difference in the behavior?
        </p>
      </>
    )
  }
}

對於class元件,在選中狀態是userA的時候,點選follow button之後立馬將select切換其他人(uerB),三秒之後的彈出框是followuserB。(這個動作會在後面多次提及)

React函式式元件和類元件[Dan]

在選中userA的時候點選關注,目的就是關注userA,但是class元件最後彈出框顯示的關注userB,這顯然不符合預期。

為什麼、如何解決


如果上面例子使用function元件,彈出框顯示的就會是正確的,雖然切換到了選中的userB,但是彈出框顯示的仍然是點選的那一刻關注的userA

function元件好像記下了點選那一刻時候的狀態?

class元件在這個場景中錯誤的原因是class元件每次三秒後從this.props.user中讀取資料,此時的this.props.user已經變了,已經是切換後的新的this.props.user資料。雖然React中props不可變,但是this是可變的

類元件會隨著時間推移改變,在渲染方法和生命週期方法中得到的是最新的例項。而函式式元件的事件處理程式就是渲染結果的一部分,事件處理程式屬於一個擁有特定的propsstate的渲染

也就是說函式式元件保持了事件處理程式與那一次渲染propsstate之間的聯絡,本身就是正確的。


來修復類元件中的這個問題:

  1. 可以在點選的時候就讀取並記錄當下的stateprops,三秒後讀取記錄的資料(而不是讀this.props.xx)再彈出。

    方案可行但是擴充套件極差,在其他多個變數也這樣做的時候逐層記錄或傳遞非常繁複。

  2. 閉包。

    閉包維持了一個可能隨時間變化的變數,而此處我們要維持的是React的propsstate,React設計中這都是不可變的。讓閉包來維持不變的stateprops,此時再去捕獲這些值,就是一致的。

    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>;
      }
    }
    

    渲染的時候這些需要使用的props已經被捕獲(就像上面方案1的記錄,在render的時候就已經讀取記錄下了)。此時表現彈出內容就會是點選時候的那個userA了。


class元件的這個問題是修復了,但是在render函式中新增那麼多的函式,且並沒有掛載到class上,有點奇怪?

其實去掉class,這就是函式式元件的形式了:

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

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

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

React將他們作為引數傳遞,props在渲染時被捕獲了。不同於class元件的this,這裡的props不會被改變。

點選事件處理函式,該函式屬於具有正確user值的一次渲染,事件處理函式和其他回撥函式也能讀到這個值。

回頭看這個結論,是不是更好理解一點了:

函式式元件捕獲了渲染所使用的值

函式式元件使用最新的propsstate


函式式元件捕獲了特定渲染的propsstate。但是我們如果又想和class元件一樣讀取最新的propsstate呢?

useRef

Dan 老師:在函式式元件中,你也可以擁有一個在所有的元件渲染幀中共享的可變變數。它被成為“ref”

this.something就像是something.current的一個映象。他們代表了同樣的概念。

每一次的渲染結果可以視為一個渲染幀,共享的變數設定為ref,包含DOMRefclass中的例項變數的功能,可以說是非常強大了。

需要最新的propsstate值,可以使用useRef建立的變數來記錄,通過useEffect可以在值變化的時候自動追蹤。

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

  // 保持追蹤最新的值。
  const latestMessage = useRef('');
  useEffect(() => {
    latestMessage.current = message;
  });

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

React函式總是捕獲他們的值

相關文章