搞懂這 12 個 Hooks,保證讓你玩轉 React

大家好,我是小杜杜,React Hooks的發佈已經有三年多了,它給函數式組件帶來了生命週期,現如今,Hooks逐漸取代class組件,相信各位 React 開發的小夥伴已經深有體會,然而你真的完全掌握 hooks 了嗎?知道如何去做一個好的自定義 hooks 嗎?

我們知道React HooksuseState設置變量,useEffect副作用,useRef來獲取元素的所有屬性,還有useMemouseCallback來做性能優化,當然還有一個自定義Hooks, 來創造出你所想要的Hooks

接下來我們來看看以下幾個問題,問問自己,是否全都知道:

如果你對以上問題有疑問,有好奇,那麼這篇文章應該能夠幫助到你~

本文將會以介紹自定義 Hooks 來解答上述問題,並結合 TSahooks 中的鉤子,以案列的形式去演示,本文過長,建議:點贊 + 收藏 哦~

注:這裏講解的自定義鉤子可能會和 ahooks上的略有不同,不會考慮過多的情況,如果用於項目,建議直接使用ahooks上的鉤子~

如果有小夥伴不懂 TS,可以看看我的這篇文章:一篇讓你完全夠用 TS 的指南 [1]

先附上一張今天的知識圖,還請各位小夥伴多多支持:

深入 Hooks.png

自定義 Hooks 是什麼?

react-hooksReact16.8以後新增的鉤子 API,目的是增加代碼的可複用性、邏輯性,最主要的是解決了函數式組件無狀態的問題,這樣既保留了函數式的簡單,又解決了沒有數據管理狀態的缺陷

那麼什麼是自定義 hooks 呢?

自定義hooks是在react-hooks基礎上的一個擴展,可以根據業務、需求去制定相應的hooks, 將常用的邏輯進行封裝,從而具備複用性

如何設計一個自定義 Hooks

hooks本質上是一個函數,而這個函數主要就是邏輯複用,我們首先要知道一件事,hooks的驅動條件是什麼?

其實就是props的修改,useStateuseReducer的使用是無狀態組件更新的條件,從而驅動自定義 hooks

通用模式

自定義 hooks 的名稱是以 use 開頭,我們設計爲:

const [xxx, ...] = useXXX(參數一,參數二...)

簡單的小例子:usePow

我們先寫一個簡單的小例子來了解下自定義hooks

// usePow.ts
const Index = (list: number[]) ={

  return list.map((item:number) ={
    console.log(1)
    return Math.pow(item, 2)
  })
}

export default Index;

// index.tsx
import { Button } from 'antd-mobile';
import React,{ useState } from 'react';
import { usePow } from '@/components';

const Index:React.FC<any> = (props)={
  const [flag, setFlag] = useState<boolean>(true)
  const data = usePow([1, 2, 3])
  
  return (
    <div>
      <div>數字:{JSON.stringify(data)}</div>
      <Button color='primary' onClick={() ={setFlag(v => !v)}}>切換</Button>
       <div>切換狀態:{JSON.stringify(flag)}</div>
    </div>
  );
}

export default Index;

我們簡單的寫了個 usePow,我們通過 usePow 給所傳入的數字平方, 用切換狀態的按鈕表示函數內部的狀態,我們來看看此時的效果:

我們發現了一個問題,爲什麼點擊切換按鈕也會觸發console.log(1)呢?

這樣明顯增加了性能開銷,我們的理想狀態肯定不希望做無關的渲染,所以我們做自定義 hooks的時候一定要注意,需要減少性能開銷, 我們爲組件加入 useMemo試試:

    import { useMemo } from 'react';

    const Index = (list: number[]) ={
      return useMemo(() => list.map((item:number) ={
        console.log(1)
        return Math.pow(item, 2)
      })[]) 
    }
    export default Index;

發現此時就已經解決了這個問題,所以要非常注意一點,一個好用的自定義hooks, 一定要配合useMemouseCallback等 Api 一起使用。

玩轉 React Hooks

在上述中我們講了用 useMemo來處理無關的渲染,接下來我們一起來看看React Hooks的這些鉤子的妙用(這裏建議先熟知、並使用對應的React Hooks, 才能造出好的鉤子)

useMemo

