React Hooks 是 React v16.8版本引入的全新API,這個 API 是 React 的未來,有必要深入理解。

類組件和函數組件

Hooks之前我們寫組件方式,主要包括兩種:類組件和函數組件。

一個React App 由多個類按照層級,一層層構成,複雜度成倍增長。再加入 Redux,就變得更復雜。

組件類有一下幾個缺點:

大型組件很難拆分和重構,也很難測試。

業務邏輯分散在組件的各個方法之中,導致重複邏輯或關聯邏輯。

組件類引入了複雜的編程模式,比如 render props 和高階組件。

React 團隊希望,組件不要變成複雜的容器,最好只是數據流的管道。開發者根據需要,組合管道即可。 組件的最佳寫法應該是函數,而不是類。

但是,這種寫法有重大限制,必須是純函數,不能包含狀態,也不支持生命週期方法,因此無法取代類。

React Hooks 的設計目的,就是加強版函數組件,完全不使用”類”,就能寫出一個全功能的組件。

Hook

Hook 這個單詞的意思是”鉤子”。

React Hooks 的意思是,組件儘量寫成純函數,如果需要外部功能和副作用,就用鉤子把外部代碼”鉤”進來。React Hooks 就是那些鉤子。

你需要什麼功能,就使用什麼鉤子。React 默認提供了一些常用鉤子,你也可以封裝自己的鉤子。

所有的鉤子都是爲函數引入外部功能,所以 React 約定,鉤子一律使用 use 前綴命名,便於識別。你要使用 xxx 功能,鉤子就命名爲 usexxx。

useState()

狀態鉤子 useState() 用於爲函數組件引入狀態(state)。純函數不能有狀態,所以把狀態放在鉤子裏面。

這個鉤子函數比較簡單,看一下例子:

import React, { useState } from 'react';

export default function Home() {

  const [age, setAge] = useState(0);

  return (
    <div>
      <h2>當前年齡: {age}</h2>
      <button onClick={e => setAge(age + 1)}>age+1</button>
    </div>
  )
}

userState()的參數是狀態的初始值。

setAge 時會觸發整個組件的重新渲染。

需要注意的是, React Hooks不能出現在條件判斷語句中

提倡的做法是每個狀態都單獨維護,而不是把所有的狀態都維護到一個狀態對象中。如果維護到狀態對象中,這樣再修改狀態時就必須要小心翼翼,哪些修改哪些不修改都要注意。

// 假如state對象中包含了所以需要的狀態
const [state, setState] = useState({});
// 那麼修改的時候時候就比較麻煩了,這是不提倡的做法
setState({
	...state,
	num:num+1
})

useEffect

副作用函數 useEffect 用來代替常用的生命週期函數。

import React, { useState , useEffect } from 'react';
function Example(){
    const [ count , setCount ] = useState(0);
    
    useEffect(()=>{
        console.log(`useEffect=>You clicked ${count} times`)
    })

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={()=>{setCount(count+1)}}>click me</button>
        </div>
    )
}
export default Example;
  • 通過useEffect的Hook,可以告訴React需要在渲染後執行某些操作;
  • useEffect要求我們傳入一個 回調函數 ,在React執行完更新DOM操作之後,就 會回調這個函數

  • 默認情況下,無論是第一次渲染之後,還是每次更新之後,都會執行這個 回調函數

如果要要實現類似 componentDidMonut 的功能,第二個參數可以傳一個空數組,代表不添加任何依賴。

// 這樣只會在頁面第一次渲染後調用一次,之後不會再調用。
useEffect(()=>{
    console.log(`useEffect=>You clicked ${count} times`)
},[])

假如需要有兩個狀態,當其中一個狀態改變時才更新組件,而另一個狀態更新時,不更新組件,這個時候就需要第二個參數依賴了。

const [ count , setCount ] = useState(0);
const [ num , setNum ] = useState(0);

// 只有當count狀態改變時,纔會更新組件。num改變時,不會更新組件。
useEffect(()=>{
    console.log(`useEffect=>You clicked ${count} times`)
},[count])

useEffect中定義的函數的執行不會阻礙瀏覽器更新視圖,也就是說這些函數是異步執行的,而 componentDidMonutcomponentDidUpdate 中的代碼都是同步執行的。

如何實現類似 componentWillUnmount 的功能,在其中做一些解綁或清除的操作呢。

useEffect傳入的 回調函數A本身 可以有一個返回值,這個返回值是 另外一個回調函數B

還是用上面的代碼例子中的userEffect:

useEffect(()=>{
    console.log(`useEffect=>You clicked ${count} times`)
    return ()=> {
    	console.log("effect清除機制");
    }
})

React 會在組件更新和卸載的時候執行清除操作。除了組件第一次渲染的時候不會調用清除操作,之後頁面的每一次重新更新渲染都會觸發這個操作。

我們可以使用多個userEffect來區分開不同的操作。

