React Hooks 使用大全(包括 React18)巨全!

以下文章來源于前端Sharing ,作者??

大家好,我是零一,今天給大家?guī)硪环?React 所有 Hooks 的使用文章,也包括 React18 新增的 Hooks 奧!下面列了一張思維導(dǎo)圖,根據(jù)功能對這些 Hooks 做了一個(gè)歸類,本文也會一一講解每個(gè) Hook 的詳細(xì)使用、注意事項(xiàng)、使用場景!



React Hooks.png
一 前言
React hooks是react16.8 以后,react新增的鉤子API,目的是增加代碼的可復(fù)用性,邏輯性,彌補(bǔ)無狀態(tài)組件沒有生命周期,沒有數(shù)據(jù)管理狀態(tài)state的缺陷。本章節(jié)筆者將介紹目前 React 提供的所有 hooks ,介紹其功能類型和基本使用方法。

創(chuàng)作不易,希望屏幕前的你能給筆者賞個(gè)贊,以此鼓勵(lì)我繼續(xù)創(chuàng)作前端硬文。??????

1.1 技術(shù)背景
react hooks 解決了什么問題?

先設(shè)想一下,如果沒有 Hooks,函數(shù)組件能夠做的只是接受 Props、渲染 UI ,以及觸發(fā)父組件傳過來的事件。所有的處理邏輯都要在類組件中寫,這樣會使 class 類組件內(nèi)部錯(cuò)綜復(fù)雜,每一個(gè)類組件都有一套獨(dú)特的狀態(tài),相互之間不能復(fù)用,即便是 React 之前出現(xiàn)過 mixin 等復(fù)用方式,但是伴隨出 mixin 模式下隱式依賴,代碼沖突覆蓋等問題,也不能成為 React 的中流砥柱的邏輯復(fù)用方案。所以 React 放棄 mixin 這種方式。

類組件是一種面向?qū)ο笏枷氲捏w現(xiàn),類組件之間的狀態(tài)會隨著功能增強(qiáng)而變得越來越臃腫,代碼維護(hù)成本也比較高,而且不利于后期 tree shaking。所以有必要做出一套函數(shù)組件代替類組件的方案,于是 Hooks 也就理所當(dāng)然的誕生了。

所以 Hooks 出現(xiàn)本質(zhì)上原因是:

讓函數(shù)組件也能做類組件的事,有自己的狀態(tài),可以處理一些副作用,能獲取 ref ,也能做數(shù)據(jù)緩存。
解決邏輯復(fù)用難的問題。
放棄面向?qū)ο缶幊蹋瑩肀Ш瘮?shù)式編程。
為什么要使用自定義 Hooks ?

自定義 hooks 是在 React Hooks 基礎(chǔ)上的一個(gè)拓展,可以根據(jù)業(yè)務(wù)需求制定滿足業(yè)務(wù)需要的組合 hooks ,更注重的是邏輯單元。通過業(yè)務(wù)場景不同,到底需要React Hooks 做什么,怎么樣把一段邏輯封裝起來,做到復(fù)用,這是自定義 hooks 產(chǎn)生的初衷。

自定義 hooks 也可以說是 React Hooks 聚合產(chǎn)物,其內(nèi)部有一個(gè)或者多個(gè) React Hooks 組成,用于解決一些復(fù)雜邏輯。

1.2 技術(shù)愿景
目前 hooks 已經(jīng)成為 React 主流的開發(fā)手段,React 生態(tài)也日益朝著 hooks 方向發(fā)展,比如 React Router, React Redux 等, hooks 也更契合 React 生態(tài)庫的應(yīng)用。

隨著項(xiàng)目功能模塊越來越復(fù)雜,一些公共邏輯不能有效的復(fù)用,這些邏輯需要和業(yè)務(wù)代碼強(qiáng)關(guān)聯(lián)到一起,這樣會讓整體工程臃腫,功能不能復(fù)用,如果涉及到修改邏輯,那么有可能牽一發(fā)動全身。

所以有必要使用自定義 hooks 的方式,hooks 可以把重復(fù)的邏輯抽離出去,根據(jù)需要?jiǎng)?chuàng)建和業(yè)務(wù)功能綁定的業(yè)務(wù)型 hooks ,或者是根據(jù)具體功能創(chuàng)建的功能型 hooks 。

1.3 功能概覽
在 React 的世界中,不同的 hooks 使命也是不同的,我這里對 React hooks 按照功能分類,分成了 數(shù)據(jù)更新驅(qū)動,狀態(tài)獲取與傳遞,執(zhí)行副作用,狀態(tài)派生與保存,和工具類型, 具體功能劃分和使用場景如下:



WechatIMG32.png
二 hooks 之?dāng)?shù)據(jù)更新驅(qū)動
2.1 useState
useState 可以使函數(shù)組件像類組件一樣擁有 state,函數(shù)組件通過 useState 可以讓組件重新渲染,更新視圖。

useState 基礎(chǔ)介紹:

const [ ①state , ②dispatch ] = useState(③initData)
① state,目的提供給 UI ,作為渲染視圖的數(shù)據(jù)源。

