30分鐘帶你全面瞭解React Hooks

概述

1. Hooks 隻能在函數組件內使用;

2. Hooks 用於擴充函數組件的功能,使函數組件可以完全代替類組件

React Hooks 都掛在 React 對象上,因此使用時為 React.useState() 的形式,若嫌麻煩,可以提前導入,如下:

import React, { useState } from “react”

React 內置的 Hooks 有很多,這裡介紹一些常用到的。全部的請看 Hooks API

用到瞭 Hook 的函數組件名必須首字母大寫,否則會被 ESLint 報錯

1. useState

const [state, setState] = useState(initialState)

1.1 概念三連問

調用 useState 有什麼作用?

useState 是用於聲明一個狀態變量的,用於為函數組件引入狀態。

我們傳遞給 useState 的參數是什麼?

useState 隻接收一個參數,這個參數可以是數字、字符串、對象等任意值,用於初始化聲明的狀態變量。也可以是一個返回初始值的函數,最好是函數,可在渲染時減少不必要的計算。

useState返回的是什麼?

它返回一個長度為2的讀寫數組,數組的第一項是定義的狀態變量本身,第二項是一個用來更新該狀態變量的函數,約定是 set 前綴加上狀態的變量名。如 setState,setState() 函數接收一個參數,該參數可以是更新後的具體值,也可以是一個返回更新後具體值的函數。若 setState 接收的是一個函數,則會將舊的狀態值作為參數傳遞給接收的函數然後得到一個更新後的具體狀態值。

1.2 舉個例子

function App(){
  const [n, setN] = useState(0)
  const [m, setM] = useState(() => 0)
  return (
    <div>
      n: {n}
      <button onClick={() => setN(n+1)}>+1</button>
      <br/>
      m: {m}
      <button onClick={() => setM(oldM => oldM+1)}>+1</button>
    </div>
  )
}

1.3 註意事項

  • useState Hook 中返回的 setState 並不會幫我們自動合並對象狀態的屬性
  • setState 中接收的對象參數如果地址沒變的話會被 React 認為沒有改變,因此不會引起視圖的更新

2. useReducer

useReducer 是 useState 的升級版。在 useState 中返回的寫接口中,我們隻能傳遞最終的結果,在 setN 的內部也隻是簡單的賦值操作。
也就是說,得到結果的計算過程需要我們在函數組件內的回調函數中書寫,這無疑增加瞭函數組件的體積,而且也不符合 Flux 的思想(狀態由誰產生的,誰負責進行各種處理,並暴露處理接口出去給別人用)

因此,React 就提供瞭比 useState 更高級的狀態管理 Hook:useReducer,介紹如下:

2.1 使用方法

  • 創建初始狀態值 initialState
  • 創建包含所有操作的 reducer(state, action) 函數,每種操作類型均返回新的 state 值
  • 根據 initialState 和 reducer 使用 const [state, dispatch] = useReducer(reducer, initialState) 得到讀寫 API
  • 調用寫接口,傳遞的參數均掛在 action 對象上

2.2 舉個例子

import React, { useReducer } from 'react';
import ReactDOM from 'react-dom';

const initialState = {
  n: 0
}

const reducer = (state, action) => {
  switch(action.type){
    case 'addOne':
      return { n: state.n + 1 }
    case 'addTwo':
      return { n: state.n + 2 }
    case 'addX':
      return { n: state.n + action.x }
    default: {
      throw new Error('unknown type')
    }
  }
}

function App(){
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <div>
      我是 App
      {state.n}
      <button onClick={()=>dispatch({type: 'addOne'})}>+1</button>
      <button onClick={()=>dispatch({type: 'addTwo'})}>+2</button>
      <button onClick={()=>dispatch({type: 'addX', x: 5})}>+5</button>
    </div>
  )
}


ReactDOM.render(<App/>,document.getElementById('root'));

3. useContext

context 是上下文的意思,上下文是局部的全局變量這個局部的范圍由開發者自己指定。

3.1 使用方法

useContext 的使用方法分三步走:

  • 使用 const x = createContext(null) 創建上下文,在創建時一般不設置初始值,因此為 null,一般是在指定上下文作用域時初始化。
  • 使用 <x.Provider value={}></x.Provider> 圈定上下文的作用域
  • 在作用域中使用 const value = useContext(x) 使用上下文的數據

3.2 舉個例子

import React, { useState, createContext, useContext } from 'react';
import ReactDOM from 'react-dom';

const Context = createContext(null)

function App(){
  const [n, setN] = useState(0)
  return (
    <Context.Provider value={{n, setN}}>
      <div>
        <Baba />
        <Uncle />
      </div>
    </Context.Provider>
  )
}

function Baba(){
  return (
    <div>
      我是爸爸
      <Child />
    </div>
  )
}

function Uncle(){
  const {n, setN} = useContext(Context)
  return (
    <div>
      我是叔叔
      我拿到的 context 數據為 {n}
    </div>
  )
}

