Hooks

HooksReact16.8 的新增特性,能夠在不寫 class 的情況下使用 state 以及其他特性。

動機

  • 在組件之間複用狀態邏輯很難
  • 複雜組件變得難以理解
  • 難以理解的 class

Hooks 規則

  • 只有在最頂層使用 Hooks不要再循環/條件/嵌套函數中使用`
  • 只有在 React 函數中調用 Hooks

函數組件和類組件的不同

函數組件能夠捕獲到當前渲染的所用的值。

對於類組件來説,雖然 props是一個不可變的數據,但是 this是一個可變的數據,在我們渲染組件的時候 this 發生了改變,所以 this.props 發生了改變,因此在 this.showMessage 中會拿到最新的 props 值。

對於函數組件來説捕獲了渲染所使用的值,當我們使用 hooks 時,這種特性也同樣的試用於 state 上。

const showMessage = () => {
	alert("寫入:" + message);
};

const handleSendClick = () => {
	setTimeout(showMessage, 3000);
};

const handleMessageChange = (e) => {
	setMessage(e.target.value);
};

如果我們想跳出'函數組件捕獲當前渲染的所用值‘這個特性,我們可以採用 ref 來追蹤某些數據。通ref.current可以獲取到最新的值

const showMessage = () => {
	alert("寫入:" + ref.current);
};

const handleSendClick = () => {
	setTimeout(showMessage, 3000);
};

const handleMessageChange = (e) => {
	setMessage(e.target.value);
	ref.current = e.target.value;
};

useEffect

useEffect 能夠在函數組件中執行副作用操作(數據獲取/涉及訂閲),其實可以把 useEffect 看作是 componentDidMount / componentDidUpdate / componentWillUnMount 的組合

  • 第一個參數是一個 callback,返回 destorydestory 作為下一個 callback 執行前調用,用於清除上一次 callback 產生的副作用
  • 第二個參數是依賴項,一個數組,可以有多個依賴項。依賴項改變,執行上一個 callback 返回的 destory,和執行新的 effect 第一個參數 callback

對於 useEffect 的執行,React 處理邏輯是採用異步調用的,對於每一個 effectcallback 會像 setTimeout 回調函數一樣,放到任務隊列裏面,等到主線程執行完畢才會執行。所以 effect 的回調函數不會阻塞瀏覽器繪製視圖

  1. 相關的生命週期替換方案
  • componentDidMount 替代方案
React.useEffect(()=>{
	//請求數據,事件監聽,操縱DOM
},[]) //dep=[],只有在初始化執行
/* 
  因為useEffect會捕獲props和state,
  所以即使是在回調函數中我們拿到的還是最初的props和state
*/
  • componentDidUnmount 替代方案
React.useEffect(()=>{
    /* 請求數據 , 事件監聽 , 操縱dom , 增加定時器,延時器 */
    return function componentWillUnmount(){
        /* 解除事件監聽器 ,清除定時器,延時器 */
    }
},[])/* 切記 dep = [] */

//useEffect第一個函數的返回值可以作為componentWillUnmount使用
  • componentWillReceiveProps 替代方案
    其實兩者的執行時機是完全不同的,一個在 render 階段,一個在 commit 階段,useEffect 會初始化執行一次,但是 componentWillReceiveProps 只會在 props 變化時執行更新
React.useEffect(()=>{
    console.log('props變化:componentWillReceiveProps')
},[ props ])
  • componentDidUpdate 替代方案
    useEffectcomponentDidUpdate 在執行時期雖然有點差別,useEffect 是異步執行,componentDidUpdate 是同步執行 ,但都是在 commit 階段
React.useEffect(()=>{
    console.log('組件更新完成:componentDidUpdate ')     
}) //沒有dep依賴項,沒有第二個參數,那麼每一次執行函數組件,都會執行該 effect。
  1. useEffect 中[]需要處理什麼

React 官網 FAQ這樣説:

只有當函數(以及它所調用的函數)不引用 propsstate 以及由它們衍生而來的值時,你才能放心地把它們從依賴列表中省略,使用 eslint-plugin-react-hooks 幫助我們的代碼做一個校驗

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}
//只會做一次更新,然後定時器不再轉動
  1. 是否應該把函數當做 effect 的依賴
const loadResourceCatalog = async () => {
	if (!templateType) return
	const reqApi = templateType === TEMPLATE_TYPE.STANDARD ? 'listCatalog' : 'getCodeManageCatalog'
	const res: any = await API[reqApi]()
	if (!res.success) return
	setCatalog(res.data)
}

useEffect(() => {
	loadResourceCatalog();
}, [])
//在函數loadResourceCatalog中使用了templateType這樣的一個state
//在開發的過程中可能會忘記函數loadResourceCatalog依賴templateType值

第一個簡單的解法,對於某些只在 useEffect 中使用的函數,直接定義在 effect 中,以至於能夠直接依賴某些 state

useEffect(() => {
	const loadResourceCatalog = async () => {
		if (!templateType) return
		const reqApi = templateType === TEMPLATE_TYPE.STANDARD ? 'listCatalog' : 'getCodeManageCatalog'
		const res: any = await API[reqApi]()
		if (!res.success) return
		setCatalog(res.data)
	}
	loadResourceCatalog();
}, [templateType])

假如我們需要在很多地方用到我們定義的函數,不能夠把定義放到當前的 effect 中,並且將函數放到了第二個的依賴參數中,那這個代碼將就進入死循環。因為函數在每一次渲染中都返回一個新的引用

const Template = () => {
	const getStandardTemplateList = async () => {
		const res: any = await API.getStandardTemplateList()
	  if (!res.success) return;
		const { data } = res;
		setCascaderOptions(data);
		getDefaultOption(data[0])
	}
	useEffect(()=>{
		getStandardTemplateList()
	}, [])
}


針對這種情況,如果當前函數沒有引用任何組件內的任何值,可以將該函數提取到組件外面去定義,這樣就不會組件每次 render 時不會再次改變函數引用。

const getStandardTemplateList = async () => {
	const res: any = await API.getStandardTemplateList()
  if (!res.success) return;
	const { data } = res;
	setCascaderOptions(data);
	getDefaultOption(data[0])
}

const Template = () => {
	useEffect(()=>{
		getStandardTemplateList()
	}, [])
}

如果説當前函數中引用了組件內的一些狀態值,可以採用 useCallBack 對當前函數進行包裹

const loadResourceCatalog = useCallback(async () => {
	if (!templateType) return
	const reqApi = templateType === TEMPLATE_TYPE.STANDARD ? 'listCatalog' : 'getCodeManageCatalog'
	const res: any = await API[reqApi]()
	if (!res.success) return
	setCatalog(res.data)
}, [templateType])

useEffect(() => {
	loadResourceCatalog();
}, [loadResourceCatalog])
//通過useCallback的包裹,如果templateType保持不變,那麼loadResourceCatalog也會保持不變,所以useEffect也不會重新運行
//如果templateType改變,那麼loadResourceCatalog也會改變,所以useEffect也會重新運行

useCallback

React 官網定義

useCallback(fn, deps)

返回一個 memoized 回調函數,該回調函數僅在某個依賴項改變時才會更新

import React, { useCallback, useState } from "react";

const CallBackTest = () => {
  const [count, setCount] = useState(0);
  const [total, setTotal] = useState(0);
  const handleCount = () => setCount(count + 1);
  //const handleCount = useCallback(() => setCount(count + 1), [count]);
  const handleTotal = () => setTotal(total + 1);

  return (
    <div>
      <div>Count is {count}</div>
      <div>Total is {total}</div>
      

      <div>
        <Child onClick={handleCount} label="Increment Count" />
        <Child onClick={handleTotal} label="Increment Total" />
      </div>
    </div>
  );
};

const Child = React.memo(({ onClick, label }) => {
  console.log(`${label} Child Render`);
  return <button onClick={onClick}>{label}</button>;
});

export default CallBackTest;

React.memo 是通過記憶組件渲染結果的方式來提高性能,memoreact16.6 引入的新屬性,通過淺比較(源碼通過 Object.is 方法比較)當前依賴的 props 和下一個 props 是否相同來決定是否重新渲染;如果使用過類組件方式,就能知道 memo 其實就相當於 class 組件中的 React.PureComponent,區別就在於 memo 用於函數組件。useCallbackReact.memo 一定要結合使用才能有效果。

使用場景

  • 作為 props,傳遞給子組件,為避免子元素不必要的渲染,需要配合 React.Memo 使用,否則無意義
  • 作為 useEffect 的依賴項,需要進行比較的時候才需要加上 useCallback

useMemo

React 官網定義

返回一個 memoized

僅會在某個依賴項改變時才重新計算 memoized 值,這種優化有助於避免在每次渲染時都進行高開銷的計算 useCallback(fn, deps) 相當於 useMemo(() => fn, deps),對於實現上,基本上是和 useCallback 相似,只是略微有些不同

使用場景

  • 避免在每次渲染時都進行高開銷的計算

兩個 hooks 內置於 React 都有特別的原因:

1.引用相等

當在 React 函數組件中定義一個對象時,它跟上次定義的相同對象,引用是不一樣的(即使它具有所有相同值和相同屬性)

  • 依賴列表
  • React.memo

大多數時候,你不需要考慮去優化不必要的重新渲染,因為優化總會帶來成本。

  1. 昂貴的計算
    計算成本很高的同步計算值的函數

總結

本文介紹了 hooks 產生動機、函數組件和類組件的區別以及 useEffect / useCallback / useMemo 等內容。重點介紹了 useEffect 的生命週期替換方案以及是否把函數作為 useEffect 的第二參數。