首頁>技術>
動機(官方) 元件之間很難重用有狀態邏輯 複雜的元件變得難以理解 類 class 混淆了人和機器 更符合 FP 的理解, React 元件本身的定位就是函式,一個吃進資料、吐出 UI 的函式 基礎hookuseState
    const [state, setState] = useState(initialState)     
useState 有一個引數,該引數可以為任意資料型別,一般用作預設值 useState 返回值為一個數組,陣列的第一個引數為我們需要使用的 state,第二個引數為一個 setFn。 完整例子
function Love() {    const [like, setLike] = useState(false)    const likeFn = () => (newLike) => setLike(newLike)    return (      <>        你喜歡我嗎: {like ? 'yes' : 'no'}        <button onClick={likeFn(true)}>喜歡</button>        <button onClick={likeFn(false)}>不喜歡</button>      </>    )  }

注意:

如果初始值是個函式;取得是他的返回值;而不是函式本身

呼叫 useState 方法的時候做了什麼?

這是一種在函式呼叫時儲存變數的方式 —— useState是一種新方法,它與 class 裡面的 this.state提供的功能完全相同。

一般來說,在函式退出後變數就會”消失”,而 state 中的變數會被 React 保留。

我們聲明瞭一個叫 count 的 state 變數,然後把它設為 0。React 會在重複渲染時記住它當前的值,並且提供最新的值給我們的函式。我們可以透過呼叫 setCount 來更新當前的 count。

useEffect

在函式元件主體內(這裡指在 React 渲染階段)改變 DOM、新增訂閱、設定定時器、記錄日誌以及執行其他包含副作用的操作都是不被允許的,因為這可能會產生莫名其妙的 bug 並破壞 UI 的一致性。

使用 useEffect 完成副作用操作。賦值給 useEffect 的函式會在元件渲染到螢幕之後執行。你可以把 effect 看作從 React 的純函式式世界通往命令式世界的逃生通道。

在瀏覽器完成佈局與繪製之後,傳給 useEffect 的函式會延遲呼叫。這使得它適用於許多常見的副作用場景,比如設定訂閱和事件處理等情況,因此不應在函式中執行阻塞瀏覽器更新螢幕的操作。

雖然 useEffect 會在瀏覽器繪製後延遲執行,但會保證在任何新的渲染前執行。React 將在元件更新前重新整理上一輪渲染的 effect。

然而,並非所有 effect 都可以被延遲執行。例如,在瀏覽器執行下一次繪製前,使用者可見的 DOM 變更就必須同步執行,這樣使用者才不會感覺到視覺上的不一致。(概念上類似於被動監聽事件和主動監聽事件的區別。)React 為此提供了一個額外的 useLayoutEffect Hook 來處理這類 effect。它和 useEffect 的結構相同,區別只是呼叫時機不同。

