1. 程式人生 > 程式設計 >30分鐘帶你全面瞭解React Hooks

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,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(){
  cons程式設計客棧t {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 從 etTGigiscundefined 變成 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,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 一遍。這樣就產生了不必要的 www.cppcns.comrender 。

為了解決不必要的 render ,React 提供了 React.memo() 介面來對子元件進行封裝。如下:

function App(){
  const [n,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,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={pr程式設計客棧ops.onClick}>click</b程式設計客棧utton>
    </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的資料請關注我們其它相關文章!