當一個父組件中調用了一個子組件的時候,父組件的 state 發生變化,會導致父組件更新,而子組件雖然沒有發生改變,但也會進行更新。

簡單的理解下,當一個頁面內容非常複雜,模塊非常多的時候,函數式組件會從頭更新到尾,只要一處改變,所有的模塊都會進行刷新,這種情況顯然是沒有必要的。

我們理想的狀態是各個模塊只進行自己的更新,不要相互去影響,那麼此時用useMemo是最佳的解決方案。

這裏要尤其注意一點,只要父組件的狀態更新,無論有沒有對自組件進行操作,子組件都會進行更新useMemo就是爲了防止這點而出現的

在講 useMemo 之前,我們先說說memo,memo的作用是結合了 pureComponent 純組件和 componentShouldUpdate 功能,會對傳入的 props 進行一次對比,然後根據第二個函數返回值來進一步判斷哪些 props 需要更新。(具體使用會在下文講到~)

useMemomemo的理念上差不多,都是判斷是否滿足當前的限定條件來決定是否執行callback函數,而useMemo的第二個參數是一個數組,通過這個數組來判定是否更新回掉函數

這種方式可以運用在元素、組件、上下文中,尤其是利用在數組上,先看一個例子:

    useMemo(() =(
        <div>
            {
                list.map((item, index) =(
                    <p key={index}>
                        {item.name}
                    </>
                )}
            }
        </div>
    ),[list])

從上面我們看出 useMemo只有在list發生變化的時候纔會進行渲染,從而減少了不必要的開銷

總結一下useMemo的好處:

useCallback

useCallbackuseMemo極其類似, 可以說是一模一樣,唯一不同的是useMemo返回的是函數運行的結果,而useCallback返回的是函數

注意:這個函數是父組件傳遞子組件的一個函數,防止做無關的刷新,其次,這個組件必須配合memo, 否則不但不會提升性能,還有可能降低性能

      import React, { useState, useCallback } from 'react';
      import { Button } from 'antd-mobile';

      const MockMemo: React.FC<any> = () ={
        const [count,setCount] = useState(0)
        const [show,setShow] = useState(true)

        const  add = useCallback(()=>{
          setCount(count + 1)
        },[count])

        return (
          <div>
            <div style={{display: 'flex', justifyContent: 'flex-start'}}>
              <TestButton title="普通點擊" onClick={() => setCount(count + 1) }/>
              <TestButton title="useCallback點擊" onClick={add}/>
            </div>
            <div style={{marginTop: 20}}>count: {count}</div>
            <Button onClick={() ={setShow(!show)}}> 切換</Button>
          </div>
        )
      }

      const TestButton = React.memo((props:any)=>{
        console.log(props.title)
        return <Button color='primary' onClick={props.onClick} style={props.title === 'useCallback點擊' ? {
        marginLeft: 20
        } : undefined}>{props.title}</Button>
      })

      export default MockMemo;

我們可以看到,當點擊切換按鈕的時候,沒有經過 useCallback封裝的函數會再次刷新,而進過過 useCallback包裹的函數不會被再次刷新

useRef

useRef 可以獲取當前元素的所有屬性,並且返回一個可變的 ref 對象,並且這個對象只有 current 屬性,可設置 initialValue

通過 useRef 獲取對應的屬性值

我們先看個案例:

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

const Index:React.FC<any> = () ={
  const scrollRef = useRef<any>(null);
  const [clientHeight, setClientHeight ] = useState<number>(0)
  const [scrollTop, setScrollTop ] = useState<number>(0)
  const [scrollHeight, setScrollHeight ] = useState<number>(0)

  const onScroll = () ={
    if(scrollRef?.current){
      let clientHeight = scrollRef?.current.clientHeight; //可視區域高度
      let scrollTop  = scrollRef?.current.scrollTop;  //滾動條滾動高度
      let scrollHeight = scrollRef?.current.scrollHeight; //滾動內容高度
      setClientHeight(clientHeight)
      setScrollTop(scrollTop)
      setScrollHeight(scrollHeight)
    }
  }

  return (
    <div >
      <div >
        <p>可視區域高度:{clientHeight}</p>
        <p>滾動條滾動高度:{scrollTop}</p>
        <p>滾動內容高度:{scrollHeight}</p>
      </div>
      <div style={{height: 200, overflowY: 'auto'}} ref={scrollRef} onScroll={onScroll} >
        <div style={{height: 2000}}></div>
      </div>
    </div>
  );
};