② dispatchAction 改變 state 的函數(shù),可以理解為推動函數(shù)組件渲染的渲染函數(shù)。

③ initData 有兩種情況,第一種情況是非函數(shù),將作為 state 初始化的值。第二種情況是函數(shù),函數(shù)的返回值作為 useState 初始化的值。

useState 基礎(chǔ)用法:

const DemoState = (props) => {
   /* number為此時(shí)state讀取值 ,setNumber為派發(fā)更新的函數(shù) */
   let [number, setNumber] = useState(0) /* 0為初始值 */
   return (<div>
       <span>{ number }</span>
       <button onClick={ ()=> {
         setNumber(number+1)
         console.log(number) /* 這里的number是不能夠即使改變的  */
       } } ></button>
   </div>)
}
useState 注意事項(xiàng):

① 在函數(shù)組件一次執(zhí)行上下文中,state 的值是固定不變的。

function Index(){
    const [ number, setNumber ] = React.useState(0)
    const handleClick = () => setInterval(()=>{
        // 此時(shí) number 一直都是 0
        setNumber(number + 1 )
    },1000)
    return <button onClick={ handleClick } > 點(diǎn)擊 { number }</button>
}
② 如果兩次 dispatchAction 傳入相同的 state 值,那么組件就不會更新。

export default function Index(){
    const [ state  , dispatchState ] = useState({ name:'alien' })
    const  handleClick = ()=>{ // 點(diǎn)擊按鈕,視圖沒有更新。
        state.name = 'Alien'
        dispatchState(state) // 直接改變 `state`,在內(nèi)存中指向的地址相同。
    }
    return <div>
         <span> { state.name }</span>
        <button onClick={ handleClick }  >changeName++</button>
    </div>
}
③ 當(dāng)觸發(fā) dispatchAction 在當(dāng)前執(zhí)行上下文中獲取不到最新的 state, 只有再下一次組件 rerender 中才能獲取到。

2.2 useReducer
useReducer 是 react-hooks 提供的能夠在無狀態(tài)組件中運(yùn)行的類似redux的功能 api 。

useReducer 基礎(chǔ)介紹:

const [ ①state , ②dispatch ] = useReducer(③reducer)
① 更新之后的 state 值。

② 派發(fā)更新的 dispatchAction 函數(shù), 本質(zhì)上和 useState 的 dispatchAction 是一樣的。

③ 一個(gè)函數(shù) reducer ,我們可以認(rèn)為它就是一個(gè) redux 中的 reducer , reducer的參數(shù)就是常規(guī)reducer里面的state和action, 返回改變后的state, 這里有一個(gè)需要注意的點(diǎn)就是:如果返回的 state 和之前的 state ,內(nèi)存指向相同,那么組件將不會更新。

useReducer 基礎(chǔ)用法:

const DemoUseReducer = ()=>{
    /* number為更新后的state值,  dispatchNumbner 為當(dāng)前的派發(fā)函數(shù) */
   const [ number , dispatchNumbner ] = useReducer((state,action)=>{
       const { payload , name  } = action
       /* return的值為新的state */
       switch(name){
           case 'add':
               return state + 1
           case 'sub':
               return state - 1
           case 'reset':
             return payload       
       }
       return state
   },0)
   return <div>
      當(dāng)前值:{ number }
      { /* 派發(fā)更新 */ }
      <button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button>
      <button onClick={()=>dispatchNumbner({ name:'sub' })} >減少</button>
      <button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >賦值</button>
      { /* 把dispatch 和 state 傳遞給子組件  */ }
      <MyChildren  dispatch={ dispatchNumbner } State={{ number }} />
   </div>
}
2.3 useSyncExternalStore
useSyncExternalStore 的誕生并非偶然,和 v18 的更新模式下外部數(shù)據(jù)的 tearing 有著十分緊密的關(guān)聯(lián)。useSyncExternalStore 能夠讓 React 組件在 concurrent 模式下安全地有效地讀取外接數(shù)據(jù)源,在組件渲染過程中能夠檢測到變化,并且在數(shù)據(jù)源發(fā)生變化的時(shí)候,能夠調(diào)度更新。當(dāng)讀取到外部狀態(tài)發(fā)生了變化,會觸發(fā)一個(gè)強(qiáng)制更新,來保證結(jié)果的一致性。

useSyncExternalStore 基礎(chǔ)介紹:

useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
)
① subscribe 為訂閱函數(shù),當(dāng)數(shù)據(jù)改變的時(shí)候,會觸發(fā) subscribe,在 useSyncExternalStore 會通過帶有記憶性的 getSnapshot 來判別數(shù)據(jù)是否發(fā)生變化,如果發(fā)生變化,那么會強(qiáng)制更新數(shù)據(jù)。

② getSnapshot 可以理解成一個(gè)帶有記憶功能的選擇器。當(dāng) store 變化的時(shí)候,會通過 getSnapshot 生成新的狀態(tài)值,這個(gè)狀態(tài)值可提供給組件作為數(shù)據(jù)源使用,getSnapshot 可以檢查訂閱的值是否改變,改變的話那么會觸發(fā)更新。