如果我的 effect 的依賴頻繁變化,我該怎麼辦?
function Counter() {  const [count, setCount] = useState(0);  useEffect(() => {    const id = setInterval(() => {      setCount(count + 1); // 這個 effect 依賴於 `count` state    }, 1000);    return () => clearInterval(id);  }, []); //  Bug: `count` 沒有被指定為依賴  return <h1>{count}</h1>;}

傳入空的依賴陣列 [],意味著該 hook 只在元件掛載時執行一次,並非重新渲染時。但如此會有問題,在 setInterval 的回撥中,count 的值不會發生變化。因為當 effect 執行時,我們會建立一個閉包,並將 count 的值被儲存在該閉包當中,且初值為 0。每隔一秒,回撥就會執行 setCount(0 + 1),因此,count 永遠不會超過 1。

指定 [count] 作為依賴列表就能修復這個 Bug,但會導致每次改變發生時定時器都被重置。事實上,每個 setInterval 在被清除前(類似於 setTimeout)都會呼叫一次。但這並不是我們想要的。要解決這個問題,我們可以使用 setState 的函式式更新形式。它允許我們指定 state 該 如何 改變而不用引用 當前 state:

function Counter() {  const [count, setCount] = useState(0);  useEffect(() => {    const id = setInterval(() => {      setCount(c => c + 1); // ✅ 在這不依賴於外部的 `count` 變數    }, 1000);    return () => clearInterval(id);  }, []); // ✅ 我們的 effect 不適用元件作用域中的任何變數  return <h1>{count}</h1>;}

(setState 函式的身份是被確保穩定的,所以可以放心地在依賴項中省略掉)

額外的Hooks useCallback
const memoizedCallback = useCallback(    () => {           doSomething(a, b);     },   [a, b], );

返回一個 memoized 回撥函式。

把內聯回撥函式及依賴項陣列作為引數傳入 useCallback,它將返回該回調函式的 memoized 版本,該回調函式僅在某個依賴項改變時才會更新。當你把回撥函式傳遞給經過最佳化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子元件時,它將非常有用。

useCallback(fn, deps) 相當於 useMemo(() => fn, deps)

注意

依賴項陣列不會作為引數傳給回撥函式。雖然從概念上來說它表現為:所有回撥函式中引用的值都應該出現在依賴項陣列中。未來編譯器會更加智慧,屆時自動建立陣列將成為可能。

為了效能最佳化而生:

不必要每個函式都用useCallback 包一下;說得很清楚;

1.函式執行只代表執行了render;如果兩個虛擬dom沒有差異;就不會更新dom, 不代表操作了dom元素。

2.明確說了傳遞給經過最佳化並使用引用相等性去避免非必要渲染的子元件時,他將非常有用,啥意思用memo包裹一下子元件,方法就用useCallback,屬性就用useMemo

場合memo一起用:

function PageA(props:any) {    const { onClick, children } = props    console.log('a---render ')    return  <TouchableOpacity onPress={onClick}>                <Text>{children}</Text>            </TouchableOpacity>  }    function PageB ({ onClick, name }) {      console.log('b----render')    useEffect(()=>{        console.log('b Mounted')        return ()=>{ }    },[])    return <TouchableOpacity onPress={onClick}>                    <Text>{name}</Text>            </TouchableOpacity>  }  const PageC = memo(PageB)  function UseCallback() {    const [a, setA] = useState(0)    const [b, setB] = useState(0)      const handleClick1 = () => {      setA(a + 1)    }    // const handleClick2 =() => {    //     setB(b + 1)    //   }        const handleClick2 = useCallback(() => {      setB(b + 1)    }, [b])      return (      <>        <PageA onClick={handleClick1}>{a}</PageA>        <PageC onClick={handleClick2} name={b} />      </>    )  }

memo與PureComponent比較類似,前者是對Function Component的最佳化,後者是對Class Component的最佳化,都會對傳入元件的資料進行淺比較,memo快取的是元件本身,是站在全域性的角度進行最佳化

const handleClick2 = useCallback(() => { setB(b + 1) }, [b])

useCallback 則是對函式的快取,依賴項b不變化;則handleCick 不必變化;就應該快取起來,提高效能,減少對資源的浪費

需不需要每個函式都是用useCallback

它的目的是為了一些子元件不必要的重新渲染。

useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一個 memoized 值。

把“建立”函式和依賴項陣列作為引數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種最佳化有助於避免在每次渲染時都進行高開銷的計算。

記住,傳入 useMemo 的函式會在渲染期間執行。請不要在這個函式內部執行與渲染無關的操作,諸如副作用這類的操作屬於 useEffect 的適用範疇,而不是 useMemo。

如果沒有提供依賴項陣列,useMemo 在每次渲染時都會計算新的值。

你可以把 useMemo 作為效能最佳化的手段,但不要把它當成語義上的保證。將來,React 可能會選擇“遺忘”以前的一些 memoized 值,並在下次渲染時重新計算它們,比如為離屏元件釋放記憶體。先編寫在沒有 useMemo 的情況下也可以執行的程式碼 —— 之後再在你的程式碼中新增 useMemo,以達到最佳化效能的目的。

注意

依賴項陣列不會作為引數傳給“建立”函式。雖然從概念上來說它表現為:所有“建立”函式中引用的值都應該出現在依賴項陣列中。未來編譯器會更加智慧,屆時自動建立陣列將成為可能。

我們推薦啟用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在新增錯誤依賴時發出警告並給出修復建議。

注意:

useMemo 快取的結果是回撥函式中return回來的值,主要用於快取計算結果的值,應用場景如需要計算的狀態

useRef
const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 物件,其 .current 屬性被初始化為傳入的引數(initialValue)。返回的 ref 物件在元件的整個生命週期內保持不變。

一個常見的用例便是命令式地訪問子元件:(注意:這個地方不能用箭頭函式)

function TextInputWithFocusButton() {  const inputEl = useRef(null);  const onButtonClick = () => {    // `current` 指向已掛載到 DOM 上的文字輸入元素    inputEl.current.focus();  };  return (    <>      <input ref={inputEl} type="text" />      <button onClick={onButtonClick}>Focus the input</button>    </>  );}//或者:function Image(props) {  // ⚠️ IntersectionObserver 在每次渲染都會被建立  const ref = useRef(new IntersectionObserver(onIntersect));  // ...}function Image(props) {  const ref = useRef(null);  // ✅ IntersectionObserver 只會被惰性建立一次  function getObserver() {    if (ref.current === null) {      ref.current = new IntersectionObserver(onIntersect);    }    return ref.current;  }  // 當你需要時,呼叫 getObserver()  // ...}

本質上,useRef 就像是可以在其 .current 屬性中儲存一個可變值的“盒子”。

你應該熟悉 ref 這一種訪問 DOM 的主要方式。如果你將 ref 物件以 <div ref={myRef} /> 形式傳入元件,則無論該節點如何改變,React 都會將 ref 物件的 .current 屬性設定為相應的 DOM 節點。

然而,useRef() 比 ref 屬性更有用。它可以很方便地儲存任何可變值,其類似於在 class 中使用例項欄位的方式。

這是因為它建立的是一個普通 Javascript 物件。而 useRef() 和自建一個 {current: ...} 物件的唯一區別是,useRef 會在每次渲染時返回同一個 ref 物件。

請記住,當 ref 物件內容發生變化時,useRef 並不會通知你。變更 .current 屬性不會引發元件重新渲染。如果想要在 React 繫結或解綁 DOM 節點的 ref 時執行某些程式碼,則需要使用回撥 ref 來實現。

useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init); 

另外 useReducer 可以讓你透過 reducer 來管理元件本地的複雜 state。

function Todos() {   	const [todos, dispatch] = useReducer(todosReducer);   // ...

例如,有個複雜的元件,其中包含了大量以特殊的方式來管理的內部狀態。useState 並不會使得集中更新邏輯變得容易,因此你可能更願意使用 redux 中的 reducer 來編寫、

function todosReducer(state, action) {     switch (action.type) {       		case 'add':         			return [...state, {         text: action.text,         completed: false       }];     			// ... other actions ...       			default:         		return state;   } }  

Reducers 非常便於單獨測試,且易於擴充套件,以表達複雜的更新邏輯。如有必要,您可以將它們分成更小的 reducer。但是,你可能還享受著 React 內部 state 帶來的好處,或者可能根本不想安裝其他庫。Reducers 非常便於單獨測試,且易於擴充套件,以表達複雜的更新邏輯。如有必要,您可以將它們分成更小的 reducer。但是,你可能還享受著 React 內部 state 帶來的好處,或者可能根本不想安裝其他庫。

那麼,為什麼我們不編寫一個 useReducer 的 Hook,使用 reducer 的方式來管理元件的內部 state 呢?其簡化版本可能如下所示:

function useReducer(reducer, initialState) {     const [state, setState] = useState(initialState);       function dispatch(action) {          const nextState = reducer(state, action);           setState(nextState);      }     return [state, dispatch]; }

在元件中使用它,讓 reducer 驅動它管理 state:

function Todos() {   	const [todos, dispatch] = useReducer(todosReducer, []);  	function handleAddClick(text) {     		dispatch({ type: 'add', text });   	}   // ...}

在複雜元件中使用 reducer 管理內部 state 的需求很常見,我們已經將 useReducer 的 Hook 內建到 React 中。你可以在 Hook API 索引中找到它使用,搭配其他內建的 Hook 一起使用

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於之前的 state 等。並且,使用 useReducer 還能給那些會觸發深更新的元件做效能最佳化,因為你可以向子元件傳遞 dispatch 而不是回撥函式 。

注意

React 會確保 dispatch 函式的標識是穩定的,並且不會在元件重新渲染時改變。這就是為什麼可以安全地從 useEffect 或 useCallback 的依賴列表中省略 dispatch。

指定初始 state

有兩種不同初始化 useReducer state 的方式,你可以根據使用場景選擇其中的一種。將初始 state 作為第二個引數傳入 useReducer 是最簡單的方法:

const [state, dispatch] = useReducer(    reducer,    {count: initialCount}  );

注意

React 不使用 state = initialState 這一由 Redux 推廣開來的引數約定。有時候初始值依賴於 props,因此需要在呼叫 Hook 時指定。如果你特別喜歡上述的引數約定,可以透過呼叫 useReducer(reducer, undefined, reducer) 來模擬 Redux 的行為,但我們不鼓勵你這麼做。

惰性初始化

你可以選擇惰性地建立初始 state。為此,需要將 init 函式作為 useReducer 的第三個引數傳入,這樣初始 state 將被設定為 init(initialArg)。

這麼做可以將用於計算 state 的邏輯提取到 reducer 外部,這也為將來對重置 state 的 action 做處理提供了便利

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};    case 'reset':      return init(action.payload);    default:      throw new Error();  }}function Counter({initialCount}) {  const [state, dispatch] = useReducer(reducer, initialCount, init);  return (    <>      Count: {state.count}      <button        onClick={() => dispatch({type: 'reset', payload: initialCount})}>        Reset      </button>      <button onClick={() => dispatch({type: 'decrement'})}>-</button>      <button onClick={() => dispatch({type: 'increment'})}>+</button>    </>  );}
跳過 dispatch

如果 Reducer Hook 的返回值與當前 state 相同,React 將跳過子元件的渲染及副作用的執行。(React 使用 Object.is 比較演算法 來比較 state。)

需要注意的是,React 可能仍需要在跳過渲染前再次渲染該元件。不過由於 React 不會對元件樹的“深層”節點進行不必要的渲染,所以大可不必擔心。如果你在渲染期間執行了高開銷的計算,則可以使用 useMemo 來進行最佳化。

useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以讓你在使用 ref時自定義暴露給父元件的例項值。在大多數情況下,應當避免使用 ref 這樣的命令式程式碼。useImperativeHandle 應當與 forwardRef 一起使用:

function FancyInput(props, ref) {  const inputRef = useRef();  useImperativeHandle(ref, () => ({    focus: () => {      inputRef.current.focus();    }  }));  return <input ref={inputRef} ... />;}FancyInput = forwardRef(FancyInput);

在本例中,渲染 <FancyInput ref={inputRef} /> 的父元件可以呼叫 inputRef.current.focus()。

注意:current時useRef多加了一層;如果你用函式的方式接受;就沒有current這一層;

useLayoutEffect

其函式簽名與 useEffect 相同,但它會在所有的 DOM 變更之後同步呼叫 effect。可以使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製之前,useLayoutEffect 內部的更新計劃將被同步重新整理。

提示

如果你正在將程式碼重寫 class 元件遷移到使用 Hook 的函式元件,則需要注意 useLayoutEffect 與 componentDidMount、componentDidUpdate 的呼叫階段是一樣的。但是,我們推薦你一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect。

如果你使用服務端渲染,請記住,無論如何 useLayoutEffect 還是 useEffect 都無法在 Javascript 程式碼載入完成之前執行。這就是為什麼在服務端渲染元件中引入 useLayoutEffect 程式碼時會觸發 React 告警。解決這個問題,需要將程式碼邏輯轉移至 useEffect 中(如果首次渲染不需要這段邏輯的情況下),或是將該元件延遲到客戶端渲染完成後再顯示(如果直到 useLayoutEffect 執行之前 HTML 都是顯示錯亂的情況下)。

若要從服務端渲染的 HTML 佈局排除依賴佈局 effect 的元件,可以透過使用 showChild && <Child /> 進行條件渲染,並使用 useEffect(() => { setShowChild(true); }, []) 延遲展示元件。這樣,在客戶端渲染完成之前,UI 就不會像之前那樣顯示錯亂了。

Hooks FAQ

向外暴露方法給父元件使用

推薦:

useImperativeHandle 向外暴露api

function  AddAbnormalGoods (props:ISProps,ref:any):React.ReactElement {   	useImperativeHandle(ref,()=>({            componentWillAppear(){                removeOnPlayCompletionSubscript = audioRecordUtils.onPlayCompletion(() => {                    setIsPlaying(false)                })            },            componentWillDisappear(){                if (removeOnPlayCompletionSubscript) {                    removeOnPlayCompletionSubscript()                    removeOnPlayCompletionSubscript = null                }                stopRecord()            },            props        })    )}
有類似例項變數的東西嗎?
function Timer() {  //可以儲存一些跨宣告週期的 資料;渲染前後資料不變  const intervalRef = useRef();  useEffect(() => {    const id = setInterval(() => {      // ...    });    intervalRef.current = id;    return () => {      clearInterval(intervalRef.current);    };  });  // ...}function  AddAbnormalGoods (props:ISProps,ref:any):React.ReactElement {     const instance = useRef<ISRefInstance>({          removeOnPlayCompletionSubscript:null,          timer:null,          voiceImg:null,          animatedView:null      })    //可以結構出來      let {removeOnPlayCompletionSubscript,timer,voiceImg,animatedView} = instance.current   }
推薦實踐:元件定義

Function Component 採用 const + 箭頭函式方式定義:

const App: React.FC<{ title: string }> = ({ title }) => {  	return React.useMemo(() => <div>{title}</div>, [title]);};App.defaultProps = {  	title: 'Function Component'}

上面的例子包含了:

用 React.FC 申明 Function Component 元件型別與定義 Props 引數型別。 用 React.useMemo 最佳化渲染效能。 用 App.defaultProps 定義 Props 的預設值。

React.FC 只能有一個入參;如果用到forwardRef,就不行了

FAQ

為什麼不用解構方式代替 defaultProps?

雖然解構方式書寫 defaultProps 更優雅,但存在一個硬傷:對於物件型別每次 Rerender 時引用都會變化,這會帶來效能問題,因此不要這麼做。

區域性狀態

區域性狀態有三種,根據常用程度依次排列: useState useRef useReducer 。

useState
const [hide, setHide] = React.useState(false); const [name, setName] = React.useState('BI');

狀態函式名要表意,儘量聚集在一起申明,方便查閱。

useRef
const dom = React.useRef(null);

useRef 儘量少用,大量 Mutable 的資料會影響程式碼的可維護性。

但對於不需重複初始化的物件推薦使用 useRef 儲存,比如 new G2() 。

useReducer

區域性狀態不推薦使用 useReducer ,會導致函式內部狀態過於複雜,難以閱讀。 useReducer 建議在多元件間通訊時,結合 useContext 一起使用。

FAQ

可以在函式內直接申明普通常量或普通函式嗎?

不可以,Function Component 每次渲染都會重新執行,常量推薦放到函式外層避免效能問題,函式推薦使用 useCallback 申明。

函式

所有 Function Component 內函式必須用 React.useCallback 包裹,以保證準確性與效能。

const [hide, setHide] = React.useState(false);    const handleClick = React.useCallback(() => {   setHide(isHide => !isHide) }, [])

useCallback 第二個引數必須寫,eslint-plugin-react-hooks 外掛會自動填寫依賴項。

不過;官網也明確說了;對於傳給經過最佳化,並使用引用相等性的子元件非常有效memo,不必每個都傳,本身快取也有一定開銷

6
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • MySQL 的 MyISAM 與 InnoDB