React Hooks 實現和由來以及解決的問題詳解
與React類元件相比,React函式式元件究竟有何不同?
一般的回答都是:
- 類元件比函式式元件多了更多的特性,比如 state,那如果有 Hooks 之後呢?
- 函式元件效能比類元件好,但是在現代瀏覽器中,閉包和類的原始效能只有在極端場景下才會有明顯的差別。
- 效能主要取決於程式碼的作用,而不是選擇函式式還是類元件。儘管優化策略有差別,但效能差異可以忽略不計。
- 參考官網:(https://zh-hans.reactjs.org/docs/hooks-faq.html#are-hooks-slow-because-of-creating-functions-in-render)
- 參考作者github:(https://github.com/ryardley/hooks-perf-issues/pull/2)
而下面會重點講述:React的函式式元件和類元件之間根本的區別: 在心智模型上。
簡單的案例
函式式元件以來,它一直存在,但是經常被忽略:函式式元件捕獲了渲染所用的值。(Function components capture the rendered values.)
思考這個元件:
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/pjqnl16lm7)
按步驟完成以下操作:
- 點選 其中某一個 Follow 按鈕。
- 在3秒內 切換 選中的賬號。
- 檢視 彈出的文字。
這時會得到一個奇怪的結果:
- 當使用 函式式元件 實現的 ProfilePage,當前賬號是 Dan 時點選 Follow 按鈕,然後立馬切換當前賬號到 Sophie,彈出的文字將依舊是 'Followed Dan'。
- 當使用 類元件 實現的 ProfilePage,彈出的文字將是 'Followed Sophie':
在這個例子中,函式元件是正確的。 如果我關注一個人,然後導航到另一個人的賬號,我的元件不應該混淆我關注了誰。 ,而類元件的實現很明顯是錯誤的。
案例解析
所以為什麼我們的例子中類元件會有這樣的表現? 讓我們仔細看看類元件中的 showMessage 方法:
showMessage = () => { alert('Followed ' + this.props.user); };
這個類方法從 this.props.user 中讀取資料。
在 React 中 Props 是 不可變(immutable)的,所以他們永遠不會改變。
而 this 是而且永遠是 可變(mutable)的。**
這也是類元件 this 存在的意義:能在渲染方法以及生命週期方法中得到最新的例項。
所以如果在請求已經發出的情況下我們的元件進行了重新渲染, this.props將會改變。 showMessage方法從一個"過於新"的 props中得到了 user。
從 this 中讀取資料的這種行為,呼叫一個回撥函式讀取 this.props 的 timeout 會讓 showMessage 回撥並沒有與任何一個特定的渲染"繫結"在一起,所以它"失去"了正確的 props。。
如何用類元件解決上述BUG?(假設函式式元件不存在)
我們想要以某種方式"修復"擁有正確 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 中)
方法二:如果我們能利用JavaScript閉包的話問題將迎刃而解。*
如果你在一次特定的渲染中捕獲那一次渲染所用的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。
Hooks 的由來
但是:如果你在 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
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。