useEffect(() => {
  console.log("操作1");
});

useEffect(() => {
  console.log("操作2");
})

useEffect(() => {
  console.log("操作3");

  return () => {
    console.log("清除操作3");
  }
})

useEffect()的第二個參數,決定該userEffect的哪些state變化時,才重新執行渲染。當沒有寫第二個參數時候,默認支持所有的狀態。

在實際驗證中注意一下執行順序的問題:

useEffect(()=>{
    console.log("操作");
    return ()=> {
    	console.log("effect清除機制");
    }
})
// 打印的結果:
effect清除機制
操作

在看一個有意思的例子:

const [count,setCount] = useState(0)
useEffect(()=>{
	setCount(count+1)
},[])

想一下,最後count是幾呢,會累加嗎?

答案是1,不會一直累加。

因爲當頁面組件第一次加載渲染後,useEffect()會調用,執行到 setCount(count+1) 會重新觸發頁面組價的渲染,組件渲染後本來又該執行useEffect()的,但是該useEffect()沒有添加任何依賴,因count狀態改變引起的組件更新,不會觸發該useEffect(),所以不會執行。

useRef

useRef返回一個ref對象,返回的ref對象在組件的整個生命週期保持不變。

最常用的ref是兩種用法:

  • 用法一:引入DOM(或者組件,但是需要是class組件)元素;
  • 用法二:保存一個數據,這個對象在整個生命週期中可以保存不變;

獲取Dom元素

這個比較簡單,直接看例子:

import React, { useRef } from 'react';

export default function RefHookDemo() {
  const inputRef = useRef();
  const titleRef = useRef();

  const handleOperating = () => {
    titleRef.current.innerHTML = "我是coderperson";
    inputRef.current.focus();
  }

  return (
    <div>
      <input type="text" ref={inputRef}/>
      <h2 ref={titleRef}>默認內容</h2>
      <button onClick={e => handleOperating()}>操作</button>
    </div>
  )
}

可以利用該特性,獲取到DOM後,給DOM綁定事件。

保存普通變量

  • useRef可以想象成在ref對象中保存了一個.current的可變盒子;
  • useRef在組件重新渲染時,返回的依然是之前的ref對象,但是current是可以修改的;
import React, { useState, useEffect, useRef } from 'react';

let preValue = 0;

export default function RefHookDemo02() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  return (
    <div>
      <h2>前一次的值: {countRef.current}</h2>
      <h2>這一次的值: {count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
    </div>
  )
}

useMemo & momo

useMemo實際的目的也是爲了進行性能的優化。(在使用上和useEffect類似,也是兩個參數,第二個參數控制依賴)。

如何進行性能的優化呢?

  • useMemo返回的也是一個 memoized(記憶的) 值;
  • 在依賴不變的情況下,多次定義的時候,返回的值是相同的;

通常用在當父組件更新時,如果子組件沒有變化,使用useMemo控制子組件不用更新。類似於 shouldCompnentUpdate .

import React, { useState, useMemo } from 'react';

// 計算操作
function calcNum(count) {
  let total = 0;
  for (let i = 0; i < count; i++) {
    total += i;
  }
  console.log("計算一遍");
  return total
}

export default function MemoHookDemo() {
  const [count, setCount] = useState(10);
  const [isLogin, setIsLogin] = useState(true);

  // 如果不使用useMemo 當點擊切換按鈕時也會執行計算操作。現在只有當count改變時纔會執行操作。
  const total = useMemo(() => {
    return calcNum(count);
  }, [count]);

  return (
    <div>
      <h2>數字和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      {isLogin && <h2>Coderwhy</h2>}
      <button onClick={e => setIsLogin(!isLogin)}>切換</button>
    </div>
  )
}

另外說一下React的 memo 。React的memo是一個高階函數,內部會自動判斷props的preprops和nextprops是否相同,如果相同則不更新組件,不過不相同才更新組件。

const Child = memo(()=>{
	return (
		<div>
			444
		</div>
	)
})

如果想自己控制條件的判斷,可以使用memo的第二個參數,該參數是一個函數的形式。

const Child = memo(()=>{
	return (
		<div>
			444
		</div>
	)
},(prev,next)=> {
  // 如果props中的count前後相等則不更新,否則更新。
	return prev.count === next.count 
})

useCallback

useCallback實際的目的是爲了進行性能的優化。

如何進行性能的優化呢?

  • useCallback會返回一個函數的 memoized(記憶的) 值;
  • 在依賴不變的情況下,多次定義的時候,返回的值是相同的;

對比和useMemo的區別:

useMemo緩存的是值
useCallback緩存的是函數
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);

(使用方法和useMemo、useEffect一樣,第一個參數是函數,第二個參數是依賴對象)。

同樣也是常用來控制子組件是否更新的。

