精讀《Function VS Class 組件》

1. 引言

為什么要了解 Function 寫法的組件呢?因為它正在變得越來越重要。

那么 React 中 Function Component 與 Class Component 有何不同?

how-are-function-components-different-from-classes 這篇文章帶來了一個獨特的視角。

順帶一提,以后會用 Function Component 代替 Stateless Component 的說法,原因是:自從 Hooks 出現(xiàn),函數(shù)式組件功能在不斷豐富,函數(shù)式組件不再需要強調(diào)其無狀態(tài)特性,因此叫 Function Component 更為恰當(dāng)。

2. 概述

原文事先申明:并沒有對 Function 與 Classes 進行優(yōu)劣對比,而僅僅進行特性對比,所以不接受任何吐槽。

這兩種寫法沒有好壞之分,性能差距也幾乎可以忽略,而且 React 會長期支持這兩種寫法。

Capture props

對比下面兩段代碼。

Class Component:

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

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

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

Function Component:

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

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

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

在線 Demo

這兩個組件都描述了同一個邏輯:點擊按鈕 3 秒后 alert 父級傳入的用戶名。

如下父級組件的調(diào)用方式:

<ProfilePageFunction user={this.state.user} />
<ProfilePageClass user={this.state.user} />

那么當(dāng)點擊按鈕后的 3 秒內(nèi),父級修改了 this.state.user,彈出的用戶名是修改前的還是修改后的呢?

Class Component 展示的是修改后的值:

<img width=500 src="https://img.alicdn.com/tfs/TB1N8ObMwTqK1RjSZPhXXXfOFXa-950-351.gif">

Function Component 展示的是修改前的值:

<img width=500 src="https://img.alicdn.com/tfs/TB1kFCmMzTpK1RjSZKPXXa3UpXa-901-293.gif">

那么 React 文檔中描述的 props 不是不可變(Immutable) 數(shù)據(jù)嗎?為啥在運行時還會發(fā)生變化呢?

原因在于,雖然 props 不可變,是 this 在 Class Component 中是可變的,因此 this.props 的調(diào)用會導(dǎo)致每次都訪問最新的 props

而 Function Component 不存在 this.props 的語法,因此 props 總是不可變的。

為了便于理解,筆者補充一些代碼注解:

Function Component:

function ProfilePage(props) {
  setTimeout(() => {
    // 就算父組件 reRender,這里拿到的 props 也是初始的
    console.log(props);
  }, 3000);
}

Class Component:

class ProfilePage extends React.Component {
  render() {
    setTimeout(() => {
      // 如果父組件 reRender,this.props 拿到的永遠(yuǎn)是最新的。
      // 并不是 props 變了,而是 this.props 指向了新的 props,舊的 props 找不到了
      console.log(this.props);
    }, 3000);
  }
}

如果希望在 Class Component 捕獲瞬時 Props,可以: const props = this.props;,但這樣的代碼很蹩腳,所以如果希望拿到穩(wěn)定的 props,使用 Function Component 是更好的選擇。

Hooks 也具有 capture value 特性

看下面的代碼:

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

在點擊 Send 按鈕后,再次修改輸入框的值,3 秒后的輸出依然是 點擊前輸入框的值。這說明 Hooks 同樣具有 capture value 的特性。

利用 useRef 可以規(guī)避 capture value 特性:

function MessageThread() {
  const latestMessage = useRef("");

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

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

  const handleMessageChange = e => {
    latestMessage.current = e.target.value;
  };
}

只要將賦值與取值的對象變成 useRef,而不是 useState,就可以躲過 capture value 特性,在 3 秒后得到最新的值。

這說明了利用 Function Component + Hooks 可以實現(xiàn) Class Component 做不到的 capture props、capture value,而且 React 官方也推薦 新的代碼使用 Hooks 編寫。

3. 精讀

原文 how-are-function-components-different-from-classes 從一個側(cè)面講述了 Function Component 與 Class Component 的不同點,之所以將 Function Component 與 Class Component 相提并論,幾乎都要歸功于 Hooks API 的出現(xiàn),有了 Hooks,F(xiàn)unction Component 的能力才得以向 Class Component 看齊。

關(guān)于 React Hooks,之前的兩篇精讀分別有過介紹:

但是,雖然 Hook 已經(jīng)發(fā)布了穩(wěn)定版本,但周邊生態(tài)跟進還需要時間(比如 useRouter)、最佳實踐整理還需要時間,因此不建議重構(gòu)老代碼。

為了更好的使用 Function Component,建議時常與 Class Component 的功能做對比,方便理解和記憶。

下面整理一些常見的 Function Component 問題:

非常建議完整閱讀 React Hooks FAQ。

怎么替代 shouldComponentUpdate

說實話,F(xiàn)unction Component 替代 shouldComponentUpdate 的方案并沒有 Class Component 優(yōu)雅,代碼是這樣的:

const Button = React.memo(props => {
  // your component
});

或者在父級就直接生成一個自帶 memo 的子元素:

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b= />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  );
}

相比之下,Class Component 的寫法通常是:

class Button extends React.PureComponent {}

這樣就自帶了 shallowEqualshouldComponentUpdate

怎么替代 componentDidUpdate

