擁抱 React Hooks

Reaper622發表於2019-05-04

React Hooks

為什麼需要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 有兩種,useCallbackuseMemo.

二者的轉換關係為:

useCallback(fn, inputs) === useMemo(() => fn, inputs)

useCallback是如何幫助我們提升效能的呢? 實際上,它其實是快取了每次渲染時的 inline 回撥函式的例項,之後無論是配合shouldComponentUpdate 或者是 React.memo都能夠達到減少不必要的渲染的作用。這也提示我們,React.memoReact.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來避免阻塞視覺更新。

Hooks 的好處

  • 避免了我們在複用含狀態元件(classes) 時使用 render props高階元件時產生的

誇張的層級巢狀。

  • 防止我們為了實現功能而在生命週期函式中寫入了大量的重複的程式碼。
  • classes 中的 this 指向十分的迷惑。

相關文章