原文: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 來講明這個差異。
打開這個示例,裏面有一個表示當前用戶的下拉框以及上面實現的兩個關注組件 -- 每一個都渲染了一個關注按鈕。
按照下面的順序,分別操做兩個的按鈕:
你會看到一個奇怪的現象:
在這個例子裏,表現正確的應該是第一個。若是我點擊關注了一我的,而後切換到另外一我的,個人組件應該知道我最後關注了誰。 類組件的表現很明顯不正確。
(可是你真的應該關注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:
這樣一來,任何內部的代碼(包括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
:
如今,咱們理解了 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 Slicing
和 Suspense
,由於它們依賴於非連續的渲染。
一般咱們不須要這樣使用 Ref。大部分狀況下,捕獲 props 或 state 是更好的方式。可是,當處理一些命令式的 APIs 像interval
和subscriptions
會比較方便。記住你能夠像使用 this 同樣來追蹤任何值 -- 一個屬性,一個狀態變量,整個 props 對象,甚至是一個函數。
這個模式能夠方便地優化代碼 -- 例如useCallback
的依賴改變的過於頻繁時。可是,使用 reducer 一般是一個更好地選擇(又是一個能夠用一篇文章來討論的新特性!)
在這片文章裏,咱們瞭解了類中常見的問題,以及閉包是如何幫咱們解決的。可是,當你想要指定依賴來優化 Hooks 的時候,可能會遇到過時的閉包問題。這是否意味着閉包纔是問題所在呢?我不這麼認爲。
就像咱們上面看到的,閉包幫助咱們解決了一些平時很難注意到的細微的問題。而且,它也能幫咱們方便地寫出在併發模式下也能夠正常工做的代碼。這是由於組件內部邏輯綁定了它渲染時所對應的的 props 和 state。
在我所瞭解的案例中,「過時的閉包」問題都是由於開發者錯誤的認爲「函數不會改變」或者「props 一直都是相同的」。但事實並不是如此,但願我這篇文章裏已經解釋清楚了。
函數組件會和 props 和 state 綁定,因此確認它們的對應性很重要。這不是 bug,而是函數組件的特色。例如,函數也不該該從 useEffect 或者 useCallback 的依賴中被移除。(正確的作法是使用 useReducer 或者 useRef 解決,咱們很快就會出一份關於 2 者如何選擇的文檔)。
當咱們在咱們的 React 項目中大量使用函數組件時,咱們須要調整對代碼優化以及哪些值會隨着時間改變的見解。
就像Fredrik 說得:
到目前爲止,我發現使用 hooks 最好的心智規則就是:編碼的時候當作任何值在任什麼時候候都會改變。
函數也不該該排除在這條規則以外。可是把它當作 React 學習中的常識還須要一段時間。由於它要求開發者從類組件的心智模型中作一些調整。可是我但願這篇文章能幫你從一個全新的視角看待這個問題。
React 函數組件老是能捕獲它們的值 -- 如今咱們知道了爲何。
由於它們是各類不一樣的神奇寶貝。