由于 useEffect 每次 Render 都會執(zhí)行,因此需要模擬一個 useUpdate 函數(shù):

const mounting = useRef(true);
useEffect(() => {
  if (mounting.current) {
    mounting.current = false;
  } else {
    fn();
  }
});

更多可以查看 精讀《怎么用 React Hooks 造輪子》

怎么替代 forceUpdate

React 官方文檔提供了一種方案:

const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
  forceUpdate();
}

每次執(zhí)行 dispatch 時,只要 state 變化就會觸發(fā)組件更新。當(dāng)然 useState 也同樣可以模擬:

const useUpdate = () => useState(0)[1];

我們知道 useState 下標(biāo)為 1 的項是用來更新數(shù)據(jù)的,而且就算數(shù)據(jù)沒有變化,調(diào)用了也會刷新組件,所以我們可以把返回一個沒有修改數(shù)值的 setValue,這樣它的功能就僅剩下刷新組件了。

更多可以查看 精讀《怎么用 React Hooks 造輪子》

state 拆分過多

useState 目前的一種實踐,是將變量名打平,而非像 Class Component 一樣寫在一個 State 對象里:

class ClassComponent extends React.PureComponent {
  state = {
    left: 0,
    top: 0,
    width: 100,
    height: 100
  };
}

// VS

function FunctionComponent {
  const [left,setLeft] = useState(0)
  const [top,setTop] = useState(0)
  const [width,setWidth] = useState(100)
  const [height,setHeight] = useState(100)
}

實際上在 Function Component 中也可以聚合管理 State:

function FunctionComponent() {
  const [state, setState] = useState({
    left: 0,
    top: 0,
    width: 100,
    height: 100
  });
}

只是更新的時候,不再會自動 merge,而需要使用 ...state 語法:

setState(state => ({ ...state, left: e.pageX, top: e.pageY }));

可以看到,更少的黑魔法,更可預(yù)期的結(jié)果。

獲取上一個 props

雖然不怎么常用,但是畢竟 Class Component 可以通過 componentWillReceiveProps 拿到 previousPropsnextProps,對于 Function Component,最好通過自定義 Hooks 方式拿到上一個狀態(tài):

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return (
    <h1>
      Now: {count}, before: {prevCount}
    </h1>
  );
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

通過 useEffect 在組件渲染完畢后再執(zhí)行的特性,再利用 useRef 的可變特性,讓 usePrevious 的返回值是 “上一次” Render 時的。

可見,合理運用 useEffect useRef,可以做許多事情,而且封裝成 CustomHook 后使用起來仍然很方便。

未來 usePrevious 可能成為官方 Hooks 之一。

性能注意事項

useState 函數(shù)的參數(shù)雖然是初始值,但由于整個函數(shù)都是 Render,因此每次初始化都會被調(diào)用,如果初始值計算非常消耗時間,建議使用函數(shù)傳入,這樣只會執(zhí)行一次:

function FunctionComponent(props) {
  const [rows, setRows] = useState(() => createRows(props.count));
}

useRef 不支持這種特性,需要寫一些冗余的函判定是否進行過初始化。

掌握了這些,F(xiàn)unction Component 使用起來與 Class Component 就幾乎沒有差別了!

4. 總結(jié)

Function Component 功能已經(jīng)可以與 Class Component 媲美了,但目前最佳實踐比較零散,官方文檔推薦的一些解決思路甚至不比社區(qū)第三方庫的更好,可以預(yù)料到,Class Component 的功能會被五花八門的實現(xiàn)出來,那些沒有被收納進官方的 Hooks 乍看上去可能會眼花繚亂。

總之選擇了 Function Component 就同時選擇了函數(shù)式的好與壞。好處是功能強大,幾乎可以模擬出任何想要的功能,壞處是由于可以靈活組合,如果自定義 Hooks 命名和實現(xiàn)不夠標(biāo)準(zhǔn),函數(shù)與函數(shù)之間對接的溝通成本會更大。

討論地址是:精讀《Stateless VS Class 組件》 · Issue #137 · dt-fe/weekly

如果你想?yún)⑴c討論,請 點擊這里,每周都有新的主題,周末或周一發(fā)布。前端精讀 - 幫你篩選靠譜的內(nèi)容。

關(guān)注 前端精讀微信公眾號

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

special Sponsors

版權(quán)聲明:自由轉(zhuǎn)載-非商用-非衍生-保持署名(創(chuàng)意共享 3.0 許可證

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南,這只是我在學(xué)習(xí)過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,953評論 1 18
  • 目前,react組件有三種寫法,分別是es5的createClass寫法,es6的class寫法,以及statel...
    ZoomFunc閱讀 1,912評論 0 1
  • 作為一個合格的開發(fā)者,不要只滿足于編寫了可以運行的代碼。而要了解代碼背后的工作原理;不要只滿足于自己的程序...
    六個周閱讀 8,684評論 1 33
  • It's a common pattern in React to wrap a component in an ...
    jplyue閱讀 3,409評論 0 2
  • HTML模版 之后出現(xiàn)的React代碼嵌套入模版中。 1. Hello world 這段代碼將一個一級標(biāo)題插入到指...
    ryanho84閱讀 6,449評論 0 9

友情鏈接更多精彩內(nèi)容