原文連接:overreacted.io/how-are-fun…javascript
在很長一段時間內,標準答案是class components
提供更多的特性(像state)。但隨着Hooks的出現,答案就再也不是這樣子了。java
或許你據說過他們中的一個性能可能更好,哪個?由於各類的判斷標準獲取都存在缺陷,因此咱們須要當心仔細的得出結論。性能的好壞主要取決於什麼?它主要取決於你的代碼在作什麼,而不是你使用的是function仍是class。在咱們的觀察中,儘管優化的策略可能會有些許的不一樣,但性能的差別幾乎能夠忽略不及。react
不管是哪一種狀況,咱們都不建議你重寫現有的組件,除非你有一些其餘的緣由或者是想成爲Hooks的早期的採用者。Hooks仍然是一個新特性(就像2014年的React同樣),一些最佳實踐尚未被寫入到教程中。編程
那咱們該怎麼辦?function components
和class components
之間有什麼本質的區別嗎?顯然,在構思模型中是不一樣的。在這篇文章中,咱們將看到他們之間最大的區別,自從2015年推出function componetns
以來,它就一直存在着,可是卻常常被忽視:數組
function components捕獲渲染值(capture value)網絡
注意: 本文並非對函數或者類的值作判斷。我只描述了React中這兩個編程模型之間的區別。有關更普遍地採用函數的問題,請參閱hooks常見問題解答。閉包
思考下面這樣一個組件:併發
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
複製代碼
這個組件經過setTimeout模擬網絡請求,在點擊按鈕3秒後彈出props.user的值,若是props.user的值是Dan的話,他將在點擊後3秒彈出「Followed Dan」。(注意,使用箭頭函數仍是函數聲明的形式並不重要,handleClick函數的工做方式徹底相同)dom
若是改寫成class形式可能長下面這個樣子:ide
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
複製代碼
一般認爲這兩段代碼是等效的。可是你們常常在這兩種形式之間來回切換代碼而不去關注他們的含義。
然而其實這兩段代碼是有細微的差異的,就我我的而言,我花費了一段時間纔看出來。
若是你本身想弄清楚的話,這裏有一個在線demo。本文的剩餘部分解釋了有什麼不一樣和它的重要性。
再繼續以前,我要強調,我所描述的差別自己和React Hooks無關,上面的例子甚至都沒有使用Hooks。
這所有都是React中,function和class的差異。若是你想要在React應用中去頻繁的使用function components
,那麼你應該去告終它。
咱們將用一個在React應用程序中常見的錯誤來講明這一區別。
打開這個示例,有一個主頁select和兩個主頁,且每個包含一個Follow按鈕。
嘗試按照下面的順序操做:
你會發現一個問題:
function components
中,在Dan的主頁點擊follow而後切換到Sophie,alert仍然會展現「Followed Dan」。class components
中,alert的倒是「Followed Sophie」。在這個例子中,第一個行爲是正確的。若是我Follow A,而後導航B的主頁,個人組件不該該Follow到B。這個class顯然有缺陷。
因此爲何咱們class的例子展現出這樣的結果呢?
讓咱們仔細研究一下class中的showMessage方法:
showMessage = () => {
alert('Followed ' + this.props.user);
};
複製代碼
這個方法從this.props.user
取值,在React中,props應該是不可變的,可是this
倒是可變的。
實際上,在React內部會隨着時間的推移改變this
,以即可以在render和生命週期中取到最新的版本。
因此若是咱們的組件在請求過程當中re-render,this.props
將會改變,showMessage方法將會從「最新」的props中取到user的值。
這就暴露了一個關於UI的有趣問題。若是說UI是一個關於當前應用state的函數,那麼事件處理函數就是render的一部分,就像是可視化輸出同樣。咱們的事件處理函數「屬於「某一特定state和props的render。
可是在包含超時操做的回調函數內讀取this.props會破壞這個關聯。showMessage沒有「綁定」到任何一個特定的render,所以它「丟失」了正確的props。
咱們說function components
不會存在這個問題。那麼咱們該怎麼去解決呢?
咱們須要去用某種方式「修復」正確的props到showMessage之間的關聯。在執行的某個地方,props丟失了。
一個簡單的方式就是在早期咱們就拿到這個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>;
}
}
複製代碼
這是可行的。然而,這種方法使代碼更加冗長,而且隨着時間的推移更容易出錯。若是咱們須要的不只僅是一個單一的props怎麼辦?若是ShowMessage調用另外一個方法,而該方法讀取this.props.something或this.state.something,咱們將再次遇到徹底相同的問題。因此咱們必須經過在ShowMessage調用的每一個方法,將this.props和this.state做爲參數傳遞。
這樣作咱們一般會破壞一個class,而且會致使不少bug出現。
一樣,在handleClick中用alert展現也不能暴露出更深的問題。若是咱們想要去結構化咱們的代碼,將代碼拆分出不一樣的方法,而且在讀取props和state時也能保持同樣的展現結果,並且不只僅在React中,你也能夠在任何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太晚了,而不是咱們使用何種語法。可是,若是咱們徹底依賴於js的閉包,問題就會獲得解決。
閉包一般是被避免的,由於它很難考慮一個隨時間變化的值。可是在React中,props和state應該是不可變的。
這意味着,若是去掉某個特定render中的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>;
}
}
複製代碼
已經在render時「捕獲」到了props。
這樣,它裏面的任何代碼(包括showMessage)均可以保證取到某個特定render中的props了。
而後咱們能夠添加不少的helper函數,他們均可以捕獲到props和state。閉包救了咱們。
上面的例子是正確的,但看起來很奇怪。若是隻是在render中定義函數而不是使用類方法,那麼咱們使用一個class又有什麼意義呢?
實際上咱們能夠經過移除class來簡化代碼:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
複製代碼
就像上面所說的,props仍然能夠被捕獲到,React將它做爲一個參數傳遞。不一樣的是,props對象自己不會因React而發生變化了。
在下面中就更明顯了:
function ProfilePage({ user }) {
const showMessage = () => {
alert('Followed ' + user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
複製代碼
當父組件根據不一樣的props渲染時,React將會再次調用function,可是咱們點擊的事件處理函數是上一個包含user值的render,而且showMessage函數已經拿到了user這個值
這就是爲何在這個版本的function demo中在Sophie主頁點擊Follow,而後改變select,將會alert 「Followed Sophie」。
如今咱們知道了在React中 function 和 class的最大不一樣。
function components捕獲渲染值(capture value)
對於鉤子,一樣的原理也適用於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
這個例子說明了相同點:在點擊send按鈕後,再次修改輸入框的值,3秒後的輸出依然是點擊前輸入框的值。這說明function Hooks一樣具備capture value
的特性。
因此咱們知道了在React中function默認狀況下會捕獲props和state(capture value
)。可是若是咱們想要去避免這個capture value
呢?
在class中,咱們能夠經過使用this.props和this.state,由於this
本事是可變的,React改變了它,在function components
中,還有一個被全部組件所共享的可變值,被叫作ref
:
function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// ...
}
複製代碼
可是,你必須本身管理它。
ref和實例字段有着相同的做用,你也許更爲熟悉「dom refs」,可是這個概念更爲廣泛,它僅僅是一個「放置一些東西的通用容器」。
儘管看起來它像是某一些東西的鏡像,但實際上他們表示着相同的概念。
React默認不會爲function components
建立保存最新props和state的refs。由於不少狀況下你是不須要他們的,而且分配他們也很浪費時間。可是須要的時候能夠手動的去跟蹤值:
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;
};
複製代碼
這時咱們發現,在點擊send按鈕後繼續輸入,3秒後alert的是點擊按鈕後輸入的值而不是點擊按鈕錢輸入的值。
一般,應該避免在渲染期間讀取或設置refs,由於它們是可變的。咱們但願保持渲染的可預測性。可是,若是咱們想要獲取特定props或state的最新值,手動更新ref可能會很煩人。咱們能夠經過使用一種效果來實現自動化(useEffect在每次render都會執行):
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
咱們在effect
中進行賦值,以便ref的值只在DOM更新後才更改。
像這樣使用ref不是常常須要的。一般capture props或state纔是默認更好的選擇。可是,在處理諸如間隔和訂閱之類的命令式API時,它很是方便。記住,您能夠跟蹤任何這樣的值:一個prop、一個state變量、整個props對象,甚至一個函數。
在本文中,咱們研究了class中常見的中斷模式,以及閉包如何幫助咱們修復它。可是,你可能已經注意到,當你試圖經過指定依賴數組來優化Hooks時,可能會遇到帶有過期閉包的錯誤。這是否意味着閉包是問題所在?我不這麼認爲。
正如咱們上面所看到的,閉包實際上幫助咱們解決了難以注意到的細微問題。相似地,它們使在併發模式下正確地編寫代碼變得更加容易。
到目前爲止,我所看到的全部狀況下,「過期的閉包」問題都是因爲錯誤地假設「函數不更改」或「props老是相同」而發生的。事實並不是如此,我但願這篇文章可以幫助澄清。
function components沒有props和state,所以它們的也一樣重要。這不是bug,而是function components的一個特性。例如,函數不該該從useEffect或useCallback的「依賴項數組」中被排除。(正確的解決方案一般是上面的useReducer或useRef解決方案。)
當咱們用函數編寫大多數React代碼時,咱們須要調整優化代碼的直覺,以及什麼值會隨着時間而改變。
正如Fredrik所說:
對於Hooks,我迄今爲止發現的最好的規則是「代碼就像任何值在任什麼時候候均可以改變」。
React的function老是捕捉它們的值(capture value)—— 如今咱們知道爲何了。