function Child(){
  const {n, setN} = useContext(Context)
  return (
    <div>
      我是兒子
      我拿到的 context 數據為 {n}
      <button onClick={() => setN(n+5)}>
        點擊改變 context 數據
      </button>
    </div>
  )
}


ReactDOM.render(<App/>,document.getElementById('root'));

4. useEffect

effect 是副作用的意思,對環境的改變就是副作用。副作用好像是函數式編程裡的一個概念,這裡不做過多解讀,也不太懂。
在 React 中,useEffect 就是在每次 render 後執行的操作,相當於 afterRender, 接收的第一個參數是回調函數,第二個參數是回調時機。可用在函數組件中模擬生命周期。

如果同時出現多個 useEffect ,會按出現順序依次執行

4.1 模擬 componentDidMount

useEffect(()=>{
  console.log('隻在第一次 render 後執行')
},[])

4.2 模擬 componentDidMount + componentDidUpdate

useEffect(()=>{
   console.log('每次 render 後都執行,包括第一次 render')
})

4.3 可添加依賴

useEffect(()=>{
    console.log('隻在 x 改變後執行,包括第一次 x 從 undefined 變成 initialValue')
},[x])
//如果有兩個依賴,則是當兩個依賴中的任何一個變化瞭都會執行

4.4 模擬 componentWillUnmount

useEffect(()=>{
  console.log('每次 render 後都執行,包括第一次 render')
  return ()=>{
    console.log('該組件要被銷毀瞭')
  }
})
//直接 return 一個函數即可,該函數會在組件銷毀前執行

5. useLayoutEffect

useEffect 總是在瀏覽器渲染完視圖過後才執行,如果 useEffect 裡面的回調函數有對 DOM 視圖的操作,則會出現一開始是初始化的視圖,後來執行瞭 useEffect 裡的回調後立馬改變瞭視圖的某一部分,會出現一個閃爍的狀態。
為瞭避免這種閃爍,可以將副作用的回調函數提前到瀏覽器渲染視圖的前面執行,當還沒有將 DOM 掛載到頁面顯示前執行 Effect 中對 DOM 進行操作的回調函數,則在瀏覽器渲染到頁面後不會出現閃爍的狀態。

layout 是視圖的意思,useLayoutEffect 就是在視圖顯示出來前執行的副作用。

useEffect 和 useLayoutEffect 就是執行的時間點不同,useLayoutEffect 是在瀏覽器渲染前執行,useEffect 是在瀏覽器渲染後執行。但二者都是在 render 函數執行過程中運行,useEffect 是在 render 完畢後執行,useLayoutEffect 是在 render 完畢前(視圖還沒渲染到瀏覽器頁面上)執行。

因此 useLayoutEffect 總是在 useEffect 前執行。

一般情況下,如果 Effect 中的回調函數中涉及到 DOM 視圖的改變,就應該用 useLayoutEffect,如果沒有,則用 useEffect。

6. useRef

useRef Hook 是用來定義一個在組件不斷 render 時保持不變的變量。
組件每次 render 後都會返回一個虛擬 DOM,組件內對應的變量都隻屬於那個時刻的虛擬 DOM。
useRef Hook 就提供瞭創建貫穿整個虛擬 DOM 更新歷史的屬於這個組件的局部的全局變量。
為瞭確保每次 render 後使用 useRef 獲得的變量都能是之前的同一個變量,隻能使用引用做到,因此,useRef 就將這個局部的全局變量的值存儲到瞭一個對象中,屬性名為:current

useRef 的 current 變化時不會自動 render

useRef 可以將創建的 Refs 對象通過 ref 屬性的方式引用到 DOM 節點或者 React 實例。這個作用在 React—ref 屬性 中有介紹。

同樣也可以作為組件的局部的全局變量使用,如下例的記錄當前是第幾次渲染頁面。

function App(){
  const [state, dispatch] = useReducer(reducer, initialState)
  const count = useRef(0)
  useEffect(()=>{
    count.current++;
    console.log(`這是第 ${count.current} 次渲染頁面`)
  })
  return (
    <div>
      我是 App
      {state.n}
      <button onClick={()=>dispatch({type: 'addOne'})}>+1</button>
      <button onClick={()=>dispatch({type: 'addTwo'})}>+2</button>
      <button onClick={()=>dispatch({type: 'addX', x: 5})}>+5</button>
    </div>
  )
}

7. forwardRef(不是 Hook)

forwardRef 主要是用來對原生的不支持 ref屬性 函數組件進行包裝使之可以接收 ref屬性 的,具體使用方法可參考 React—ref 屬性

forwardRef 接收一個函數組件,返回一個可以接收 ref 屬性的函數組件

8. useMemo && useCallback

React 框架是通過不斷地 render 來得到不同的虛擬 DOM ,然後進行 DOM Diff 來進行頁面 DOM 的選擇性更新的,因此,在每次的 render 之後都會短時間內存在新舊兩個虛擬 DOM 。

