Hook 前言
什麼是Hook
自從 16.8 版本開始,hooks 的出現使得你可以在不編寫 class 的情況下使用狀態管理以及其它 React 的特性。
那麼在 React Hooks 出現之前,class 類元件和 function 函式元件有什麼區別?Hooks 出現之後,函式元件又是如何滿足原來只有類元件才有的功能的?
1.類元件和沒有 hooks 加持的函式元件:
函式元件常被稱為無狀態元件,意思就是它內部沒有狀態管理,只能做一些展示型的元件或者是完全受控元件。因此差別主要體現在:
- 函式元件沒有內部狀態管理
- 函式元件內部沒有生命週期鉤子
- 函式元件不能被獲取元件例項 ref,函式元件內也不能獲取類元件的 ref
2.類元件和有 hooks 加持的函式元件:
有了 hooks 加持之後,函式元件具備了狀態管理,除了可以使用內建的 hooks ,我們還可以自定義 hooks。
- 類元件有完備的生命週期鉤子,而函式元件只能具備:DidMount / WillUnmount / DidUpdate / willUpdate
- 函式元件內部可以通過內建 hook 獲取類元件 ref,也可以通過一些 API 的組合使用達到獲取函式元件 ref 的功能
- 函式元件具備了針對狀態變數的 setter 監聽(類似於 vue watch),類元件沒有這種 API。(useCallback、useEffect、useMemo等)
類元件原本比函式元件更加完整,為什麼還需要 hooks?
這要說到 React 的設計理論:
- React 認為,UI 檢視是資料的一種視覺對映,即 UI = F(DATA) ,這裡的 F 需要負責對輸入的資料進行加工、並對資料的變更做出響應
- 公式裡的 F 在 React 裡抽象成元件,React 是以元件為粒度編排應用的,元件是程式碼複用的最小單元
- 在設計上,React 採用 props 來接收外部的資料,使用 state 屬性來管理元件自身產生的資料(狀態),而為了實現(執行時)對資料變更做出響應需要,React 採用基於類 Class 的元件設計
- 除此之外,React 認為元件是有生命週期的,因此開創性地將生命週期的概念引入到了元件設計,從元件的 create 到 destroy 提供了一系列的 API 共開發者使用
類元件 Class Component 的困局
元件狀態邏輯複用困局
對於有狀態元件的複用,React 團隊和社群嘗試過許多方案,早期使用 CreateClass + Mixins,使用 Class Component 後又設計了 Render Props 和 HOC,再到後來的 Hooks設計,React 團隊對於元件複用的探索一直沒有停止。
HOC 和 Render Props 都有自己的缺點,都不是完美的複用方案(詳情瞭解 React HOC 和 Render Props),官方團隊認為應該為共享狀態邏輯提供更好的原生途徑。在 Hooks 加持後,功能相對獨立的部分完全抽離到 hook 實現,例如網路請求、登入狀態、使用者核驗等;也可以將 UI 和功能(狀態)分離,功能放到 hook 實現,例如表單驗證。
複雜元件變得難以理解
我們經常維護一些元件,它們起初很簡單,但是逐漸會被狀態邏輯和副作用充斥。在多數情況下,不可能將元件拆分為更小的粒度,因為狀態邏輯無處不在。這也給測試帶來了挑戰。Hook 可將元件中相互關聯的部分拆分成更小的函式
JavaScript Class 的缺陷
- this的指向問題(語言缺陷)
- 編譯後體積和效能的問題
同樣功能的類元件和函式元件,在經過 Webpack 編譯後體積相差明顯,也伴隨著一定的效能問題。這是因為 class 在 JavaScript 中本質是函式,在 React 內部也是當做 Function類 來處理的。而函式元件編譯後就是一個普通的 function,function 對 JS 引擎是友好的。
內建 Hooks
useState
const [state, setState] = useState(initialState);
用來承擔與類元件中的 state 一樣的作用,元件內部的狀態管理
function () { const [ count, setCount ] = useState(0); const onClick = () => { setCount( count + 1 ); // setCount(count => count + 1); }; return <div onClick={onClick}>{ count }</div> }
除了直接傳入最新的值,還可以函式式更新,這樣可以訪問到先前的 state。如果你的初始 State 建立比較昂貴時,可以傳一個函式給 useState:
function Table(props) { // ⚠️ createRows() 每次渲染都會被呼叫 const [rows, setRows] = useState(createRows(props.count)); // ... } function Table(props) { // ✅ createRows() 只會被呼叫一次 const [rows, setRows] = useState(() => createRows(props.count)); // ... }
如果是複雜型別的 state,需要傳入修改後的完整的資料,不再像類元件中的 setState 可以自動合併物件,需要手動合併:
setState(prevState => ({...prevState, ...updatedValues}));
此外,useReducer 是另一種可選的方案。
useEffect
useEffect(func, [deps]);
可以用來模擬生命週期,即可以完成某些副作用。什麼叫副作用?一般我們認為一個函式不應該對外部產生影響,一旦在函式內部有某些影響外部的操作,將其稱之為副作用。例如改變 DOM、改變 Window物件(Global)、設定定時器、使用原生API繫結事件等等,如果處理不好,它們可能會產生 bug 併產生破壞。
如果只傳一個引數,每次元件渲染都會執行回撥函式(掛載+跟新),相當於 componentDidMount() + componentDidUpdate()
返回值函式:在元件更新前、元件解除安裝時執行,相當於 componentWillUnmount() + componentWillUpdate()
useEffect(() => { // 每次渲染後執行此函式,獲取到的值是最新的 console.log("Effect after render", count); return () => { // 每次執行useEffect前,先執行此函式,獲取到的資料是更新之前的值 console.log("remove last", count); } });
第二個引數是依賴列表,當依賴的狀態資料發生改變時會執行回撥
1.如果是一個空陣列,表示沒有依賴項
- 回撥函式:只在元件掛載的時候執行一次,相當於 componentDidMount()
- 返回值函式:只在元件解除安裝的時候執行一次,相當於 componentWillUnmount()
2.如果有值
- 回撥函式:除了具有 componentDidMount(),還當 陣列內的變數發生變化時執行 componentDidUpdate()
- 返回值函式:除了具有 componentWillUnmount(),還當 陣列內的值發生變化時執行 componentWillUpdate()
需要注意的是,
1.第二個引數的比較其實是淺比較,傳入引用型別進去是無意義的
2.一個元件內可以使用多個 useEffect,它們相互之間互不影響
3.useEffect 第一個引數不能是 async 非同步函式,因為它總是返回一個 Promise,這不是我們想要的。你可以在其內部定義 async 函式並呼叫
useLayoutEffect
它與 useEffect 的用法完全一樣,作用也基本相同,唯一的不同在於執行時機,它會在所有的 DOM 變更之後同步呼叫 effect,可以使用它來
useEffect 不會阻塞瀏覽器的繪製任務,它會在頁面更新之後才執行。而 useLayoutEffect 跟 componentDidMount 和 componentDidUpdate 的執行時機一樣,會阻塞頁面渲染,如果當中有耗時任務的話,頁面就會卡頓。大多數情況下 useEffect 比 class 的生命週期函式效能更好,我們應該優先使用它。
如果你正在將程式碼從 class 元件遷移到使用 Hook 的函式元件,則需要注意 useLayoutEffect 與 componentDidMount、componentDidUpdate 的呼叫階段是一樣的。但是,我們推薦你一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState 的替代方案,它接收一個 (state, action) => newState 的 reducer 處理函式,並返回當前的 state 和 配套的 dispatch 方法。使用方法與 redux 非常相似。
某些場景下,useReducer 比 useState 更加適用:
- 當狀態變數比較複雜且包含多個子值的時候
- 下一個 state 依賴之前的 state
const initialState = {count: 0}; function init(initialCount) { return {count: initialCount}; } function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter(props) { const [state, dispatch] = useReducer(reducer, initialState); // const [state, dispatch] = useReducer(reducer, props.initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }
此外,它還可以模擬 forceUpdate()
const [ignored, forceUpdate] = useReducer(x => x + 1, 0); function handleClick() { forceUpdate(); }
useCallback
const memoizedCallback = useCallback(func, [deps]);
useCallback 快取了方法的引用。它有的作用:效能優化,父元件更新,傳遞給子元件的函式指標不會每次都改變,只有當依賴項發生改變的時候才會改變指標。避免了子元件的無謂渲染
它的本質是對函式依賴進行分析,依賴變更時才重新執行。
useMemo & React.memo
useMemo 用於快取一些耗時的計算結果(返回值),只有當依賴項改變時才重新進行計算。
useCallback(func, [deps]) 等同於 useMemo(() => func, [deps])
useCallback 快取的是方法的引用,useMemo 快取的是方法的返回值,適用場景都是避免不必要的子元件渲染。
在類元件中有 React.PureComponent,與之對應的函式元件可以使用 React.memo,它們都會在自身 re-render 時,對每一個 props 項進行淺對比,如果引用沒有發生改變,就不會觸發渲染。
那麼,useMemo 和 React.memo 有什麼共同點呢?前者可以在元件內部使用,可以擁有比後者更細粒度的依賴控制。它們兩個與 useCallback 的本質一樣,都是進行依賴控制。
useContext
專門為函式元件提供的 context hook API,可以更加方便地獲取 context 的值。
const value = useContext(MyContext);
useContext(MyContext) 接收一個 context 物件,當前獲取到的值由上層元件中距離最近的 <MyContext.Provider> 的 value 決定。
useContext(MyContext) 相當於之前的 static contextType = MyContext 或者 <MyContext.Consumer>
useRef
const refContainer = useRef(initialValue);
useRef 返回一個可變的 ref 物件,其 current 屬性被初始化為傳入的引數。返回的 ref 物件在元件的整個生命週期內保持不變。
注意:此 hook 可以獲取 DOM 元素、類元件示例,但無法獲取函式元件例項,因為函式元件根本沒有例項。如果想讓函式元件被獲取到 ref,可以使用 useImperativeHandle 來達到這樣的效果
另外,useRef 獲取到的“ref”物件是一個 current 屬性可變且可以容納任意值的通用容器。可以實現如下功能:
- 模擬例項變數
- 獲取 prevProps、prevState
// 當做 class 例項變數 function Timer() { const intervalRef = useRef(); useEffect(() => { const id = setInterval(() => { // ... }); intervalRef.current = id; return () => { clearInterval(intervalRef.current); }; }); // ... } // 獲取prevProps,prevState function Counter(props) { const [count, setCount] = useState(0); const prevProps = useRef(props); const prevCount = useRef(count); useEffect(() => { prevCount.current = count; prevProps.current = props; }); return <h1>Now: {count} - {props}, before: {prevCount.current} - {prevProps.current}</h1>; }
useImperativeHandle
useImperativeHandle 可以讓你在使用 ref 時自定義對外暴露的屬性。官方指出,它應當與 forwardRef 一起使用。
示例:
function FancyInput(props, ref) { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} ... />; } FancyInput = forwardRef(FancyInput);
此時,通過 ref 獲取到 FancyInput 的"例項",其 current 屬性內只有 foucs 屬性可供訪問