話外:隨着 react 新特性 HOOKS 的提出,最新不少人開始討論 react 裏面的函數組件和 class 組件到底有什麼不一樣?這裏是 Dan Abramov 的一篇文章 How Are Function Components Different From Classes, 下面是對這篇文章的翻譯,大體意思大體表達出來了,不足的地方,請你們多多指教 :)html
React 函數組件和 React 類有何不一樣?react
有一段時間,規範的答案是類提供了更多的功能(如狀態)。有了 Hooks,就再也不那麼正確了。git
也許你據說其中一個的性能更好。那是哪個呢?許多基於此類的論證過程都存在缺陷,所以我會謹慎地從中得出結論。性能主要取決於代碼作了什麼,而不是您選擇的是函數仍是類。在咱們的觀察中,雖然優化策略略有不一樣,但性能差別能夠忽略不計。github
在任何一種狀況下,除非您有其餘緣由或者不介意成爲第一個使用它的人,不然咱們不建議您使用HOOKS重寫現有組件。 Hooks 仍然是新的(就像 2014 年的 React 同樣),而且一些「最佳實踐」尚未在教程中提到。數組
那咱們該怎麼辦呢? React 函數和類之間是否有任何根本的區別?固然,還有在心理模型中。在這篇文章中,我將看看它們之間的最大區別。函數組件自 2015 年推出以來,它就一直存在,但卻常常被忽視:瀏覽器
函數組件捕獲已渲染的值。網絡
讓咱們來看看這是什麼意思。閉包
注意:這篇文章不是重在評價類或者函數。我只闡述在 react 中的這兩個語法模型之間的區別。關於更普遍地採用函數式組件的問題,請參考 Hooks FQA併發
思考一下這個組件:ide
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
複製代碼
它顯示一個按鈕,使用 setTimeout
模擬網絡請求,而後顯示確認警告彈窗。例如,若是 props.user
爲 'Dan'
,那麼調用這個函數3s以後將會顯示 '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 中函數和類之間的區別。若是您計劃在 React 應用程序中更頻繁地使用函數式組件,那你可能想要了解它。
咱們將用 React 應用程序中常見的 bug 來講明它們之間的區別。
使用一個即時 profile 選擇器和上面的兩個 ProfilePage 實現打開這個示例沙箱 - 每一個都實現了一個 Follow 按鈕。
用兩個按鈕嘗試如下操做序列:
你將會注意到他們結果的差別:
ProfilePage
上,點擊跟隨 Dan 的配置,而後改變配置爲 Sophie 的,3s 後的警告任然是 'Followed Dan'
。ProfilePage
上,則將會警告 'Followed Sophie'
在這個例子中,第一個的行爲是正確的。若是我跟隨了一我的而後導航到另外一我的的配置,個人組件不該該對我到底跟隨了誰而感到困惑。這個類的實現顯然是錯誤的。
(儘管你可能想去關注 sophie)
爲何咱們使用類的結果是這樣的呢?
讓咱們仔細觀察咱們類中的 showMessage
方法。
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
複製代碼
這個類方法讀取 this.props.user
,Props 在 React 中是不可變的因此它永遠不會改變,可是 this
老是可變的。
實際上,這就是類中存在this的所有意義。React 自己會隨着時間而改變,以便您能夠在 render
和生命週期函數中讀取新版本。
所以,若是咱們的組件在請求運行時更新。this.props
將會改變。showMessage 方法從「最新」的 props
中讀取 user
。
這是揭示了關於用戶界面本質的一個有趣的現象。若是咱們說UI在概念上是當前應用程序狀態的函數,那麼事件處理函數就是渲染輸出的一部分,就像可視化輸出同樣。咱們的事件處理程序「屬於」具備特定的 props
和 state
的特定的 render
。
可是當定時器的回調函數讀取 this.props
時 打破了這種規則。咱們的 showMessage
回調函數沒有綁定到任何特定的 render
,因此它失去了正確的 props
, 從 this
裏讀取 props 切斷了這種關聯。
假設說函數組件不存在,咱們該怎麼解決這個問題呢?
咱們但願以某種方式在渲染以後「修復」 props 與讀取它們的 showMessage
回調之間的鏈接。由於Props
在傳遞的過程當中失去了正確的意義。
一個方法是在事件處理函數初始就讀取 this.props
,而後精確的將它傳遞到 setTimeout
的回調函數中。
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 怎麼辦?若是咱們還須要訪問狀態怎麼辦?若是 showMessage 調用另外一個方法,而且該方法讀取 this.props.something
或 this.state.something
,咱們將再次遇到徹底相同的問題。因此咱們必須將 this.props 和 this.state 做爲參數傳遞給 showMessage
調用的每一個方法。
這麼作不只不符合一般咱們對類的認知,同時也極其難以記錄並施行,最後代碼就會不可避免的出現 bug。
一樣,在 handleClick 中 alert
代碼並不能解決更大的問題。咱們想要使用一種能夠拆分爲多個方法的方式來構造代碼,同時也要讀取被調用時與之對應的參數和狀態,這個問題甚至不是 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
。
這樣,在這個特定的 render 函數內部的任何代碼(包括 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>
);
}
複製代碼
就像上面這樣,prop
依舊被捕獲到了。React 將他們像參數同樣進行傳遞進來,不一樣於 this
, 這個 props
對象自己永遠不會被React改變。
若是你在函數定義的時候對 props
解構賦值,效果會更加明顯
function ProfilePage({ user }) {
const showMessage = () => {
alert('Followed ' + user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
複製代碼
當父組件使用不一樣的 props
渲染 ProfilePage
時,React 會再一次調用 ProfilePage
函數。可是咱們已經點擊的事件處理程序「屬於」前一個 render
函數,他本身的 showMessage
回調函數讀取的 user
值沒有任何改變。
這就是爲何在這個例子的函數版本中,點擊跟隨 sophie's 配置以後改變配置爲 sunil 時會顯示 ‘Followed Sophie’
,
如今咱們理解了在 React 中函數和類之間最大的區別了:
函數組件獲取已經渲染過的值(Function components capture the rendered values.)
使用 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>
</>
);
}
複製代碼
(這裏是線上例子)
雖然這不是一個很是好的消息應用 UI,但它說明了一樣的觀點:若是我發送特定消息,組件不該該對實際發送的消息感到困惑。此函數組件的 message
捕獲到的是渲染的數據,而後被瀏覽器調用的單擊事件處理函數返回。所以當我點擊發送的時候 message
會被設置成我輸入的東西。
因此咱們知道 React 裏面的函數會默認捕獲 props
和 state
。但若是咱們想讀取不屬於特定 render 的最新 props
和 state
時該怎麼辦呢?就是若是咱們想從將來讀取它們該怎麼辦?
在類裏面,你能夠經過讀取 this.props
和 this.state
來作到,由於他們自己是可變的。由 React 來改變。在函數組件裏,你一樣有一個能夠被全部組件渲染共享的可變值 ,他被叫作 "ref"。
function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// ...
}
複製代碼
可是你必須本身去管理它。
ref 與實例字段扮演一樣的角色。它是進入可變命令世界的逃脫艙。您可能熟悉 「DOM refs」,但這個的概念更爲通用。它只是一個你能夠把東西放進去的盒子。
即便在視覺上,this.something
看起來就像是 something.current
同樣,它們表達的概念是相同的。
在函數組件裏 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
,當咱們按下 send 按鈕的時候咱們會看見這個 message,但當咱們讀取 latestMessage.current
時,咱們會獲得最新的值,即便是在咱們按下send按鈕以後繼續輸入的值。
你能夠經過這兩個例子來比較他們的不一樣。ref 是一種「選擇性退出」渲染一致性的方案,在某些狀況下能夠很方便。
一般狀況下,若是想要保持渲染的可預測性,您應該避免在渲染的時候讀取或者設置refs,由於他們是可變的。可是若是咱們想要得到特定的 props
和 state
的最新值,手動更新 ref 可能很煩人。咱們能夠經過使用 effect 自動更新它:
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);
};
複製代碼
這是一個例子
咱們在 effect 裏面作了賦值,因此 ref 的值只會在 DOM 更新以後纔會改變,這確保咱們的變化不會破壞依賴於可中斷渲染的功能。好比 Time Slicing and Suspense。
一般咱們不須要常用 ref。默認獲取 props
和 state
一般更好。可是,在處理 intervals 和 subscriptions 等命令式 API 時它會很方便。請記住,您能夠跟蹤任何值,好比:prop 和 state 變量、整個 prop 對象、甚至是函數。
此模式的優化也很方便 - 例如,當 useCallback 標識常常更改時。可是,使用 reducer 一般是更好的解決方案。 (將來博客文章的主題!)
在這篇文章中,咱們研究了類中常見的 bug,以及如何使用閉包來幫助咱們修復它。可是,您可能已經注意到,當您試圖經過指定依賴項數組來優化 Hooks 時,可能會遇到閉包還將來得及更新致使的 bug。這是否意味着閉包是問題所在呢?我不這麼認爲。
正如咱們在上面看到的,閉包實際上幫助咱們解決了難以注意到的細微問題。相似地,它們使編寫在併發模式下正確工做的代碼變得容易得多。這是可能的,由於組件內部的邏輯在渲染它時屏蔽了正確的 props 和 state。
到目前爲止,我所見過的全部狀況下,「陳舊的閉包」問題都是因爲錯誤地假設「函數不會改變」或 「 props 老是相同的」而發生的。事實並不是如此,我但願這篇文章有助於澄清這一點。
函數屏蔽了他們更新的 props
和 state
——所以它們的標識也一樣重要。這不是一個 bug,而是函數組件的一個特性。例如,對於 useEffect 或 useCallback,函數不該該被排除在「相關數組」以外。(正確的修復一般是 useReducer 或上面的 useRef 解決方案——咱們很快將在文檔中解釋如何在二者之間進行選擇)。
當咱們用函數編寫大多數 React 代碼時,咱們須要調整咱們關於優化代碼的直覺以及哪些值會隨時間變化
就像 Fredrik 寫的那樣:
到目前爲止,我所發現的關於 hook 的最好的心理預期是「代碼裏好像任何值均可以隨時更改」。
函數也不例外。這須要一些時間才能成爲 React 學習材料的常識。它須要從 class 的思惟方式進行一些調整。但我但願這篇文章能夠幫助你以新的眼光看待它。
React 函數老是捕獲它們的值 - 如今咱們知道緣由了。