③ getServerSnapshot 用于 hydration 模式下的 getSnapshot。

useSyncExternalStore 基礎(chǔ)用法:

import { combineReducers , createStore  } from 'redux'

/* number Reducer */
function numberReducer(state=1,action){
    switch (action.type){
      case 'ADD':
        return state + 1
      case 'DEL':
        return state - 1
      default:
        return state
    }
}

/* 注冊reducer */
const rootReducer = combineReducers({ number:numberReducer  })
/* 創(chuàng)建 store */
const store = createStore(rootReducer,{ number:1  })

function Index(){
    /* 訂閱外部數(shù)據(jù)源 */
    const state = useSyncExternalStore(store.subscribe,() => store.getState().number)
    console.log(state)
    return <div>
        {state}
        <button onClick={() => store.dispatch({ type:'ADD' })} >點(diǎn)擊</button>
    </div>
}
點(diǎn)擊按鈕,會觸發(fā) reducer ,然后會觸發(fā) store.subscribe 訂閱函數(shù),執(zhí)行 getSnapshot 得到新的 number ,判斷 number 是否發(fā)生變化,如果變化,觸發(fā)更新。

2.4 useTransition
在 React v18 中,有一種新概念叫做過渡任務(wù),這種任務(wù)是對比立即更新任務(wù)而產(chǎn)生的,通常一些影響用戶交互直觀響應(yīng)的任務(wù),例如按鍵,點(diǎn)擊,輸入等,這些任務(wù)需要視圖上立即響應(yīng),所以可以稱之為立即更新的任務(wù),但是有一些更新不是那么急迫,比如頁面從一個(gè)狀態(tài)過渡到另外一個(gè)狀態(tài),這些任務(wù)就叫做過渡任務(wù)。打個(gè)比方如下圖當(dāng)點(diǎn)擊 tab 從 tab1 切換到 tab2 的時(shí)候,本質(zhì)上產(chǎn)生了兩個(gè)更新任務(wù)。

第一個(gè)就是 hover 狀態(tài)由 tab1 變成 tab2。

第二個(gè)就是內(nèi)容區(qū)域由 tab1 內(nèi)容變換到 tab2 內(nèi)容。

這兩個(gè)任務(wù),用戶肯定希望 hover 狀態(tài)的響應(yīng)更迅速,而內(nèi)容的響應(yīng)有可能還需要請求數(shù)據(jù)等操作,所以更新狀態(tài)并不是立馬生效,通常還會有一些 loading 效果。所以第一個(gè)任務(wù)作為立即執(zhí)行任務(wù),而第二個(gè)任務(wù)就可以視為過渡任務(wù)。



WechatIMG6496.jpeg
useTransition 基礎(chǔ)介紹:

useTransition 執(zhí)行返回一個(gè)數(shù)組。數(shù)組有兩個(gè)狀態(tài)值:

第一個(gè)是,當(dāng)處于過渡狀態(tài)的標(biāo)志——isPending。
第二個(gè)是一個(gè)方法,可以理解為上述的 startTransition??梢园牙锩娴母氯蝿?wù)變成過渡任務(wù)。
import { useTransition } from 'react'
/* 使用 */
const  [ isPending , startTransition ] = useTransition ()
useTransition 基礎(chǔ)用法:

除了上述切換 tab 場景外,還有很多場景非常適合 useTransition 產(chǎn)生的過渡任務(wù),比如輸入內(nèi)容,實(shí)時(shí)搜索并展示數(shù)據(jù),這本質(zhì)上也是有兩個(gè)優(yōu)先級的任務(wù),第一個(gè)任務(wù)就是受控表單的實(shí)時(shí)響應(yīng);第二個(gè)就是輸入內(nèi)容改變,數(shù)據(jù)展示的變化。那么接下來我們寫一個(gè) demo 來看一下 useTransition 的基本使用。

/* 模擬數(shù)據(jù) */
const mockList1 = new Array(10000).fill('tab1').map((item,index)=>item+'--'+index )
const mockList2 = new Array(10000).fill('tab2').map((item,index)=>item+'--'+index )
const mockList3 = new Array(10000).fill('tab3').map((item,index)=>item+'--'+index )

const tab = {
  tab1: mockList1,
  tab2: mockList2,
  tab3: mockList3
}

export default function Index(){
  const [ active, setActive ] = React.useState('tab1') //需要立即響應(yīng)的任務(wù),立即更新任務(wù)
  const [ renderData, setRenderData ] = React.useState(tab[active]) //不需要立即響應(yīng)的任務(wù),過渡任務(wù)
  const [ isPending,startTransition  ] = React.useTransition()
  const handleChangeTab = (activeItem) => {
     setActive(activeItem) // 立即更新
     startTransition(()=>{ // startTransition 里面的任務(wù)優(yōu)先級低
       setRenderData(tab[activeItem])
     })
  }
  return <div>
    <div className='tab' >
       { Object.keys(tab).map((item)=> <span className={ active === item && 'active' } onClick={()=>handleChangeTab(item)} >{ item }</span> ) }
    </div>
    <ul className='content' >
       { isPending && <div> loading... </div> }
       { renderData.map(item=> <li key={item} >{item}</li>) }
    </ul>
  </div>
}