// 使用useCallback生成的increment1傳遞給Child子組件,當父組件更新時,子組件實現不更新。
const increment1 = useCallback(function increment() {
    setCount(count + 1);
  }, []);
  
 return (
 	<div>
 		<Child increment={increment1}/>
 	</div>
 )

useReducer

reducer源於redux的興起和廣泛使用,本身是一個函數。

看一個簡單的reducer的例子:

function countReducer(state, action) {
    switch(action.type) {
        case 'add':
            return state + 1;
        case 'sub':
            return state - 1;
        default: 
            return state;
    }
}

你只需要理解的就是這種形式和兩個參數的作用,一個參數是狀態,一個參數是如何控制狀態。

useReducer 它也是React hooks提供的函數,來實現reducer的功能。

利用上面的的reducer例子,在useReducer中使用:

import React, { useReducer } from 'react'
import { countReducer } from '../reducer/counter'
export default function Home() {
  const [state, dispatch] = useReducer(countReducer, {counter: 100});
  return (
    <div>
      <h2>當前計數: {state.counter}</h2>
      <button onClick={e => dispatch({type: "add"})}>+1</button>
      <button onClick={e => dispatch({type: "sub"})}>-1</button>
    </div>
  )
}

useReducer有兩個參數,第一個參數是控制狀態的reducer函數,第二個參數是初始值。返回值是一個數組,第一個元素是state狀態,第二個元素是 dispatch ,用來分發type, 實現state狀態的改變。

看起來是不是和 useState 有點像,其實 useState 就是基於 useReducer 實現的。

下面利用 useReducer 來簡單模擬一下 useState 的實現:

useState = (initState)=> {
	const [state,dispatch] = useReducer((state,action)=>(state||initState),initState)
	return [state,dispatch]
}

useContext

Context 的作用就是對它所包含的組件樹提供全局共享數據的一種技術。

useContext可以實現父子組件之間的傳值。

下面直接看代碼:

// 創建Context
export const UserContext = createContext();
export const ThemeContext = createContext();
export default function App() {
  return (
    <div>
      <UserContext.Provider value={{name: "why", age: 18}}>
        <ThemeContext.Provider value={{color: "red", fontSize: "20px"}}>
          <ContextHook/>
        </ThemeContext.Provider>
      </UserContext.Provider>
    </div>
  )
}

在函數組件 ContextHook 中使用 useContext :

export default function ContextHook() {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);
  console.log(user);
  console.log(theme);
  return (
    <div>
      ContextHook
    </div>
  )
}

通常 useReduceruseContext 一起結合來使用 。

還是用 useReducer 例子中計數器的reducer,搭配使用上 useContext 實現父子組件之間值的同步傳遞。

const Ctx = createContext();
// APP組件:
const [count,dispatch] = useReducer(reducer,10)
return (
	<Ctx.Provider value={[count,dispatch]}>
		<div>
			<Parent />
		</div>
	</Ctx.Provider>
)

// Parent
const [count] = useContext(Ctx)
return (
	<div>
		count:{count}
		<Child />
	</div>
)

// Child
const [count,dispatch] = useContext(Ctx)
return (
	<div>
		count:{count}
		{/* 實現count的加減 */}
		<button onClick={()=>dispatch({type:'add'})}>+1</buttton>
		<button onClick={()=>dispatch({type:'sub'})}>-1</buttton>
	</div>
)

你會發現,在Child中實現count的加減,Parent中的count也跟着改變了,實現了數據的共享。

自定義Hooks

自定義Hook本質上是一種函數代碼邏輯的封裝。它可以做到之前類組件做不到或比較難實現的功能邏輯的抽取封裝。

自定義Hook函數偏向於功能,而組件偏向於界面和業務邏輯。

自定義Hook,函數必須以 use 開頭,類似 useXxx .

比如如果要實現實時監聽屏幕的寬高的功能,如果在類組件中,需要在 componentDidMount 中添加監聽,然後在其它函數中獲取 width 和 height,最後在 componentWillUnMount 中移除監聽。流程處理分散在各個角落,不便於封裝處理。

而自定義Hook,可以很好的實現該功能的封裝。看一下代碼:

function useWinSize(){
    const [ size , setSize] = useState({
        width:document.documentElement.clientWidth,
        height:document.documentElement.clientHeight
    })

    const onResize = useCallback(()=>{
        setSize({
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight
        })
    },[]) 
    useEffect(()=>{
        window.addEventListener('resize',onResize)
        return ()=>{
            window.removeEventListener('resize',onResize)
        }
    },[])

    return size;
}

然後在其它組件中引入使用即可。

小結

React Hooks 是以後的發展趨勢,學好這一塊還是很有必要的。寫這篇文章花了我將近一天的時間,之後在公司還要做一個Hooks方面的技術分享,也算是一個技術鋪墊吧。

Hooks雖然實現了生命週期的所有功能,但是對我而言 componentDidMount componentxxx 這種生命週期的流程形式,還是比較容易理解,也更容易被人所接受。

相關文章