對於組件內包含子組件的情況,當父組件內觸發 render 時,就算子組件依賴的 props 沒有變化,子組件也會因為父組件的重新 render 再次 render 一遍。這樣就產生瞭不必要的 render 。

為瞭解決不必要的 render ,React 提供瞭 React.memo() 接口來對子組件進行封裝。如下:

function App(){
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)
  return (
    <div>
      我是父組件
      n: {n}
      <button onClick={()=>setN(n+1)}>n+1</button>
      <button onClick={()=>setM(m+1)}>m+1</button>
      <Child value={m}/>  //這樣當子組件依賴的 m 值沒有變化時,子組件就不會重新 render
    </div>
  )
}

const Child = React.memo((props)=>{
  useEffect(()=>{
    console.log('子組件 render 瞭')
  })
  return (
  <div>我是子組件,我收到來自父組件的值為:m {props.value}</div>
  )
})

但是上述方式存在 bug,因為 React.memo 在判斷子組件依賴的屬性有沒有發生改變時僅僅是做的前後值是否相等的比較,如果子組件從父組件處接收的依賴是一個對象的話,比較的就會是對象的地址,而不是對象裡面的內容,因此在每次父組件重新 render 後得到的會是不同地址的對象,盡管對象裡面的值沒有更新,但是子組件發現地址變瞭也會重新 render。

為瞭解決這個問題,就又出來瞭 useMemo() Hook,useMemo 是用於在新舊組件交替時緩存復用一個函數或者一個對象,當某個依賴重新變化時才重新生成。

useMemo Hook 接收一個無參數的返回函數(或對象)的函數。並且 useMemo 必須有個依賴,告訴其在什麼時候重新計算。有點類似於 Vue 的計算屬性的原理。如下:

function App(){
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)
  const onClickChild = useMemo(()=>{
    return () => {
      console.log(m)
    }
  },[m])  
  return (
    <div>
      我是父組件
      n: {n}
      <button onClick={()=>setN(n+1)}>n+1</button>
      <button onClick={()=>setM(m+1)}>m+1</button>
      <Child value={m} onClick = {onClickChild}/>
    </div>
  )
}

const Child = React.memo((props)=>{
  useEffect(()=>{
    console.log('子組件 render 瞭')
  })
  return (
    <div>
      我是子組件,我收到來自父組件的值為:m {props.value}
      <br/>
      <button onClick={props.onClick}>click</button>
    </div>
  )
})

useCallback() 是 useMemo 的語法糖,因為 useMemo 是接收一個沒有參數的返回函數(或對象)的函數,會有些奇怪,因此提供瞭 useCallback 來直接接收函數或對象。

const onClickChild = useMemo(() => {
      console.log(m)
  },[m])

9. useInperativeHandle

useInperativeHandel 是和 ref 相關的一個 Hook。

我們知道,ref 屬性是會將當前的組件實例或 原生DOM 直接賦值給傳入的 Ref 對象的 current 屬性上,而且函數組件不能接收 ref 屬性,因為函數組件沒有實例。但是如果函數組件經過 React.forwardRef() 封裝過後 可以接收 ref,一般情況下,這個 ref 是訪問的經過函數組件轉發過後的 原生DOM,但是,如果在函數組件內不僅僅是想讓外來的 ref 指向一個 原生DOM 呢?可不可以讓函數組件的 ref 像類組件中的 ref 指向實例一樣擁有更多的可控性操作呢?React 就為函數組件提供瞭一種封裝返回的 ref 指向的對象的方法,就是 useInperativeHandle Hook。

9.1 舉個例子

function App(){
  const myRef = useRef(null)
  useEffect(()=>{
    console.log(myRef.current.real)
    console.log(myRef.current.getParent())
  }, [])
  return (
    <div>
      我是父組件
      <Child ref={myRef}/>
    </div>
  )
}

const Child = forwardRef((props, ref)=>{
  const childRef = useRef(null)
  useImperativeHandle(ref, ()=>{
    return {
      real: childRef.current,
      getParent(){
        return childRef.current.parentNode
      }
    }
  })
  return (
    <div>
      我是子組件,我有一個子DOM
      <button ref={childRef}>按鈕</button>
    </div>
  )
})

10. 自定義 Hook

自定義 Hook 就是自定義一個函數,這個函數必須以 use 開頭,並且,該函數裡必須用到原生的 Ract Hooks,返回值一般是一個數組或一個對象,用於暴露該 Hooks 的讀寫接口。

自定義 Hook 通常是將函數組件中多次用到的 hook 整合到一起,盡量在函數組件中不要出現多次 hook 操作。

以上就是30分鐘帶你全面瞭解React Hooks的詳細內容,更多關於全面瞭解React Hooks的資料請關註WalkonNet其它相關文章!

推薦閱讀: