擁抱 React Hooks
我們知道,React 提供的單向資料流以及元件化幫助我們將一個龐大的專案變為小型、獨立、可複用的元件。但有時,我們無法進一步拆分很複雜的元件 ,因為它們內部的邏輯是有狀態的 ,無法抽象為函式式元件。所以有時我們可能會寫出非常不適合複用性開發的:
- 巨大的元件 難以重構
- 重複的邏輯 需要在多個元件的多個生命週期中寫重複的程式碼
- 複雜的應用模式 類似於 render props 於 高階元件
但謝天謝地,Hooks 的出現,讓我們把元件內部的邏輯組織成為了可複用的隔離單元 。
Hooks 要解決的問題:
跨元件地複用包含狀態的邏輯,通過 Hooks 可以將含有 state 的邏輯從組建抽象出來,同時也可以幫助我們在不重寫元件結構 的情況下複用邏輯。Hooks 一般是用於函式式元件的,在類class元件中無效。讓我們根據程式碼的作用將它們拆分,而不是生命週期。簡而言之, Hooks 實現了我們在函式式元件中使用狀態變數 與類似 於生命週期的操作。
使用 Hooks 的語法規則
- 只能在頂層呼叫鉤子。不在迴圈、控制流和巢狀的函式中呼叫鉤子。
- 只能從React的函式式元件中呼叫鉤子。不在常規的JS函式中呼叫鉤子。
建立Hooks
-
使用
useState
建立Hook
import {useState} from 'react'; function hooks(){ // 宣告一個名為 count 的新狀態變數 const [count, setCount] = useState(0); // 第二個引數 setCount 為一個可以更新狀態的函式 // useState 的引數即為初始值 return ( <div> <p>當前的狀態量為: {count}</p> <button onClick={() => setCount(count + 1)}>點選加一</button> </div> ) } 複製程式碼
-
使用
useEffect
來執行相應操作
import {useState, useEffect} from 'react'; function hooks(){ const [count, setCount] = useState(0); // 類似於 componentDidMount 和 componentDidUpdate // 在 useEffect 中可以使用組建的 state 和 props // 在每次渲染後都執行 useEffect useEffect(() => { window.alert(`You have clicked ${count} times`); }) return ( <div> <p>當前的狀態量為: {count}</p> <button onClick={() => setCount(count + 1)}>點選加一</button> </div> ) } 複製程式碼
鉤子是獨立的
我們在兩個不同的元件使用同一個鉤子,他們是相互獨立的,甚至在一個元件使用兩個鉤子他們也是相互獨立的。
React如何保證useState相互獨立
React 其實是根據useState
傳出現的順序來保證useState
之間相互獨立。
// 首次渲染 const [num, setNum] = useState(1); // 將num初始化為1 const [str, setStr] = useState('string'); // 將str初始化為'string' const [obj, setObj] = useState({id:1}); // .... // 第二次渲染 const [num, setNum] = useState(1); // 讀取狀態變數num的值, 此時傳入的引數已被忽略,下同 const [str, setStr] = useState('string'); // 讀取狀態變數str的值 const [obj, setObj] = useState({id:1}); // .... 複製程式碼
同時正是由於根據順序保證獨立,所以 React 規定我們必須把 hooks 寫在最外層,而不能寫在條件語句之中,來確保hooks的執行順序一致,若要進行條件判斷,我們應該在useEffect
的函式中寫入條件
Effect Hooks
useEffect 來傳遞給 React 一個方法,React會在進行了 DOM 更新之後呼叫。我們通常將 useEffect 放入元件內部,這樣我們可以直接訪問 state 與 props。記得,useEffect 在每次 render 後都要呼叫。
需要清理的Effect
我們有時需要從外部資料來源
獲取資料,此時我們就要保證清理Effect來避免記憶體洩露 ,此時我們需要在 effect 中返回一個函式來清理它, React 會在元件每次接觸掛載的時候清理。一個比較使用的場景就是我們在useEffect
中若執行了非同步請求,由於非同步的時間不確定性,我們很需要在執行下一次非同步請求時先結束上一次的請求,因此我們就需要清理。
useEffect(() => { let canceled = false; const getData = async () => { const res = await fetch(api); if(!canceled) { // 展示 res } } getData(); // return 的即為我們的清理函式 return () => { canceled = true; } }); 複製程式碼
此時我們在進行重新渲染時,就可以避免非同步請求帶來的競態問題,從而避免資料的不穩定性。
配置根據條件執行的Effect
我們可以給useEffect
傳入第二個引數只有當第二個引數(陣列)裡的所有的state 值發生變化時,才重新執行Effect
useEffect(() => { window.alert(`you had clicked ${count} times`); }, [count]); //只有當 count 發生變化時才會重新執行effect 複製程式碼
在函式式元件使用例項
由於函式式元件中沒有 this ,所以我們無法使用ref,但hooks幫助我們解決了這個問題,他提供了useRef
方法來為我們建立一個例項,而傳入的引數會被掛載在這個例項的.current
屬性上,返回的例項會持續到整個生命週期結束為止。
function RefExample() { const ref1 = useRef(null); return ( <div> <input ref={ref1} type="text" /> <button onClick={() => {ref1.current.focus()}} </div> ) } 複製程式碼
型別的Hooks
如果比起上面的狀態變數型別,你更想要使用 Redux 型別的狀態管理,OK,React 也給我們提供了useReducer
這個方法。作為useState
的一種替代,我們可以使用dispatch
方法來改變狀態變數。
// 初始化的狀態變數 const initState = {count:0}; // 編寫 reducer 處理函式 function reducer(state, action) { switch(action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; } } function counter({initState}) { const [state, dispatch] = useReducer(reducer, initState); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </div> ) } 複製程式碼
回撥形式的Hooks
我們可以通過監聽狀態變數並在變換後執行回撥函式來執行 Effect ,此時你可能會問,為什麼使用 Hooks 會使用這麼多的 inline 函式,豈不是很影響效能? 謝天謝地,JavaScript 中的閉包函式
的效能十分的快,它幫助了我們很多。回撥形式的 Hooks 有兩種,useCallback
與useMemo
.
二者的轉換關係為:
useCallback(fn, inputs) === useMemo(() => fn, inputs)
useCallback
是如何幫助我們提升效能的呢? 實際上,它其實是快取了每次渲染時的 inline 回撥函式的例項,之後無論是配合shouldComponentUpdate
或者是React.memo
都能夠達到減少不必要的渲染的作用。這也提示我們,React.memo
和React.useCallback
一般是配合使用,缺了其一都可能無法達到提升效能的功效。
下面以一個表單元件表示使用方法
function FormComponent() { const [text, setText] = useState(' '); const handleSubmit = useCallback(() => { console.log(`new test is ${text}`); }, [text]); return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} /> <BigTree onSubmit={handleSubmit} /> // 巨大無比的元件,不優化卡的不行 </div> ) } 複製程式碼
但此時有一個很嚴重的問題,就是我們的 BigTree 依賴於一個太容易變化的 state, 只要我們在input框隨意輸入, BigTree 就會重新渲染好多次來獲取最新的callback,此時這個callback就無法使用快取了。
一個解決辦法是我們定義一個新的例項,這個例項只有在 re-render 時才會更新最新的值,這樣我們就可以不根據一個經常變換的state,而是根據一個在useLayoutEffect
中更新的ref例項來更新。
function FormComponent() { const [text, setText] = useState(' '); const textRef = useRef(); useLayoutEffect(() => { textRef.current = text; }) const handleSubmit = useCallback(() => { console.log(`new test is ${text}`); }, [textRef]); // 只根據 textRef 的變化而產生變化,並不會在 text 改變就變化 return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} /> <BigTree onSubmit={handleSubmit} /> // 巨大無比的元件,不優化卡的不行 </div> ) } 複製程式碼
Hooks的多重 Effect 更新場景
useLayoutEffect
DOM 突變之後,重新繪製之前同步觸發
它與useEffect
的作用相同,都是用來執行副作用的,但不同的是,它會在所有的 DOM 變更結束後同步地呼叫 effect。一個與useEffect
很大的區別是,useLayoutEffect
是同步地,而useEffect
是非同步的,在瀏覽器重新繪製頁面佈局前,useLayoutEffect
內部的更新將會同步重新整理,但官方給出的建議是儘量使用useEffect
來避免阻塞視覺更新。