如上當(dāng)切換 tab 的時(shí)候,產(chǎn)生了兩個(gè)優(yōu)先級任務(wù),第一個(gè)任務(wù)是 setActive 控制 tab active 狀態(tài)的改變,第二個(gè)任務(wù)為 setRenderData 控制渲染的長列表數(shù)據(jù) (在現(xiàn)實(shí)場景下長列表可能是一些數(shù)據(jù)量大的可視化圖表)。

2.5 useDeferredValue
React 18 提供了 useDeferredValue 可以讓狀態(tài)滯后派生。useDeferredValue 的實(shí)現(xiàn)效果也類似于 transtion,當(dāng)迫切的任務(wù)執(zhí)行后,再得到新的狀態(tài),而這個(gè)新的狀態(tài)就稱之為 DeferredValue。

useDeferredValue 基礎(chǔ)介紹:

useDeferredValue 和上述 useTransition 本質(zhì)上有什么異同呢?


相同點(diǎn): useDeferredValue 本質(zhì)上和內(nèi)部實(shí)現(xiàn)與 useTransition 一樣都是標(biāo)記成了過渡更新任務(wù)。

不同點(diǎn): useTransition 是把 startTransition 內(nèi)部的更新任務(wù)變成了過渡任務(wù)transtion,而 useDeferredValue 是把原值通過過渡任務(wù)得到新的值,這個(gè)值作為延時(shí)狀態(tài)。一個(gè)是處理一段邏輯,另一個(gè)是生產(chǎn)一個(gè)新的狀態(tài)。

useDeferredValue 接受一個(gè)參數(shù) value ,一般為可變的 state , 返回一個(gè)延時(shí)狀態(tài) deferrredValue。

const deferrredValue = React.useDeferredValue(value)
useDeferredValue 基礎(chǔ)用法:

接下來把上邊那個(gè)例子用 useDeferredValue 來實(shí)現(xiàn)。

export default function Index(){
  const [ active, setActive ] = React.useState('tab1') //需要立即響應(yīng)的任務(wù),立即更新任務(wù)
  const deferActive = React.useDeferredValue(active) // 把狀態(tài)延時(shí)更新,類似于過渡任務(wù)
  const handleChangeTab = (activeItem) => {
     setActive(activeItem) // 立即更新
  }
  const renderData = tab[deferActive] // 使用滯后狀態(tài)
  return <div>
    <div className='tab' >
       { Object.keys(tab).map((item)=> <span className={ active === item && 'active' } onClick={()=>handleChangeTab(item)} >{ item }</span> ) }
    </div>
    <ul className='content' >
       { renderData.map(item=> <li key={item} >{item}</li>) }
    </ul>
  </div>
  }
如上 active 為正常改變的狀態(tài),deferActive 為滯后的 active 狀態(tài),我們使用正常狀態(tài)去改變 tab 的 active 狀態(tài),使用滯后的狀態(tài)去更新視圖,同樣達(dá)到了提升用戶體驗(yàn)的作用。

三 hooks 之執(zhí)行副作用
3.1 useEffect
React hooks也提供了 api ,用于彌補(bǔ)函數(shù)組件沒有生命周期的缺陷。其本質(zhì)主要是運(yùn)用了 hooks 里面的 useEffect , useLayoutEffect,還有 useInsertionEffect。其中最常用的就是 useEffect 。我們首先來看一下 useEffect 的使用。

useEffect 基礎(chǔ)介紹:

useEffect(()=>{
    return destory
},dep)
useEffect 第一個(gè)參數(shù) callback, 返回的 destory , destory 作為下一次callback執(zhí)行之前調(diào)用,用于清除上一次 callback 產(chǎn)生的副作用。

第二個(gè)參數(shù)作為依賴項(xiàng),是一個(gè)數(shù)組,可以有多個(gè)依賴項(xiàng),依賴項(xiàng)改變,執(zhí)行上一次callback 返回的 destory ,和執(zhí)行新的 effect 第一個(gè)參數(shù) callback 。

對于 useEffect 執(zhí)行, React 處理邏輯是采用異步調(diào)用 ,對于每一個(gè) effect 的 callback, React 會向 setTimeout回調(diào)函數(shù)一樣,放入任務(wù)隊(duì)列,等到主線程任務(wù)完成,DOM 更新,js 執(zhí)行完成,視圖繪制完畢,才執(zhí)行。所以 effect 回調(diào)函數(shù)不會阻塞瀏覽器繪制視圖。

useEffect 基礎(chǔ)用法:

/* 模擬數(shù)據(jù)交互 */
function getUserInfo(a){
    return new Promise((resolve)=>{
        setTimeout(()=>{
           resolve({
               name:a,
               age:16,
           })
        },500)
    })
}

