React function 組件和 React classes 有什麼不一樣?javascript
之前,一個標準答案是說 classes 提供更多的功能(例如 state)。有了 Hooks,便不是這樣了。html
可能你聽過其中一個性能更好。哪個?許多這樣的性能基準都存在缺陷,因此我會當心地從中得出結論。性能主要取決於代碼而不是選擇一個 function 或者 一個 class。在咱們觀察中,即便優化策略有所不一樣,但性能的差距其實微乎其微。java
另外一方面咱們不推薦重寫你寫好的組件,除非你有其餘緣由且不介意成爲早期試驗者。Hooks 仍然很新(就像 2014 年的 React),而且一些「最佳實踐」還未寫進教程。react
React function 和 classes 是否存在本質上的區別?固然,它們 —— 在心智模型中。在這篇文章裏,我會看看它們之間的最大區別。這在2015年的 function components 中介紹過,但它常常被忽視了:git
Function 組件捕獲 render 後的值。github
讓咱們來分析下這是什麼意思。編程
注意:這片文章不作 classes 或者 functions 的價值衡量,我只描述兩種編程模型在 React 中的區別。更多關於採用 functions 的問題,請參閱 Hooks 常見問題解答。數組
思考這個組件:瀏覽器
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
複製代碼
它展現一個帶有模擬網絡請求的 setTimeout
且以後會在確認彈窗中現實出來的按鈕。例如,若是 props.user
爲 'Dan'
,它會在三秒後顯示'Followed Dan'
,很是簡單。網絡
(請注意,上面例子中不管我是否使用箭頭仍是普通函數,function handleClick()
確定是同樣效果的。)
那咱們把它寫成 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>;
}
}
複製代碼
通常會認爲這兩段代碼是等效的,你們常常在這些模式中隨意的重構,而不會注意到它們的含義:
可是,這兩段代碼略微不一樣。 好好看看它們,有看出不一樣了嗎?就我的而言,我花了好一會才發現。
前面有劇透,若是你想本身找到的話,這是一個在線demo。文章接下來的部分來分析這個差別及爲何會這樣。
在咱們繼續以前,我想強調下,我所描述的差別與 React Hooks 自身無關,上面的例子甚至不須要用 Hooks!
這徹底是關於 functions 和 classes 在 React 中的區別的,若是你打算在 React 應用中更經常使用 functions,你可能想去弄懂它。
咱們將經過 React 應用中常見的一個 bug 來講明這區別。
使用當前的條目選擇器和以前兩個 ProfilePage
實現來打開這個 sandbox 例子 —— 每一個渲染一個 Follow 按鈕。
按照這種操做順序使用兩個按鈕:
你會注意到一個特殊的區別:
當爲 function 的 ProfilePage
時,點擊 Follow Dan 的條目而後切換成 Sophie 的,仍然彈出 'Followed Dan'
。
當爲 class 的 ProfilePage
時,它會彈出 'Followed Sophie'
:
這個例子中,第一種行爲是正確的。若是你關注一我的,而後切換到另一我的的條目,個人組件不該該困惑於我要關注的是誰。class 的實現明顯是個錯誤。
因此爲何咱們的 class 例子會以這種方式運行?
讓咱們仔細看看 class 中 showMessage
方法:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
複製代碼
這個 class 方法讀取了 this.props.user
,Props 在 React 中是不可變的。 可是,this
是,且已經改變了。
實際上,這就是在 class 中有 this
的目的,React 自己會隨着時間的推移而變異,以便你在能夠渲染和生命週期中獲取到新版本。
因此若是咱們組件在處於請求狀態時重渲染,this.props
會發生改變。 showMessage
方法從「太新」的 props
中獲取 user
。
這暴露了一個 UI 層性質上的有趣現象。若是咱們說 UI 在概念上是當前應用程序狀態的函數,則事件處理程序是渲染結果的一部分 —— 就像視覺輸出同樣。咱們的事件處理程序「屬於」具備特定 props 和 state 的特定 render。
可是,調度一個回掉讀取 this.props
的 timeout 會中斷該聯繫。咱們的 showMessage
回調沒有「綁定」到任何特定 render 上,所以它「丟失」了正確的 props,而讀取了 this
切斷這種聯繫。
能夠說 function 組件不存在這個問題。咱們要這麼解決這個問題?
咱們想以某種方式 「修復」 有正確 props 的 render
與獲取它們的 showMessage
回調之間的聯繫。沿着這種方式 props
會跟丟。
一種方法是在事件早期就讀取 this.props
,而後顯示地將它們傳遞到 timeout 處理程序:
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>;
}
}
複製代碼
這樣可行。可是,這種方法使代碼明顯更加冗餘,且隨着時間推移容易出錯。若是咱們須要超過一個 prop 怎麼辦?若是咱們也須要獲取 state 怎麼辦?若是 showMessage
調用其餘方法,且這個方法讀取 this.props.something
或 this.state.something
,咱們會再次遇到一樣的問題。因此咱們不得不將 this.props
和 this.state
作爲參數傳給每一個調用了 showMessage
的方法。
這樣作一般會破壞一般由 class 提供的人體工程學,也難以記住或強制執行,這就是你們常常出現 bugs 的緣由。
相似的,把 alert
放入 handleClick
中也沒法解決這個難題。咱們但願以容許拆分更多方法的方式構造代碼,同時咱們還要讀取與該調用相關 render 的對應 props 和 state。這個問題甚至不是 React 獨有的 —— 你能夠在任何將數據放入像 this
可變對象的 UI 庫中重現它。
或許,咱們能夠在 constructor 裏 bind 方法?
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 是不能夠變的!(或者至少,這是一個強烈推薦。)這去除了閉包的一個殺手鐗。
這意味着若是你封鎖一個特定 render 的 props 或 state,你老是能夠獲取相同的它們:
class ProfilePage extends React.Component {
render() {
// 捕獲 props!
const props = this.props;
// 注意: 咱們在 *render 裏面*
// 這不是 class 方法。
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
複製代碼
你在 render 時已經「捕獲」到 props 了:
這樣,它內部的任何代碼(包括 showMessage
)均可以保證看到這個特定 render 的 props,React 不會再「動咱們的奶酪」了。
咱們在裏邊添加多少個輔助方法均可以,而且它們全都使用被捕獲的 props 和 state,救回了閉包。
上面的例子沒有錯但看起來奇怪。若是在 render 中定義函數而不是使用 class 的方法,那還要 class 作什麼?
事實上,咱們能夠去掉 class 這個「殼」來簡化代碼:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
複製代碼
就像上面那樣,props
仍然被捕獲了 —— React 用參數形式傳遞它們。不像 this
,props
對象自己沒有被 React 改變。
若是在 function 定義時解構 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
值的上一個 render 和讀取它的 showMessage
回調,它們都無缺無損。
這就是爲何,在這個 demo 的 function 版本中,在 Sophie 的條目時點擊 Follow 以後切換成 Sunil 會彈出 'Followed Sophie'
:
這反應是正確的。(雖然你也可能想關注 Sunil!)
如今咱們明白了 functions 與 classes 在 React 中的最大不一樣了:
Function 組件捕獲 渲染後的值。
使用 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。])
雖然這不是一個好的消息應用 UI,它實現了一樣的東西:若是我發送一個特定的信息,這個組件不該該困惑於發送哪一個消息。這個 function 組件的消息
捕獲了 state 且「屬於」返回被瀏覽器點擊事件調用的 render。因此這個消息
被設定爲當我點擊」發送「時 input 裏的值。
因此咱們知道 React 中的 functions 會默認捕獲 props 和 state。但若是咱們但願讀取的是最新的 props 或者 state,它們不屬於特定的 render 要怎麼辦?若是咱們想在將來裏讀取到它們怎麼辦?
在 classes 中,你能夠讀取 this.props
或 this.state
,由於 this
自己是可變的,React 會改變它。在 function 組件中,你也能夠有一個共享於全部組件 renders 的可變值,它叫作 「ref」:
function MyComponent() {
const ref = useRef(null);
// 你能夠讀寫 `ref.current`。
// ...
}
複製代碼
可是,你須要本身管理它。
ref 和實例字段扮演相同的角色,它是進入可變命令世界的逃脫倉。你可能熟悉 「DOM refs」,但這個原理要通俗的多,它只是一個你能夠往裏面放東西的箱子。
即使在視覺上,this.someting
看起來像 something.current
的鏡像。它們表明了相同的概念。
默認狀況下,在 function 組件中 React 不會建立最新 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;
};
複製代碼
若是咱們讀取 showMessage
的 message
,咱們會看到咱們第幾發送按鈕時的消息。但當咱們讀取 latestMessage.current
,咱們獲取到的是最新的值 —— 即便咱們在按下發送按鈕後繼續輸入。
你能夠比較這兩個 demos 看看區別。ref 是一種「選擇退出」渲染一致的方法,在某些狀況下能夠很方便。
一般你應該避免在渲染期間讀取或設置 refs,由於它們是可變的。咱們想保持渲染的可預測性。可是,若是咱們想獲取到特定 prop 或 state 最新的值,手動更新 ref 會很麻煩。咱們能夠用 effect 自動化它:
function MessageThread() {
const [message, setMessage] = useState('');
// 保持 track 是最新值
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
複製代碼
(這是一個demo。)
咱們在 effect 裏面賦值來實現 ref 值只在 DOM 被更新時才改變。這確保咱們的變異不會破壞像 Time Slicing 和 Suspense 等中斷渲染的功能。
不多會像這樣去使用 ref,捕獲 props 或 state 默認下是更好的。然而,在處理像定時器和訂閱這樣的棘手地 APIs 時是很方便的。記住你能夠跟蹤任何這樣的值 —— prop、state 變量、整個 props 對象,甚至是一個 function。
這種模式也能夠用來作優化 —— 例如在 useCallback
標示頻繁改變時。可是,使用一個 reducer 一般是一個更好的解決方案。(這個會在之後的博客文章中寫!)
這片文章裏,咱們看到在 classes 中的廣泛破壞模式,及閉包是如何幫助咱們修復它的。可是,你可能注意到了當你試着經過指定依賴數組來優化 Hooks 時,你可能會遇到過期閉包帶來的 bugs。這意味着閉包是問題?我也不這麼認爲。
正如咱們以前所見,閉包確實幫助咱們修復了難以注意到的細微問題。一樣地,它們使編寫在併發模式下的代碼正常工做變得更簡單。這多是由於組件內部的邏輯在渲染後封鎖正確的 props 和 state。
在目前爲止看到的全部狀況中,「過期閉包」問題發生是因爲 「functions 不發生變化」 或 「props 老是相同」的錯誤假設。事實並不是如此,我但願這篇文章有助於澄清。
Functions 鎖住它們的 props 和 state —— 全部它們是什麼很重要。這不是一個 bug,而是一個 function 組件的特性。例如,Functions 不該該從 userEffect
或 useCallback
的「依賴數組」中被排除。(上面提到經常使用的適當修復無論是 useReducer
或是 useRef
的解決方案 —— 咱們很快會在文檔中說明如何在它們之間作選擇)
在咱們用 functions 寫大多數 React 代碼時,咱們須要適配咱們的關於 優化代碼 和 什麼值會一直改變的狀況。
到目前爲止我用 hooks 找到的最好的心理規則是 「代碼的任何值彷佛能夠在任意時間改變」。
Functions 也不例外。這須要一些時間才能在 React 學習材料裏面變成廣泛的知識,從 class 心態過來的須要一些適應,但我但願這篇文章能夠幫助你用新的眼光看待它。
React functions 總會捕獲它們的值 —— 且如今咱們知道爲何了。
它們是一個徹底不一樣的神奇寶貝。
翻譯原文How Are Function Components Different from Classes?(2019-03-03)