與React類組件相比,React函數式組件究竟有何不一樣?html
通常的回答都是:前端
state
,那若是有 Hooks
以後呢?函數組件性能比類組件好,可是在現代瀏覽器中,閉包和類的原始性能只有在極端場景下才會有明顯的差異。react
而下面會重點講述:React的函數式組件和類組件之間根本的區別: 在心智模型上。git
函數式組件以來,它一直存在,可是常常被忽略:函數式組件捕獲了渲染所用的值。(Function components capture the rendered values.)github
思考這個組件:數組
function ProfilePage(props) { const showMessage = () => alert('你好 ' + props.user); const handleClick = () => setTimeout(showMessage, 3000); return <button onClick={handleClick}>Follow</button> }
上述組件:若是 props.user
是 Dan
,它會在三秒後顯示 你好 Dan
。瀏覽器
若是是類組件咱們怎麼寫?一個簡單的重構可能就象這樣:閉包
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 應用程序中的一個常見錯誤來講明其中的不一樣。函數
咱們添加一個父組件,用一個下拉框來更改傳遞給子組件(ProfilePage
),的 props.user
,實例地址:(https://codesandbox.io/s/pjqn... 。
按步驟完成如下操做:
這時會獲得一個奇怪的結果:
ProfilePage
, 當前帳號是 Dan 時點擊 Follow 按鈕,而後立馬切換當前帳號到 Sophie,彈出的文本將依舊是 'Followed Dan'
。ProfilePage
, 彈出的文本將是 'Followed Sophie'
:在這個例子中,函數組件是正確的。 若是我關注一我的,而後導航到另外一我的的帳號,個人組件不該該混淆我關注了誰。 ,而類組件的實現很明顯是錯誤的。
因此爲何咱們的例子中類組件會有這樣的表現? 讓咱們仔細看看類組件中的 showMessage
方法:
showMessage = () => { alert('Followed ' + this.props.user); };
這個類方法從 this.props.user
中讀取數據。
this
是並且永遠是 可變(mutable)的。**這也是類組件 this
存在的意義:能在渲染方法以及生命週期方法中獲得最新的實例。
因此若是在請求已經發出的狀況下咱們的組件進行了從新渲染, this.props
將會改變。 showMessage
方法從一個"過於新"的 props
中獲得了 user
。
從 this 中讀取數據的這種行爲,調用一個回調函數讀取 this.props
的 timeout 會讓 showMessage
回調並無與任何一個特定的渲染"綁定"在一塊兒,因此它"失去"了正確的 props。。
咱們想要以某種方式"修復"擁有正確 props 的渲染與讀取這些 props 的 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}>Followbutton>; } }
然而,這種方法使得代碼明顯變得更加冗長。若是咱們須要的不止是一個props 該怎麼辦? 若是咱們還須要訪問state 又該怎麼辦? 若是 showMessage
調用了另外一個方法,而後那個方法中讀取了 this.props.something
或者 this.state.something
,咱們又將遇到一樣的問題。而後咱們不得不將 this.props
和 this.state
以函數參數的形式在被 showMessage
調用的每一個方法中一路傳遞下去。
這樣的作法破壞了類提供的工程學。同時這也很難讓人去記住傳遞的變量或者強制執行,這也是爲何人們老是在解決bugs。
這個問題能夠在任何一個將數據放入相似 this
這樣的可變對象中的UI庫中重現它(不只只存在 React 中)
若是你在一次特定的渲染中捕獲那一次渲染所用的props或者state,你會發現他們老是會保持一致,就如同你的預期那樣:
class ProfilePage extends React.Component { render() { const props = this.props; const showMessage = () => { alert('Followed ' + props.user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return <button onClick={handleClick}>Follow</button>; } }
你在渲染的時候就已經"捕獲"了props:。這樣,在它內部的任何代碼(包括 showMessage
)都保證能夠獲得這一次特定渲染所使用的props。
可是:若是你在 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
, props
對象自己永遠不會被React改變。
當父組件使用不一樣的props來渲染 ProfilePage
時,React會再次調用 ProfilePage
函數。可是咱們點擊的事件處理函數,"屬於"具備本身的 user
值的上一次渲染,而且 showMessage
回調函數也能讀取到這個值。它們都保持無缺無損。
這就是爲何,在上面那個的函數式版本中,點擊關注帳號1,而後改變選擇爲帳號2,仍舊會彈出 'Followed 帳號1'
:
函數式組件捕獲了渲染所使用的值。
使用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> </>; }
若是我發送一條特定的消息,組件不該該對實際發送的是哪條消息感到困惑。這個函數組件的 message
變量捕獲了"屬於"返回了被瀏覽器調用的單擊處理函數的那一次渲染。因此當我點擊"發送"時 message
被設置爲那一刻在input中輸入的內容。
所以咱們知道,在默認狀況下React中的函數會捕獲props和state。 可是若是咱們想要讀取並不屬於這一次特定渲染的,最新的props和state呢?若是咱們想要["從將來讀取他們"]呢?
在類中,你經過讀取 this.props
或者 this.state
來實現,由於 this
自己時可變的。React改變了它。在函數式組件中,你也能夠擁有一個在全部的組件渲染幀中共享的可變變量。它被成爲"ref":
function MyComponent() { const ref = useRef(null); }
可是,你必須本身管理它。
一個ref與一個實例字段扮演一樣的角色。這是進入可變的命令式的世界的後門。你可能熟悉'DOM refs',可是ref在概念上更爲普遍通用。它只是一個你能夠放東西進去的盒子。
甚至在視覺上, 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
,咱們將獲得在咱們按下發送按鈕那一刻的信息。可是當咱們讀取 latestMessage.current
,咱們將獲得最新的值 —— 即便咱們在按下發送按鈕後繼續輸入。
ref是一種"選擇退出"渲染一致性的方法,在某些狀況下會十分方便。
一般狀況下,你應該避免在渲染期間讀取或者設置refs,由於它們是可變得。咱們但願保持渲染的可預測性。 然而,若是咱們想要特定props或者state的最新值,那麼手動更新ref會有些煩人。咱們能夠經過使用一個effect來自動化實現它:
function MessageThread() { const [message, setMessage] = useState(''); const latestMessage = useRef(''); useEffect(() => { latestMessage.current = message; }); const showMessage = () => { alert('You said: ' + latestMessage.current); };
咱們在一個effect 內部執行賦值操做以便讓ref的值只會在DOM被更新後纔會改變。這確保了咱們的變量突變不會破壞依賴於可中斷渲染的時間切片和 Suspense 等特性。
一般來講使用這樣的ref並非很是地必要。 捕獲props和state一般是更好的默認值。 然而,在處理相似於intervals和訂閱這樣的命令式API時,ref會十分便利。你能夠像這樣跟蹤 任何值 —— 一個prop,一個state變量,整個props對象,或者甚至一個函數。
這種模式對於優化來講也很方便 —— 例如當 useCallback
自己常常改變時。然而,使用一個reducer 一般是一個更好的解決方式
閉包幫咱們解決了很難注意到的細微問題。一樣,它們也使得在併發模式下能更輕鬆地編寫可以正確運行的代碼。這是可行的,由於組件內部的邏輯在渲染它時捕獲幷包含了正確的props和state。
函數捕獲了他們的props和state —— 所以它們的標識也一樣重要。這不是一個bug,而是一個函數式組件的特性。例如,對於 useEffect
或者 useCallback
來講,函數不該該被排除在"依賴數組"以外。(正確的解決方案一般是使用上面說過的 useReducer
或者 useRef
)
當咱們用函數來編寫大部分的React代碼時,咱們須要調整關於優化代碼和什麼變量會隨着時間改變的認知與直覺。
到目前爲止,我發現的有關於hooks的最好的內心規則是"寫代碼時要認爲任何值均可以隨時更改"。
React函數老是捕獲他們的值 —— 如今咱們也知道這是爲何了。
文章參考:React做者 Dan Abramov 的github