const Demo = ({ a }) => {
    const [ userMessage , setUserMessage ] :any= useState({})
    const div= useRef()
    const [number, setNumber] = useState(0)
    /* 模擬事件監(jiān)聽處理函數(shù) */
    const handleResize =()=>{}
    /* useEffect使用 ,這里如果不加限制 ,會是函數(shù)重復(fù)執(zhí)行,陷入死循環(huán)*/
    useEffect(()=>{
       /* 請求數(shù)據(jù) */
       getUserInfo(a).then(res=>{
           setUserMessage(res)
       })
       /* 定時(shí)器 延時(shí)器等 */
       const timer = setInterval(()=>console.log(666),1000)
       /* 操作dom  */
       console.log(div.current) /* div */
       /* 事件監(jiān)聽等 */
       window.addEventListener('resize', handleResize)
         /* 此函數(shù)用于清除副作用 */
       return function(){
           clearInterval(timer)
           window.removeEventListener('resize', handleResize)
       }
    /* 只有當(dāng)props->a和state->number改變的時(shí)候 ,useEffect副作用函數(shù)重新執(zhí)行 ,如果此時(shí)數(shù)組為空[],證明函數(shù)只有在初始化的時(shí)候執(zhí)行一次相當(dāng)于componentDidMount */
    },[ a ,number ])
    return (<div ref={div} >
        <span>{ userMessage.name }</span>
        <span>{ userMessage.age }</span>
        <div onClick={ ()=> setNumber(1) } >{ number }</div>
    </div>)
}
如上在 useEffect 中做的功能如下:

① 請求數(shù)據(jù)。
② 設(shè)置定時(shí)器,延時(shí)器等。
③ 操作 dom , 在 React Native 中可以通過 ref 獲取元素位置信息等內(nèi)容。
④ 注冊事件監(jiān)聽器, 事件綁定,在 React Native 中可以注冊 NativeEventEmitter 。
⑤ 還可以清除定時(shí)器,延時(shí)器,解綁事件監(jiān)聽器等。
3.2 useLayoutEffect
useLayoutEffect 基礎(chǔ)介紹:

useLayoutEffect 和 useEffect 不同的地方是采用了同步執(zhí)行,那么和useEffect有什么區(qū)別呢?

① 首先 useLayoutEffect 是在 DOM 更新之后,瀏覽器繪制之前,這樣可以方便修改 DOM,獲取 DOM 信息,這樣瀏覽器只會繪制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 執(zhí)行是在瀏覽器繪制視圖之后,接下來又改 DOM ,就可能會導(dǎo)致瀏覽器再次回流和重繪。而且由于兩次繪制,視圖上可能會造成閃現(xiàn)突兀的效果。

② useLayoutEffect callback 中代碼執(zhí)行會阻塞瀏覽器繪制。

useEffect 基礎(chǔ)用法:

const DemoUseLayoutEffect = () => {
    const target = useRef()
    useLayoutEffect(() => {
        /*我們需要在dom繪制之前,移動dom到制定位置*/
        const { x ,y } = getPositon() /* 獲取要移動的 x,y坐標(biāo) */
        animate(target.current,{ x,y })
    }, []);
    return (
        <div >
            <span ref={ target } className="animate"></span>
        </div>
    )
}
3.3 useInsertionEffect
useInsertionEffect 基礎(chǔ)介紹:

useInsertionEffect 是在 React v18 新添加的 hooks ,它的用法和 useEffect 和 useLayoutEffect 一樣。那么這個(gè) hooks 用于什么呢?

在介紹 useInsertionEffect 用途之前,先看一下 useInsertionEffect 的執(zhí)行時(shí)機(jī)。

React.useEffect(()=>{
    console.log('useEffect 執(zhí)行')
},[])

React.useLayoutEffect(()=>{
    console.log('useLayoutEffect 執(zhí)行')
},[])

React.useInsertionEffect(()=>{
    console.log('useInsertionEffect 執(zhí)行')
},[])
打?。簎seInsertionEffect 執(zhí)行 -> useLayoutEffect 執(zhí)行 -> useEffect 執(zhí)行

可以看到 useInsertionEffect 的執(zhí)行時(shí)機(jī)要比 useLayoutEffect 提前,useLayoutEffect 執(zhí)行的時(shí)候 DOM 已經(jīng)更新了,但是在 useInsertionEffect 的執(zhí)行的時(shí)候,DOM 還沒有更新。本質(zhì)上 useInsertionEffect 主要是解決 CSS-in-JS 在渲染中注入樣式的性能問題。這個(gè) hooks 主要是應(yīng)用于這個(gè)場景,在其他場景下 React 不期望用這個(gè) hooks 。

useInsertionEffect 模擬使用:

export default function Index(){

  React.useInsertionEffect(()=>{
     /* 動態(tài)創(chuàng)建 style 標(biāo)簽插入到 head 中 */
     const style = document.createElement('style')
     style.innerHTML = `
       .css-in-js{
         color: red;
         font-size: 20px;
       }
     `
     document.head.appendChild(style)
  },[])

  return <div className="css-in-js" > hello , useInsertionEffect </div>
}
如上模擬了 useInsertionEffect 的使用。

四 hooks 之狀態(tài)獲取與傳遞
4.1 useContext
useContext 基礎(chǔ)介紹

