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 屬性可供訪問