export default Index;

從上述可知,我們可以通過useRef來獲取對應元素的相關屬性,以此來做一些操作

效果:

緩存數據

除了獲取對應的屬性值外,useRef還有一點比較重要的特性,那就是 緩存數據

上述講到我們封裝一個合格的自定義hooks的時候需要結合 useMemouseCallback 等 Api,但我們控制變量的值用 useState 有可能會導致拿到的是舊值,並且如果他們更新會帶來整個組件重新執行,這種情況下,我們使用 useRef 將會是一個非常不錯的選擇

react-redux的源碼中,在 hooks 推出後,react-redux用大量的 useMemo 重做了 Provide 等核心模塊,其中就是運用 useRef 來緩存數據,並且所運用的 useRef() 沒有一個是綁定在 dom 元素上的,都是做數據緩存用的

可以簡單的來看一下:

    // 緩存數據
    /* react-redux 用userRef 來緩存 merge之後的 props */ 
    const lastChildProps = useRef() 
    
    // lastWrapperProps 用 useRef 來存放組件真正的 props信息 
    const lastWrapperProps = useRef(wrapperProps) 
    
    //是否儲存props是否處於正在更新狀態 
    const renderIsScheduled = useRef(false)

    //更新數據
    function captureWrapperProps( 
        lastWrapperProps, 
        lastChildProps, 
        renderIsScheduled, 
        wrapperProps, 
        actualChildProps, 
        childPropsFromStoreUpdate, 
        notifyNestedSubs 
    ) { 
        lastWrapperProps.current = wrapperProps 
        lastChildProps.current = actualChildProps 
        renderIsScheduled.current = false 
   }

我們看到 react-redux 用重新賦值的方法,改變了緩存的數據源,減少了不必要的更新,如過採取useState勢必會重新渲染

useLatest

經過上面的講解我們知道useRef 可以拿到最新值,我們可以進行簡單的封裝,這樣做的好處是:可以隨時確保獲取的是最新值,並且也可以解決閉包問題

   import { useRef } from 'react';

   const useLatest = <T>(value: T) ={
     const ref = useRef(value)
     ref.current = value

     return ref
   };

   export default useLatest;

結合 useMemo 和 useRef 封裝 useCreation

useCreation :是 useMemouseRef的替代品。換言之,useCreation這個鉤子增強了 useMemouseRef,讓這個鉤子可以替換這兩個鉤子。(來自 ahooks-useCreation[2])

這裏的性能隱患是指:

   // 每次重渲染,都會執行實例化 Subject 的過程,即便這個實例立刻就被扔掉了
   const a = useRef(new Subject()) 
   
   // 通過 factory 函數,可以避免性能隱患
   const b = useCreation(() => new Subject()[])

接下來我們來看看如何封裝一個useCreation, 首先我們要明白以下三點:

明白了一上三點我們就可以自己實現一個useCreation

import { useRef } from 'react';
import type { DependencyList } from 'react';

const depsAreSame = (oldDeps: DependencyList, deps: DependencyList):boolean ={
  if(oldDeps === deps) return true
  
  for(let i = 0; i < oldDeps.length; i++) {
    // 判斷兩個值是否是同一個值
    if(!Object.is(oldDeps[i], deps[i])) return false
  }

  return true
}

const useCreation = <T>(fn:() => T, deps: DependencyList)={

  const { current } = useRef({ 
    deps,
    obj:  undefined as undefined | T ,
    initialized: false
  })

  if(current.initialized === false || !depsAreSame(current.deps, deps)) {
    current.deps = deps;
    current.obj = fn();
    current.initialized = true;
  }

  return current.obj as T
} 

export default useCreation;

useRef判斷是否更新值通過initializeddepsAreSame來判斷,其中depsAreSame通過存儲在 useRef下的deps(舊值) 和 新傳入的 deps(新值)來做對比,判斷兩數組的數據是否一致,來確定是否更新

驗證 useCreation

接下來我們寫個小例子,來驗證下 useCreation是否能滿足我們的要求:

    import React, { useState } from 'react';
    import { Button } from 'antd-mobile';
    import { useCreation } from '@/components';

    const Index: React.FC<any> = () => {
      const [_, setFlag] = useState<boolean>(false)

      const getNowData = () => {
        return Math.random()
      }

      const nowData = useCreation(() => getNowData(), []);

      return (
        <div style={{padding: 50}}>
          <div>正常的函數:{getNowData()}</div>
          <div>useCreation包裹後的:{nowData}</div>
          <Button color='primary' onClick={() => {setFlag(v => !v)}}> 渲染</Button>
        </div>
      )
    }

    export default Index;

我們可以看到,當我們做無關的state改變的時候,正常的函數也會刷新,但useCreation沒有刷新,從而增強了渲染的性能~

useEffect

useEffect相信各位小夥伴已經用的熟的不能再熟了,我們可以使用useEffect來模擬下classcomponentDidMountcomponentWillUnmount的功能。

useMount

這個鉤子不必多說,只是簡化了使用useEffect的第二個參數:

    import { useEffect } from 'react';

    const useMount = (fn: () => void) ={

      useEffect(() ={
        fn?.();
      }[]);
    };

    export default useMount;

useUnmount

這個需要注意一個點,就是使用useRef來確保所傳入的函數爲最新的狀態,所以可以結合上述講的 useLatest 結合使用

    import { useEffect, useRef } from 'react';

    const useUnmount = (fn: () => void) ={

      const ref = useRef(fn);
      ref.current = fn;

      useEffect(
        () =() ={
          fn?.()
        },
        [],
      );
    };

    export default useUnmount;

結合useMountuseUnmount做個小例子

    import { Button, Toast } from 'antd-mobile';
    import React,{ useState } from 'react';
    import { useMount, useUnmount } from '@/components';

    const Child = () ={

      useMount(() ={
        Toast.show('首次渲染')
      });

      useUnmount(() ={
        Toast.show('組件已卸載')
      })

      return <div>你好,我是小杜杜</div>
    }

    const Index:React.FC<any> = (props)={
      const [flag, setFlag] = useState<boolean>(false)

      return (
        <div style={{padding: 50}}>
          <Button color='primary' onClick={() ={setFlag(v => !v)}}>切換 {flag ? 'unmount' : 'mount'}</Button>
          {flag && <Child />}
        </div>
      );
    }

    export default Index;

效果如下:

useUpdate

useUpdate: 強制更新

有的時候我們需要組件強制更新,這個時候就可以使用這個鉤子:

    import { useCallback, useState } from 'react';

    const useUpdate = () ={
      const [, setState] = useState({});

      return useCallback(() => setState({})[]);
    };

    export default useUpdate;
    
    //示例:
    import { Button } from 'antd-mobile';
    import React from 'react';
    import { useUpdate } from '@/components';


    const Index:React.FC<any> = (props)={
      const update = useUpdate();

      return (
        <div style={{padding: 50}}>
          <div>時間:{Date.now()}</div>
          <Button color='primary' onClick={update}>更新時間</Button>
        </div>
      );
    }

    export default Index;

效果如下:

案例

案例 1: useReactive

useReactiv: 一種具備響應式useState

緣由:我們知道用useState可以定義變量其格式爲:

const [count, setCount] = useState<number>(0)

通過setCount來設置,count來獲取,使用這種方式才能夠渲染視圖

來看看正常的操作,像這樣 let count = 0; count =7 此時count的值就是 7,也就是說數據是響應式的

那麼我們可不可以將 useState也寫成響應式的呢?我可以自由設置 count 的值, 並且可以隨時獲取到 count 的最新值,而不是通過setCount來設置。

我們來想想怎麼去實現一個具備 響應式 特點的 useState 也就是 useRective, 提出以下疑問,感興趣的,可以先自行思考一下:

分析

以上四個小問題,最關鍵的就是第二個,我們如何將數據弄成響應式,想要弄成響應式,就必須監聽到值的變化,在做出更改,也就是說,我們對這個數進行操作的時候,要進行相應的攔截,這時就需要ES6的一個知識點:Proxy

在這裏會用到 ProxyReflect 的點,感興趣的可以看看我的這篇文章:🔥花一個小時,迅速瞭解 ES6~ES12 的全部特性 [3]

Proxy:接受的參數是對象,所以第一個問題也解決了,入參就爲對象。那麼如何去刷新視圖呢?這裏就使用上述的 useUpdate 來強制刷新,使數據更改。

至於優化這一塊,使用上文說的useCreation就好,再配合useRef來放initialState即可

代碼

import { useRef } from 'react';
import { useUpdate, useCreation } from '../index';

const observer = <T extends Record<string, any>>(initialVal: T, cb: () => void)T ={

 const proxy = new Proxy<T>(initialVal, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      return typeof res === 'object' ? observer(res, cb) : Reflect.get(target, key);
    },
    set(target, key, val) {
      const ret = Reflect.set(target, key, val);
      cb();
      return ret;
    },
  });

  return proxy;
}

const useReactive = <T extends Record<string, any>>(initialState: T):T ={
  const ref = useRef<T>(initialState);
  const update = useUpdate();

  const state = useCreation(() ={
    return observer(ref.current, () ={
      update();
    });
  }[]);

  return state
};

export default useReactive;

這裏先說下TS,因爲我們不知道會傳遞什麼類型的initialState所以在這需要使用泛型,我們接受的參數是對象,可就是 key-value 的形式,其中 key 爲 string,value 可以是 任意類型,所以我們使用 Record<string, any>

有不熟悉的小夥伴可以看看我的這篇文章:一篇讓你完全夠用 TS 的指南 [4](又推銷一遍,有點打廣告,別在意~)

再來說下攔截這塊, 我們只需要攔截設置(set)獲取(get) 即可,其中:

驗證

接下來我們來驗證一下我們寫的 useReactive, 我們將以 字符串、數字、布爾、數組、函數、計算屬性幾個方面去驗證一下:

    import { Button } from 'antd-mobile';
    import React from 'react';
    import { useReactive } from '@/components'

    const Index:React.FC<any> = (props)={

      const state = useReactive<any>({
        count: 0,
        name: '小杜杜',
        flag: true,
        arr: [],
        bugs: ['小杜杜''react''hook'],
        addBug(bug:string) {
          this.bugs.push(bug);
        },
        get bugsCount() {
          return this.bugs.length;
        },
      })

      return (
        <div style={{padding: 20}}>
          <div style={{fontWeight: 'bold'}}>基本使用:</div>
           <div style={{marginTop: 8}}> 對數字進行操作:{state.count}</div>
           <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
             <Button color='primary' onClick={() => state.count++ } >加1</Button>
             <Button color='primary' style={{marginLeft: 8}} onClick={() => state.count-- } >減1</Button>
             <Button color='primary' style={{marginLeft: 8}} onClick={() => state.count = 7 } >設置爲7</Button>
           </div>
           <div style={{marginTop: 8}}> 對字符串進行操作:{state.name}</div>
           <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
             <Button color='primary' onClick={() => state.name = '小杜杜' } >設置爲小杜杜</Button>
             <Button color='primary' style={{marginLeft: 8}} onClick={() => state.name = 'Domesy'} >設置爲Domesy</Button>
           </div>
           <div style={{marginTop: 8}}> 對布爾值進行操作:{JSON.stringify(state.flag)}</div>
           <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
             <Button color='primary' onClick={() => state.flag = !state.flag } >切換狀態</Button>
           </div>
           <div style={{marginTop: 8}}> 對數組進行操作:{JSON.stringify(state.arr)}</div>
           <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
             <Button color="primary" onClick={() => state.arr.push(Math.floor(Math.random() * 100))} >push</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.pop()} >pop</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.shift()} >shift</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.unshift(Math.floor(Math.random() * 100))} >unshift</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.reverse()} >reverse</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.sort()} >sort</Button>
           </div>
           <div style={{fontWeight: 'bold', marginTop: 8}}>計算屬性:</div>
           <div style={{marginTop: 8}}>數量:{ state.bugsCount } 個</div>
           <div style={{margin: '8px 0'}}>
             <form
               onSubmit={(e) ={
                 state.bug ? state.addBug(state.bug) : state.addBug('domesy')
                 state.bug = '';
                 e.preventDefault();
               }}
             >
               <input type="text" value={state.bug} onChange={(e) =(state.bug = e.target.value)} />
               <button type="submit"  style={{marginLeft: 8}} >增加</button>
               <Button color="primary" style={{marginLeft: 8}} onClick={() => state.bugs.pop()}>刪除</Button>
             </form>

           </div>
           <ul>
             {
               state.bugs.map((bug:any, index:number) =(
                 <li key={index}>{bug}</li>
               ))
             }
           </ul>
        </div>
      );
    }

    export default Index;

效果如下:

案例 2: useEventListener

緣由:我們監聽各種事件的時候需要做監聽,如:監聽點擊事件、鍵盤事件、滾動事件等,我們將其統一封裝起來,方便後續調用

說白了就是在addEventListener的基礎上進行封裝,我們先來想想在此基礎上需要什麼?

首先,useEventListener的入參可分爲三個

在這裏需要注意一點就是在銷燬的時候需要移除對應的監聽事件

代碼

    import { useEffect } from 'react';

    const useEventListener = (event: string, handler: (...e:any) => void, target: any = window) ={

      useEffect(() ={
        const targetElement  = 'current' in target ? target.current : window;
        const useEventListener = (event: Event) ={
          return handler(event)
        }
        targetElement.addEventListener(event, useEventListener)
        return () ={
          targetElement.removeEventListener(event, useEventListener)
        }
      }[event])
    };

    export default useEventListener;

注:這裏把target默認設置成了window,至於爲什麼要這麼寫:'current' in target是因爲我們用useRef拿到的值都是 ref.current

優化

接下來我們一起來看看如何優化這個組件,這裏的優化與 useCreation 類似,但又有不同,原因是這裏的需要判斷的要比useCreation複雜一點。

再次強調一下,傳遞過來的值,優先考慮使用useRef,再考慮用useState,可以直接使用useLatest,防止拿到的值不是最新值

這裏簡單說一下我的思路(又不對的地方或者有更好的建議歡迎評論區指出):

詳細代碼

    import { useEffect } from 'react';
    import type { DependencyList } from 'react';
    import { useRef } from 'react';
    import useLatest from '../useLatest';
    import useUnmount from '../useUnmount';

    const depsAreSame = (oldDeps: DependencyList, deps: DependencyList):boolean ={
      for(let i = 0; i < oldDeps.length; i++) {
        if(!Object.is(oldDeps[i], deps[i])) return false
      }
      return true
    }

    const useEffectTarget = (effect: () => void, deps:DependencyList, target: any) ={

      const hasInitRef = useRef(false); // 一開始設置初始化
      const elementRef = useRef<(Element | null)[]>([]);// 存儲具體的值
      const depsRef = useRef<DependencyList>([]); // 存儲傳遞的deps
      const unmountRef = useRef<any>(); // 存儲對應的effect

      // 初始化 組件的初始化和更新都會執行
      useEffect(() ={
        const targetElement  = 'current' in target ? target.current : window;

        // 第一遍賦值
        if(!hasInitRef.current){
          hasInitRef.current = true;

          elementRef.current = targetElement;
          depsRef.current = deps;
          unmountRef.current = effect();
          return
        }
        // 校驗變值: 目標的值不同, 依賴值改變
        if(elementRef.current !== targetElement || !depsAreSame(deps, depsRef.current)){
          //先執行對應的函數
          unmountRef.current?.();
          //重新進行賦值
          elementRef.current = targetElement;
          depsRef.current = deps; 
          unmountRef.current = effect();
        }
      })

      useUnmount(() ={
        unmountRef.current?.();
        hasInitRef.current = false;
      })
    }

    const useEventListener = (event: string, handler: (...e:any) => void, target: any = window) ={
      const handlerRef = useLatest(handler);

      useEffectTarget(() ={
        const targetElement  = 'current' in target ? target.current : window;

        //  防止沒有 addEventListener 這個屬性
        if(!targetElement?.addEventListener) return;

        const useEventListener = (event: Event) ={
          return handlerRef.current(event)
        }
        targetElement.addEventListener(event, useEventListener)
        return () ={
          targetElement.removeEventListener(event, useEventListener)
        }
      }[event], target)
    };

    export default useEventListener;

驗證

驗證一下useEventListener是否能夠正常的使用,順變驗證一下初始化、卸載的,代碼:

    import React, { useState, useRef } from 'react';
    import { useEventListener } from '@/components'
    import { Button } from 'antd-mobile';

    const Index:React.FC<any> = (props)={

      const [count, setCount] = useState<number>(0)
      const [flag, setFlag] = useState<boolean>(true)
      const [key, setKey] = useState<string>('')
      const ref = useRef(null);

      useEventListener('click'() => setCount(v => v +1), ref)
      useEventListener('keydown'(ev) => setKey(ev.key));

      return (
        <div style={{padding: 20}}>
          <Button color='primary' onClick={() ={setFlag(v => !v)}}>切換 {flag ? 'unmount' : 'mount'}</Button>
          {
            flag && <div>
              <div>數字:{count}</div>
              <button ref={ref} >加1</button>
              <div>監聽鍵盤事件:{key}</div>
            </div>
          }

        </div>
      );
    }

    export default Index;

效果:

我們可以利用useEventListener這個鉤子去封裝其他鉤子,如 鼠標懸停,長按事件,鼠標位置等,在這裏在舉一個鼠標懸停的小例子

小例子 useHover

useHover:監聽 DOM 元素是否有鼠標懸停

這個就很簡單了,只需要通過 useEventListener來監聽mouseentermouseleave即可,在返回布爾值就行了:

    import { useState } from 'react';
    import useEventListener  from '../useEventListener';

    interface Options {
      onEnter?: () => void;
      onLeave?: () => void;
    }

    const useHover = (target:any, options?:Options)boolean ={

      const [flag, setFlag] = useState<boolean>(false)
      const { onEnter, onLeave } = options || {};

      useEventListener('mouseenter'() ={
        onEnter?.()
        setFlag(true)
      }, target)

      useEventListener('mouseleave'() ={
        onLeave?.()
        setFlag(false)
      }, target)

      return flag
    };

    export default useHover;

效果:

案例 3: 有關時間的 Hooks

在這裏主要介紹有關時間的三個 hooks, 分別是:useTimeoutuseIntervaluseCountDown

useTimeout

useTimeout:一段時間內,執行一次

傳遞參數只要函數和延遲時間即可,需要注意的是卸載的時候將定時器清除下就 OK 了

詳細代碼:

    import { useEffect } from 'react';
    import useLatest from '../useLatest';


    const useTimeout = (fn:() => void, delay?: number)void ={

      const fnRef = useLatest(fn)

      useEffect(() ={
        if(!delay || delay < 0) return;

        const timer = setTimeout(() ={
          fnRef.current();
        }, delay)

        return () ={
          clearTimeout(timer)
        }
      }[delay])

    };

    export default useTimeout;

效果展示:

useInterval

useInterval: 每過一段時間內一直執行

大體上與useTimeout一樣,多了一個是否要首次渲染的參數immediate

詳細代碼:

    import { useEffect } from 'react';
    import useLatest from '../useLatest';


    const useInterval = (fn:() => void, delay?: number, immediate?:boolean)void ={

      const fnRef = useLatest(fn)

      useEffect(() ={
        if(!delay || delay < 0) return;
        if(immediate) fnRef.current();

        const timer = setInterval(() ={
          fnRef.current();
        }, delay)

        return () ={
          clearInterval(timer)
        }
      }[delay])

    };

    export default useInterval;

效果展示:

useCountDown

useCountDown:簡單控制倒計時的鉤子

跟之前一樣我們先來想想這個鉤子需要什麼:

詳細代碼

    import { useState, useEffect, useMemo } from 'react';
    import useLatest from '../useLatest';
    import dayjs from 'dayjs';

    type DTime = Date | number | string | undefined;

    interface Options {
      targetDate?: DTime;
      interval?: number;
      onEnd?: () => void;
    }

    interface FormattedRes {
      days: number;
      hours: number;
      minutes: number;
      seconds: number;
      milliseconds: number;
    }

    const calcTime = (time: DTime) ={
      if(!time) return 0

      const res = dayjs(time).valueOf() - new Date().getTime(); //計算差值

      if(res < 0) return 0

      return res
    }

    const parseMs = (milliseconds: number)FormattedRes ={
      return {
        days: Math.floor(milliseconds / 86400000),
        hours: Math.floor(milliseconds / 3600000) % 24,
        minutes: Math.floor(milliseconds / 60000) % 60,
        seconds: Math.floor(milliseconds / 1000) % 60,
        milliseconds: Math.floor(milliseconds) % 1000,
      };
    };

    const useCountDown = (options?: Options) ={

      const { targetDate, interval = 1000, onEnd } = options || {};

      const [time, setTime] = useState(() =>  calcTime(targetDate));
      const onEndRef = useLatest(onEnd);

      useEffect(() ={

        if(!targetDate) return setTime(0)

        setTime(calcTime(targetDate))

        const timer = setInterval(() ={
          const target = calcTime(targetDate);

          setTime(target);
          if (target === 0) {
            clearInterval(timer);
            onEndRef.current?.();
          }
        }, interval);
        return () => clearInterval(timer);
      },[targetDate, interval])

      const formattedRes = useMemo(() ={
        return parseMs(time);
      }[time]);

      return [time, formattedRes] as const
    };

    export default useCountDown;

驗證

    import React, { useState } from 'react';
    import { useCountDown } from '@/components'
    import { Button, Toast } from 'antd-mobile';

    const Index:React.FC<any> = (props)={

      const [_, formattedRes] = useCountDown({
        targetDate: '2022-12-31 24:00:00',
      });

      const { days, hours, minutes, seconds, milliseconds } = formattedRes;

      const [count, setCount] = useState<number>();

      const [countdown] = useCountDown({
        targetDate: count,
        onEnd: () ={
          Toast.show('結束')
        },
      });

      return (
        <div style={{padding: 20}}>
          <div> 距離 2022-12-31 24:00:00 還有 {days} 天 {hours} 時 {minutes} 分 {seconds} 秒 {milliseconds} 毫秒</div>
          <div>
            <p style={{marginTop: 12}}>動態變化:</p>
            <Button color='primary' disabled={countdown !== 0} onClick={() => setCount(Date.now() + 3000)}>
              {countdown === 0 ? '開始' : `還有 ${Math.round(countdown / 1000)}s`}
            </Button>
            <Button style={{marginLeft: 8}} onClick={() => setCount(undefined)}>停止</Button>
          </div>
        </div>
      );
    }

    export default Index;

效果展示:

End

參考

總結

簡單的做下總結:

盤點

本文一共講解了 12 個自定義 hooks,分別是:usePowuseLatestuseCreationuseMountuseUnmountuseUpdateuseReactiveuseEventListeneruseHoveruseTimeoutuseIntervaluseCountDown

這裏的素材來源爲 ahooks,但與 ahooks 的不是完全一樣,有興趣的小夥伴可以結合ahooks源碼對比來看,自己動手敲敲,加深理解

相信在這篇文章的幫助下,各位小夥伴應該跟我一樣對Hooks有了更深的理解,當然,實踐是檢驗真理的唯一標準,多多敲代碼纔是王道~

另外,覺得這篇文章能夠幫助到你的話,請點贊 + 收藏一下吧,順便關注下專欄,之後會輸出有關React的好文,一起上車學習吧~

react其他好文:「React 深入」這就是 HOC,這次我終於悟了!!![6]

關於本文

來自:小杜杜

https://juejin.cn/post/7101486767336849421

參考資料

[1]

https://juejin.cn/post/7088304364078497800: https://juejin.cn/post/7088304364078497800

[2]

https://ahooks.js.org/zh-CN/hooks/use-creation: https://link.juejin.cn?target=https%3A%2F%2Fahooks.js.org%2Fzh-CN%2Fhooks%2Fuse-creation

[3]

https://juejin.cn/post/7068935394191998990#heading-36: https://juejin.cn/post/7068935394191998990#heading-36

[4]

https://juejin.cn/post/7088304364078497800#heading-82: https://juejin.cn/post/7088304364078497800#heading-82

[5]

https://ahooks.js.org/zh-CN/hooks/use-request/index: https://link.juejin.cn?target=https%3A%2F%2Fahooks.js.org%2Fzh-CN%2Fhooks%2Fuse-request%2Findex

[6]

https://juejin.cn/post/7103345085089054727: https://juejin.cn/post/7103345085089054727

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Bdv9Kx9zFqm76OVXjPKq9Q