可以使用 useContext ,來獲取父級組件傳遞過來的 context 值,這個(gè)當(dāng)前值就是最近的父級組件 Provider 設(shè)置的 value 值,useContext 參數(shù)一般是由 createContext 方式創(chuàng)建的 ,也可以父級上下文 context 傳遞的 ( 參數(shù)為 context )。useContext 可以代替 context.Consumer 來獲取 Provider 中保存的 value 值。

const contextValue = useContext(context)
useContext 接受一個(gè)參數(shù),一般都是 context 對象,返回值為 context 對象內(nèi)部保存的 value 值。

useContext 基礎(chǔ)用法:

/* 用useContext方式 */
const DemoContext = ()=> {
    const value:any = useContext(Context)
    /* my name is alien */
return <div> my name is { value.name }</div>
}

/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
    return <Context.Consumer>
         {/*  my name is alien  */}
        { (value)=> <div> my name is { value.name }</div> }
    </Context.Consumer>
}

export default ()=>{
    return <div>
        <Context.Provider value={{ name:'alien' , age:18 }} >
            <DemoContext />
            <DemoContext1 />
        </Context.Provider>
    </div>
}
4.2 useRef
useRef 基礎(chǔ)介紹:

useRef 可以用來獲取元素,緩存狀態(tài),接受一個(gè)狀態(tài) initState 作為初始值,返回一個(gè) ref 對象 cur, cur 上有一個(gè) current 屬性就是 ref 對象需要獲取的內(nèi)容。

const cur = React.useRef(initState)
console.log(cur.current)
useRef 基礎(chǔ)用法:

useRef 獲取 DOM 元素,在 React Native 中雖然沒有 DOM 元素,但是也能夠獲取組件的節(jié)點(diǎn)信息( fiber 信息 )。

const DemoUseRef = ()=>{
    const dom= useRef(null)
    const handerSubmit = ()=>{
        /*  <div >表單組件</div>  dom 節(jié)點(diǎn) */
        console.log(dom.current)
    }
    return <div>
        {/* ref 標(biāo)記當(dāng)前dom節(jié)點(diǎn) */}
        <div ref={dom} >表單組件</div>
        <button onClick={()=>handerSubmit()} >提交</button>
    </div>
}
如上通過 useRef 來獲取 DOM 節(jié)點(diǎn)。

useRef 保存狀態(tài), 可以利用 useRef 返回的 ref 對象來保存狀態(tài),只要當(dāng)前組件不被銷毀,那么狀態(tài)就會一直存在。

const status = useRef(false)
/* 改變狀態(tài) */
const handleChangeStatus = () => {
  status.current = true
}
4.3 useImperativeHandle
useImperativeHandle 基礎(chǔ)介紹:

useImperativeHandle 可以配合 forwardRef 自定義暴露給父組件的實(shí)例值。這個(gè)很有用,我們知道,對于子組件,如果是 class 類組件,我們可以通過 ref 獲取類組件的實(shí)例,但是在子組件是函數(shù)組件的情況,如果我們不能直接通過 ref 的,那么此時(shí) useImperativeHandle 和 forwardRef 配合就能達(dá)到效果。

useImperativeHandle 接受三個(gè)參數(shù):

① 第一個(gè)參數(shù)ref: 接受 forWardRef 傳遞過來的 ref。
② 第二個(gè)參數(shù) createHandle :處理函數(shù),返回值作為暴露給父組件的 ref 對象。
③ 第三個(gè)參數(shù) deps : 依賴項(xiàng) deps ,依賴項(xiàng)更改形成新的 ref 對象。
useImperativeHandle 基礎(chǔ)用法:

我們來模擬給場景,用useImperativeHandle,使得父組件能讓子組件中的input自動賦值并聚焦。






function Son (props,ref) {
    console.log(props)
    const inputRef = useRef(null)
    const [ inputValue , setInputValue ] = useState('')
    useImperativeHandle(ref,()=>{
       const handleRefs = {
           /* 聲明方法用于聚焦input框 */
           onFocus(){
              inputRef.current.focus()
           },
           /* 聲明方法用于改變input的值 */
           onChangeValue(value){
               setInputValue(value)
           }
       }
       return handleRefs
    },[])
    return <div>
        <input
            placeholder="請輸入內(nèi)容"
            ref={inputRef}
            value={inputValue}
        />
    </div>
}

const ForwarSon = forwardRef(Son)

class Index extends React.Component{
    inputRef = null
    handerClick(){
       const { onFocus , onChangeValue } =this.cur
       onFocus()
       onChangeValue('let us learn React!')
    }
    render(){
        return <div style={{ marginTop:'50px' }} >
            <ForwarSon ref={node => (this.inputRef = node)} />
            <button onClick={this.handerClick.bind(this)} >操控子組件</button>
        </div>
    }
}
效果:



8e8c05f0c82c43719079d4db9536abc0_tplv-k3u1fbpfcp-watermark.gif
五 hooks 之狀態(tài)派生與保存
5.1 useMemo
useMemo 可以在函數(shù)組件 render 上下文中同步執(zhí)行一個(gè)函數(shù)邏輯,這個(gè)函數(shù)的返回值可以作為一個(gè)新的狀態(tài)緩存起來。那么這個(gè) hooks 的作用就顯而易見了:

場景一:在一些場景下,需要在函數(shù)組件中進(jìn)行大量的邏輯計(jì)算,那么我們不期望每一次函數(shù)組件渲染都執(zhí)行這些復(fù)雜的計(jì)算邏輯,所以就需要在 useMemo 的回調(diào)函數(shù)中執(zhí)行這些邏輯,然后把得到的產(chǎn)物(計(jì)算結(jié)果)緩存起來就可以了。

場景二:React 在整個(gè)更新流程中,diff 起到了決定性的作用,比如 Context 中的 provider 通過 diff value 來判斷是否更新

useMemo 基礎(chǔ)介紹:

const cacheSomething = useMemo(create,deps)
① create:第一個(gè)參數(shù)為一個(gè)函數(shù),函數(shù)的返回值作為緩存值,如上 demo 中把 Children 對應(yīng)的 element 對象,緩存起來。
② deps:第二個(gè)參數(shù)為一個(gè)數(shù)組,存放當(dāng)前 useMemo 的依賴項(xiàng),在函數(shù)組件下一次執(zhí)行的時(shí)候,會對比 deps 依賴項(xiàng)里面的狀態(tài),是否有改變,如果有改變重新執(zhí)行 create ,得到新的緩存值。
③ acheSomething:返回值,執(zhí)行 create 的返回值。如果 deps 中有依賴項(xiàng)改變,返回的重新執(zhí)行 create 產(chǎn)生的值,否則取上一次緩存值。
useMemo 基礎(chǔ)用法:

派生新狀態(tài):

function Scope() {
    const keeper = useKeep()
    const { cacheDispatch, cacheList, hasAliveStatus } = keeper
   
    /* 通過 useMemo 得到派生出來的新狀態(tài) contextValue  */
    const contextValue = useMemo(() => {
        return {
            cacheDispatch: cacheDispatch.bind(keeper),
            hasAliveStatus: hasAliveStatus.bind(keeper),
            cacheDestory: (payload) => cacheDispatch.call(keeper, { type: ACTION_DESTORY, payload })
        }
      
    }, [keeper])
    return <KeepaliveContext.Provider value={contextValue}>
    </KeepaliveContext.Provider>
}
如上通過 useMemo 得到派生出來的新狀態(tài) contextValue ,只有 keeper 變化的時(shí)候,才改變 Provider 的 value 。

緩存計(jì)算結(jié)果:

function Scope(){
    const style = useMemo(()=>{
      let computedStyle = {}
      // 經(jīng)過大量的計(jì)算
      return computedStyle
    },[])
    return <div style={style} ></div>
}
緩存組件,減少子組件 rerender 次數(shù):

function Scope ({ children }){
   const renderChild = useMemo(()=>{ children()  },[ children ])
   return <div>{ renderChild } </div>
}
5.2 useCallback
useCallback 基礎(chǔ)介紹:

useMemo 和 useCallback 接收的參數(shù)都是一樣,都是在其依賴項(xiàng)發(fā)生變化后才執(zhí)行,都是返回緩存的值,區(qū)別在于 useMemo 返回的是函數(shù)運(yùn)行的結(jié)果,useCallback 返回的是函數(shù),這個(gè)回調(diào)函數(shù)是經(jīng)過處理后的也就是說父組件傳遞一個(gè)函數(shù)給子組件的時(shí)候,由于是無狀態(tài)組件每一次都會重新生成新的 props 函數(shù),這樣就使得每一次傳遞給子組件的函數(shù)都發(fā)生了變化,這時(shí)候就會觸發(fā)子組件的更新,這些更新是沒有必要的,此時(shí)我們就可以通過 usecallback 來處理此函數(shù),然后作為 props 傳遞給子組件。

useCallback 基礎(chǔ)用法:

/* 用react.memo */
const DemoChildren = React.memo((props)=>{
   /* 只有初始化的時(shí)候打印了 子組件更新 */
    console.log('子組件更新')
   useEffect(()=>{
       props.getInfo('子組件')
   },[])
   return <div>子組件</div>
})

const DemoUseCallback=({ id })=>{
    const [number, setNumber] = useState(1)
    /* 此時(shí)usecallback的第一參數(shù) (sonName)=>{ console.log(sonName) }
     經(jīng)過處理賦值給 getInfo */
    const getInfo  = useCallback((sonName)=>{
          console.log(sonName)
    },[id])
    return <div>
        {/* 點(diǎn)擊按鈕觸發(fā)父組件更新 ,但是子組件沒有更新 */}
        <button onClick={ ()=>setNumber(number+1) } >增加</button>
        <DemoChildren getInfo={getInfo} />
    </div>
}
六 hooks 之工具 hooks
6.1 useDebugValue
我們不推薦你向每個(gè)自定義 Hook 添加 debug 值。當(dāng)它作為共享庫的一部分時(shí)才最有價(jià)值。在某些情況下,格式化值的顯示可能是一項(xiàng)開銷很大的操作。除非需要檢查 Hook,否則沒有必要這么做。因此,useDebugValue 接受一個(gè)格式化函數(shù)作為可選的第二個(gè)參數(shù)。該函數(shù)只有在 Hook 被檢查時(shí)才會被調(diào)用。它接受 debug 值作為參數(shù),并且會返回一個(gè)格式化的顯示值。

useDebugValue 基礎(chǔ)介紹:

useDebugValue 可用于在 React 開發(fā)者工具中顯示自定義 hook 的標(biāo)簽。這個(gè)hooks目的就是檢查自定義hooks。

useDebugValue 基本使用:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  // ...
  // 在開發(fā)者工具中的這個(gè) Hook 旁邊顯示標(biāo)簽
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}
6.2 useId
useID 基礎(chǔ)介紹:

useId 也是 React v18 產(chǎn)生的新的 hooks , 它可以在 client 和 server 生成唯一的 id , 解決了在服務(wù)器渲染中,服務(wù)端和客戶端產(chǎn)生 id 不一致的問題,更重要的是保障了 React v18 中 streaming renderer (流式渲染) 中 id 的穩(wěn)定性。

低版本 React ssr 存在的問題:

比如在一些項(xiàng)目或者是開源庫中用 Math.random() 作為 ID 的時(shí)候,可以會有一些隨機(jī)生成 id 的場景:

const rid = Math.random() + '_id_'  /* 生成一個(gè)隨機(jī)id  */
function Demo (){
   // 使用 rid
   return <div id={rid} ></div>
}
這在純客戶端渲染中沒有問題,但是在服務(wù)端渲染的時(shí)候,傳統(tǒng)模式下需要走如下流程:



e54da686-6d8e-4431-a378-c05ac49cb6fb.png
在這個(gè)過程中,當(dāng)服務(wù)端渲染到 html 和 hydrate 過程分別在服務(wù)端和客戶端進(jìn)行,但是會走兩遍 id 的生成流程,這樣就會造成 id不一致的情況發(fā)生。useId 的出現(xiàn)能有效的解決這個(gè)問題。

useId 基本用法:

function Demo (){
   const rid = useId() // 生成穩(wěn)定的 id
   return <div id={rid} ></div>
}
v18 ssr

在 React v18 中 對 ssr 增加了流式渲染的特性 New Suspense SSR Architecture in React 18 , 那么這個(gè)特性是什么呢?我們來看一下:

在傳統(tǒng) React ssr 中,如果正常情況下, hydrate 過程如下所示:



WechatIMG6936.jpeg
剛開始的時(shí)候,因?yàn)榉?wù)端渲染,只會渲染 html 結(jié)構(gòu),此時(shí)還沒注入 js 邏輯,所以我們把它用灰色不能交互的模塊表示。(如上灰色的模塊不能做用戶交互,比如點(diǎn)擊事件之類的。)

hydrate js 加載之后,此時(shí)的模塊可以正常交互,所以用綠色的模塊展示。

但是如果其中一個(gè)模塊,服務(wù)端請求數(shù)據(jù),數(shù)據(jù)量比較大,耗費(fèi)時(shí)間長,我們不期望在服務(wù)端完全形成 html 之后在渲染,那么 React 18 給了一個(gè)新的可能性??梢允褂冒b頁面的一部分,然后讓這一部分的內(nèi)容先掛起。

接下來會通過 script 加載 js 的方式 流式注入 html 代碼的片段,來補(bǔ)充整個(gè)頁面。接下來的流程如下所示:



d94d8ddb-bdcd-4be8-a851-4927c7966b99.png
在這個(gè)原理基礎(chǔ)之上, React 這個(gè)特性叫 Selective Hydration,可以根據(jù)用戶交互改變 hydrate 的順序。

比如有兩個(gè)模塊都是通過 Suspense 掛起的,當(dāng)兩個(gè)模塊發(fā)生交互邏輯時(shí),會根據(jù)交互來選擇性地改變 hydrate 的順序。



ede45613-9994-4e77-9f50-5b7c1faf1160.png
如上 C D 選擇性的 hydrate 就是 Selective Hydration 的結(jié)果。那么回到主角 useId 上,如果在 hydrate 過程中,C D 模塊 id 是動態(tài)生成的,比如如下:

let id = 0
function makeId(){
  return id++
}
function Demo(){
  const id = useRef( makeId() )
  return <div id={id}  >...</div>
}
那么如果組件是 Selective Hydration , 那么注冊組件的順序服務(wù)端和客戶端有可能不統(tǒng)一,這樣表現(xiàn)就會不一致了。那么用 useId 動態(tài)生成 id 就不會有這個(gè)問題產(chǎn)生了,所以說 useId 保障了 React v18 中 streaming renderer (流式渲染) 中 id 的穩(wěn)定性。

七 總結(jié)
本文詳細(xì)介紹了 React Hooks 產(chǎn)生初衷以及 React Hooks,希望看到這篇文章的同學(xué),可以記住每一個(gè) hooks 的使用場景,在項(xiàng)目中熟練使用起來。

參考文檔
streaming renderer
react-hooks如何使用?
React進(jìn)階實(shí)踐指南

作者:??


歡迎關(guān)注微信公眾號 :前端印象