<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          搞懂這12個(gè)Hooks,保證讓你玩轉(zhuǎn)React

          共 54020字,需瀏覽 109分鐘

           ·

          2022-07-04 12:59

          大家好,我是小杜杜,React Hooks的發(fā)布已經(jīng)有三年多了,它給函數(shù)式組件帶來(lái)了生命周期,現(xiàn)如今,Hooks逐漸取代class組件,相信各位 React 開(kāi)發(fā)的小伙伴已經(jīng)深有體會(huì),然而你真的完全掌握hooks了嗎?知道如何去做一個(gè)好的自定義hooks嗎?

          我們知道React HooksuseState設(shè)置變量,useEffect副作用,useRef來(lái)獲取元素的所有屬性,還有useMemouseCallback來(lái)做性能優(yōu)化,當(dāng)然還有一個(gè)自定義Hooks,來(lái)創(chuàng)造出你所想要的Hooks

          接下來(lái)我們來(lái)看看以下幾個(gè)問(wèn)題,問(wèn)問(wèn)自己,是否全都知道:

          • Hooks的由來(lái)是什么?
          • useRef的高級(jí)用法是什么?
          • useMemouseCallback 是怎么做優(yōu)化的?
          • 一個(gè)好的自定義Hooks該如何設(shè)計(jì)?
          • 如何做一個(gè)不需要useState就可以直接修改屬性并刷新視圖的自定義Hooks?
          • 如何做一個(gè)可以監(jiān)聽(tīng)任何事件的自定義Hooks?

          如果你對(duì)以上問(wèn)題有疑問(wèn),有好奇,那么這篇文章應(yīng)該能夠幫助到你~

          本文將會(huì)以介紹自定義Hooks來(lái)解答上述問(wèn)題,并結(jié)合 TSahooks中的鉤子,以案列的形式去演示,本文過(guò)長(zhǎng),建議:點(diǎn)贊 + 收藏 哦~

          注:這里講解的自定義鉤子可能會(huì)和 ahooks上的略有不同,不會(huì)考慮過(guò)多的情況,如果用于項(xiàng)目,建議直接使用ahooks上的鉤子~

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

          先附上一張今天的知識(shí)圖,還請(qǐng)各位小伙伴多多支持:

          深入 Hooks.png

          自定義Hooks是什么?

          react-hooksReact16.8以后新增的鉤子API,目的是增加代碼的可復(fù)用性、邏輯性,最主要的是解決了函數(shù)式組件無(wú)狀態(tài)的問(wèn)題,這樣既保留了函數(shù)式的簡(jiǎn)單,又解決了沒(méi)有數(shù)據(jù)管理狀態(tài)的缺陷

          那么什么是自定義hooks呢?

          自定義hooks是在react-hooks基礎(chǔ)上的一個(gè)擴(kuò)展,可以根據(jù)業(yè)務(wù)、需求去制定相應(yīng)的hooks,將常用的邏輯進(jìn)行封裝,從而具備復(fù)用性

          如何設(shè)計(jì)一個(gè)自定義Hooks

          hooks本質(zhì)上是一個(gè)函數(shù),而這個(gè)函數(shù)主要就是邏輯復(fù)用,我們首先要知道一件事,hooks的驅(qū)動(dòng)條件是什么?

          其實(shí)就是props的修改,useStateuseReducer的使用是無(wú)狀態(tài)組件更新的條件,從而驅(qū)動(dòng)自定義hooks

          通用模式

          自定義hooks的名稱是以use開(kāi)頭,我們?cè)O(shè)計(jì)為:

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

          簡(jiǎn)單的小例子:usePow

          我們先寫一個(gè)簡(jiǎn)單的小例子來(lái)了解下自定義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([123])
            
            return (
              <div>
                <div>數(shù)字:{JSON.stringify(data)}</div>
                <Button color='primary' onClick={() => {setFlag(v => !v)}}>切換</
          Button>
                 <div>切換狀態(tài):{JSON.stringify(flag)}</div>
              </
          div>
            );
          }

          export default Index;

          我們簡(jiǎn)單的寫了個(gè) usePow,我們通過(guò) usePow 給所傳入的數(shù)字平方, 用切換狀態(tài)的按鈕表示函數(shù)內(nèi)部的狀態(tài),我們來(lái)看看此時(shí)的效果:

          img2.gif

          我們發(fā)現(xiàn)了一個(gè)問(wèn)題,為什么點(diǎn)擊切換按鈕也會(huì)觸發(fā)console.log(1)呢?

          這樣明顯增加了性能開(kāi)銷,我們的理想狀態(tài)肯定不希望做無(wú)關(guān)的渲染,所以我們做自定義 hooks的時(shí)候一定要注意,需要減少性能開(kāi)銷,我們?yōu)榻M件加入 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;
          img3.gif

          發(fā)現(xiàn)此時(shí)就已經(jīng)解決了這個(gè)問(wèn)題,所以要非常注意一點(diǎn),一個(gè)好用的自定義hooks,一定要配合useMemouseCallback等 Api 一起使用。

          玩轉(zhuǎn)React Hooks

          在上述中我們講了用 useMemo來(lái)處理無(wú)關(guān)的渲染,接下來(lái)我們一起來(lái)看看React Hooks的這些鉤子的妙用(這里建議先熟知、并使用對(duì)應(yīng)的React Hooks,才能造出好的鉤子)

          useMemo

          當(dāng)一個(gè)父組件中調(diào)用了一個(gè)子組件的時(shí)候,父組件的 state 發(fā)生變化,會(huì)導(dǎo)致父組件更新,而子組件雖然沒(méi)有發(fā)生改變,但也會(huì)進(jìn)行更新。

          簡(jiǎn)單的理解下,當(dāng)一個(gè)頁(yè)面內(nèi)容非常復(fù)雜,模塊非常多的時(shí)候,函數(shù)式組件會(huì)從頭更新到尾,只要一處改變,所有的模塊都會(huì)進(jìn)行刷新,這種情況顯然是沒(méi)有必要的。

          我們理想的狀態(tài)是各個(gè)模塊只進(jìn)行自己的更新,不要相互去影響,那么此時(shí)用useMemo是最佳的解決方案。

          這里要尤其注意一點(diǎn),只要父組件的狀態(tài)更新,無(wú)論有沒(méi)有對(duì)自組件進(jìn)行操作,子組件都會(huì)進(jìn)行更新useMemo就是為了防止這點(diǎn)而出現(xiàn)的

          在講 useMemo 之前,我們先說(shuō)說(shuō)memo,memo的作用是結(jié)合了pureComponent純組件和 componentShouldUpdate功能,會(huì)對(duì)傳入的props進(jìn)行一次對(duì)比,然后根據(jù)第二個(gè)函數(shù)返回值來(lái)進(jìn)一步判斷哪些props需要更新。(具體使用會(huì)在下文講到~)

          useMemomemo的理念上差不多,都是判斷是否滿足當(dāng)前的限定條件來(lái)決定是否執(zhí)行callback函數(shù),而useMemo的第二個(gè)參數(shù)是一個(gè)數(shù)組,通過(guò)這個(gè)數(shù)組來(lái)判定是否更新回掉函數(shù)

          這種方式可以運(yùn)用在元素、組件、上下文中,尤其是利用在數(shù)組上,先看一個(gè)例子:

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

          從上面我們看出 useMemo只有在list發(fā)生變化的時(shí)候才會(huì)進(jìn)行渲染,從而減少了不必要的開(kāi)銷

          總結(jié)一下useMemo的好處:

          • 可以減少不必要的循環(huán)和不必要的渲染
          • 可以減少子組件的渲染次數(shù)
          • 通過(guò)特地的依賴進(jìn)行更新,可以避免很多不必要的開(kāi)銷,但要注意,有時(shí)候在配合 useState拿不到最新的值,這種情況可以考慮使用 useRef解決

          useCallback

          useCallbackuseMemo極其類似,可以說(shuō)是一模一樣,唯一不同的是useMemo返回的是函數(shù)運(yùn)行的結(jié)果,而useCallback返回的是函數(shù)

          注意:這個(gè)函數(shù)是父組件傳遞子組件的一個(gè)函數(shù),防止做無(wú)關(guān)的刷新,其次,這個(gè)組件必須配合memo,否則不但不會(huì)提升性能,還有可能降低性能

                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="普通點(diǎn)擊" onClick={() => setCount(count + 1) }/>
                        <TestButton title="useCallback點(diǎn)擊" 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點(diǎn)擊' ? {
                  marginLeft: 20
                  } : undefined}>{props.title}</Button>
                })

                export default MockMemo;
          img2.gif

          我們可以看到,當(dāng)點(diǎn)擊切換按鈕的時(shí)候,沒(méi)有經(jīng)過(guò) useCallback封裝的函數(shù)會(huì)再次刷新,而進(jìn)過(guò)過(guò) useCallback包裹的函數(shù)不會(huì)被再次刷新

          useRef

          useRef 可以獲取當(dāng)前元素的所有屬性,并且返回一個(gè)可變的ref對(duì)象,并且這個(gè)對(duì)象只有current屬性,可設(shè)置initialValue

          通過(guò)useRef獲取對(duì)應(yīng)的屬性值

          我們先看個(gè)案例:

          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; //可視區(qū)域高度
                let scrollTop  = scrollRef?.current.scrollTop;  //滾動(dòng)條滾動(dòng)高度
                let scrollHeight = scrollRef?.current.scrollHeight; //滾動(dòng)內(nèi)容高度
                setClientHeight(clientHeight)
                setScrollTop(scrollTop)
                setScrollHeight(scrollHeight)
              }
            }

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

          export default Index;

          從上述可知,我們可以通過(guò)useRef來(lái)獲取對(duì)應(yīng)元素的相關(guān)屬性,以此來(lái)做一些操作

          效果:

          緩存數(shù)據(jù)

          除了獲取對(duì)應(yīng)的屬性值外,useRef還有一點(diǎn)比較重要的特性,那就是 緩存數(shù)據(jù)

          上述講到我們封裝一個(gè)合格的自定義hooks的時(shí)候需要結(jié)合useMemouseCallback等Api,但我們控制變量的值用useState 有可能會(huì)導(dǎo)致拿到的是舊值,并且如果他們更新會(huì)帶來(lái)整個(gè)組件重新執(zhí)行,這種情況下,我們使用useRef將會(huì)是一個(gè)非常不錯(cuò)的選擇

          react-redux的源碼中,在hooks推出后,react-redux用大量的useMemo重做了Provide等核心模塊,其中就是運(yùn)用useRef來(lái)緩存數(shù)據(jù),并且所運(yùn)用的 useRef() 沒(méi)有一個(gè)是綁定在dom元素上的,都是做數(shù)據(jù)緩存用的

          可以簡(jiǎn)單的來(lái)看一下:

              // 緩存數(shù)據(jù)
              /* react-redux 用userRef 來(lái)緩存 merge之后的 props */ 
              const lastChildProps = useRef() 
              
              // lastWrapperProps 用 useRef 來(lái)存放組件真正的 props信息 
              const lastWrapperProps = useRef(wrapperProps) 
              
              //是否儲(chǔ)存props是否處于正在更新?tīng)顟B(tài) 
              const renderIsScheduled = useRef(false)

              //更新數(shù)據(jù)
              function captureWrapperProps( 
                  lastWrapperProps, 
                  lastChildProps, 
                  renderIsScheduled, 
                  wrapperProps, 
                  actualChildProps, 
                  childPropsFromStoreUpdate, 
                  notifyNestedSubs 
              

                  lastWrapperProps.current = wrapperProps 
                  lastChildProps.current = actualChildProps 
                  renderIsScheduled.current = false 
             }

          我們看到 react-redux 用重新賦值的方法,改變了緩存的數(shù)據(jù)源,減少了不必要的更新,如過(guò)采取useState勢(shì)必會(huì)重新渲染

          useLatest

          經(jīng)過(guò)上面的講解我們知道useRef 可以拿到最新值,我們可以進(jìn)行簡(jiǎn)單的封裝,這樣做的好處是:可以隨時(shí)確保獲取的是最新值,并且也可以解決閉包問(wèn)題

             import { useRef } from 'react';

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

               return ref
             };

             export default useLatest;

          結(jié)合useMemo和useRef封裝useCreation

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

          • useMemo的值不一定是最新的值,但useCreation可以保證拿到的值一定是最新的值
          • 對(duì)于復(fù)雜常量的創(chuàng)建,useRef容易出現(xiàn)潛在的的性能隱患,但useCreation可以避免

          這里的性能隱患是指:

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

          接下來(lái)我們來(lái)看看如何封裝一個(gè)useCreation,首先我們要明白以下三點(diǎn):

          • 第一點(diǎn):先確定參數(shù),useCreation 的參數(shù)與useMemo的一致,第一個(gè)參數(shù)是函數(shù),第二個(gè)參數(shù)參數(shù)是可變的數(shù)組
          • 第二點(diǎn):我們的值要保存在 useRef中,這樣可以將值緩存,從而減少無(wú)關(guān)的刷新
          • 第三點(diǎn):更新值的判斷,怎么通過(guò)第二個(gè)參數(shù)來(lái)判斷是否更新 useRef里的值。

          明白了一上三點(diǎn)我們就可以自己實(shí)現(xiàn)一個(gè)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++) {
              // 判斷兩個(gè)值是否是同一個(gè)值
              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判斷是否更新值通過(guò)initializeddepsAreSame來(lái)判斷,其中depsAreSame通過(guò)存儲(chǔ)在 useRef下的deps(舊值) 和 新傳入的 deps(新值)來(lái)做對(duì)比,判斷兩數(shù)組的數(shù)據(jù)是否一致,來(lái)確定是否更新

          驗(yàn)證 useCreation

          接下來(lái)我們寫個(gè)小例子,來(lái)驗(yàn)證下 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>正常的函數(shù):{getNowData()}</div>
          <div>useCreation包裹后的:{nowData}</div>
          <Button color='primary' onClick={() => {setFlag(v => !v)}}> 渲染</Button>
          </div>
          )
          }

          export default Index;
          useCreation.gif

          我們可以看到,當(dāng)我們做無(wú)關(guān)的state改變的時(shí)候,正常的函數(shù)也會(huì)刷新,但useCreation沒(méi)有刷新,從而增強(qiáng)了渲染的性能~

          useEffect

          useEffect相信各位小伙伴已經(jīng)用的熟的不能再熟了,我們可以使用useEffect來(lái)模擬下classcomponentDidMountcomponentWillUnmount的功能。

          useMount

          這個(gè)鉤子不必多說(shuō),只是簡(jiǎn)化了使用useEffect的第二個(gè)參數(shù):

              import { useEffect } from 'react';

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

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

              export default useMount;

          useUnmount

          這個(gè)需要注意一個(gè)點(diǎn),就是使用useRef來(lái)確保所傳入的函數(shù)為最新的狀態(tài),所以可以結(jié)合上述講的useLatest結(jié)合使用

              import { useEffect, useRef } from 'react';

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

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

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

              export default useUnmount;

          結(jié)合useMountuseUnmount做個(gè)小例子

              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:強(qiáng)制更新

          有的時(shí)候我們需要組件強(qiáng)制更新,這個(gè)時(shí)候就可以使用這個(gè)鉤子:

              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>時(shí)間:{Date.now()}</div>
                    <Button color='primary' onClick={update}>更新時(shí)間</
          Button>
                  </div>
                );
              }

              export default Index;

          效果如下:

          img6.gif

          案例

          案例1: useReactive

          useReactiv: 一種具備響應(yīng)式useState

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

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

          通過(guò)setCount來(lái)設(shè)置,count來(lái)獲取,使用這種方式才能夠渲染視圖

          來(lái)看看正常的操作,像這樣 let count = 0; count =7 此時(shí)count的值就是7,也就是說(shuō)數(shù)據(jù)是響應(yīng)式的

          那么我們可不可以將 useState也寫成響應(yīng)式的呢?我可以自由設(shè)置count的值,并且可以隨時(shí)獲取到count的最新值,而不是通過(guò)setCount來(lái)設(shè)置。

          我們來(lái)想想怎么去實(shí)現(xiàn)一個(gè)具備 響應(yīng)式 特點(diǎn)的 useState 也就是 useRective,提出以下疑問(wèn),感興趣的,可以先自行思考一下:

          • 這個(gè)鉤子的出入?yún)⒃撛趺丛O(shè)定?
          • 如何將數(shù)據(jù)制作成響應(yīng)式(畢竟普通的操作無(wú)法刷新視圖)?
          • 如何使用TS去寫,完善其類型?
          • 如何更好的去優(yōu)化?

          分析

          以上四個(gè)小問(wèn)題,最關(guān)鍵的就是第二個(gè),我們?nèi)绾螌?shù)據(jù)弄成響應(yīng)式,想要弄成響應(yīng)式,就必須監(jiān)聽(tīng)到值的變化,在做出更改,也就是說(shuō),我們對(duì)這個(gè)數(shù)進(jìn)行操作的時(shí)候,要進(jìn)行相應(yīng)的攔截,這時(shí)就需要ES6的一個(gè)知識(shí)點(diǎn):Proxy

          在這里會(huì)用到 ProxyReflect的點(diǎn),感興趣的可以看看我的這篇文章:??花一個(gè)小時(shí),迅速了解ES6\~ES12的全部特性[3]

          Proxy:接受的參數(shù)是對(duì)象,所以第一個(gè)問(wèn)題也解決了,入?yún)⒕蜑閷?duì)象。那么如何去刷新視圖呢?這里就使用上述的useUpdate來(lái)強(qiáng)制刷新,使數(shù)據(jù)更改。

          至于優(yōu)化這一塊,使用上文說(shuō)的useCreation就好,再配合useRef來(lái)放initialState即可

          代碼

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

          const observer = <T extends Record<stringany>>(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<stringany>>(initialState: T):T => {
            const ref = useRef<T>(initialState);
            const update = useUpdate();

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

            return state
          };

          export default useReactive;

          這里先說(shuō)下TS,因?yàn)槲覀儾恢罆?huì)傳遞什么類型的initialState所以在這需要使用泛型,我們接受的參數(shù)是對(duì)象,可就是 key-value 的形式,其中 key 為 string,value 可以是 任意類型,所以我們使用 Record<string, any>

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

          再來(lái)說(shuō)下攔截這塊,我們只需要攔截設(shè)置(set)獲取(get) 即可,其中:

          • 設(shè)置這塊,需要改變是圖,也就是說(shuō)需要,使用useUpdate來(lái)強(qiáng)制刷新
          • 獲取這塊,需要判斷其是否為對(duì)象,是的話繼續(xù)遞歸,不是的話返回就行

          驗(yàn)證

          接下來(lái)我們來(lái)驗(yàn)證一下我們寫的 useReactive,我們將以 字符串、數(shù)字、布爾、數(shù)組、函數(shù)、計(jì)算屬性幾個(gè)方面去驗(yàn)證一下:

              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}}> 對(duì)數(shù)字進(jìn)行操作:{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 } >設(shè)置為7</Button>
                     </
          div>
                     <div style={{marginTop: 8}}> 對(duì)字符串進(jìn)行操作:{state.name}</div>
                     <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
                       <Button color='primary' onClick={() => state.name = '小杜杜' } >設(shè)置為小杜杜</
          Button>
                       <Button color='primary' style={{marginLeft: 8}} onClick={() => state.name = 'Domesy'} >設(shè)置為Domesy</Button>
                     </
          div>
                     <div style={{marginTop: 8}}> 對(duì)布爾值進(jìn)行操作:{JSON.stringify(state.flag)}</div>
                     <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
                       <Button color='primary' onClick={() => state.flag = !state.flag } >切換狀態(tài)</
          Button>
                     </div>
                     <div style={{marginTop: 8}}> 對(duì)數(shù)組進(jìn)行操作:{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}}>計(jì)算屬性:</
          div>
                     <div style={{marginTop: 8}}>數(shù)量:{ state.bugsCount } 個(gè)</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>
                         ))
                       }
                     </u
          l>
                  </div>
                );
              }

              export default Index;

          效果如下:

          useuse.gif

          案例2: useEventListener

          緣由:我們監(jiān)聽(tīng)各種事件的時(shí)候需要做監(jiān)聽(tīng),如:監(jiān)聽(tīng)點(diǎn)擊事件、鍵盤事件、滾動(dòng)事件等,我們將其統(tǒng)一封裝起來(lái),方便后續(xù)調(diào)用

          說(shuō)白了就是在addEventListener的基礎(chǔ)上進(jìn)行封裝,我們先來(lái)想想在此基礎(chǔ)上需要什么?

          首先,useEventListener的入?yún)⒖煞譃槿齻€(gè)

          • 第一個(gè)event是事件(如:click、keydown)
          • 第二個(gè)回調(diào)函數(shù)(所以不需要出參)
          • 第三個(gè)就是目標(biāo)(是某個(gè)節(jié)點(diǎn)還是全局)

          在這里需要注意一點(diǎn)就是在銷毀的時(shí)候需要移除對(duì)應(yīng)的監(jiān)聽(tīng)事件

          代碼

              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默認(rèn)設(shè)置成了window,至于為什么要這么寫:'current' in target是因?yàn)槲覀冇?code style="font-size: 14px;word-wrap: break-word;border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: #9b6e23;background-color: #fff5e3;padding: 3px;margin: 3px;">useRef拿到的值都是 ref.current

          優(yōu)化

          接下來(lái)我們一起來(lái)看看如何優(yōu)化這個(gè)組件,這里的優(yōu)化與 useCreation 類似,但又有不同,原因是這里的需要判斷的要比useCreation復(fù)雜一點(diǎn)。

          再次強(qiáng)調(diào)一下,傳遞過(guò)來(lái)的值,優(yōu)先考慮使用useRef,再考慮用useState,可以直接使用useLatest,防止拿到的值不是最新值

          這里簡(jiǎn)單說(shuō)一下我的思路(又不對(duì)的地方或者有更好的建議歡迎評(píng)論區(qū)指出):

          • 首先需要hasInitRef來(lái)存儲(chǔ)是否是第一次進(jìn)入,通過(guò)它來(lái)判斷初始化存儲(chǔ)
          • 然后考慮有幾個(gè)參數(shù)需要存儲(chǔ),從上述代碼上來(lái)看,可變的變量有兩個(gè),一個(gè)是event,另一個(gè)是target,其次,我們還需要存儲(chǔ)對(duì)應(yīng)的卸載后的函數(shù),所以存儲(chǔ)的變量應(yīng)該有3個(gè)
          • 接下來(lái)考慮一下什么情況下觸發(fā)更新,也就是可變的兩個(gè)參數(shù):eventtarget
          • 最后在卸載的時(shí)候可以考慮使用useUnmount,并執(zhí)行存儲(chǔ)對(duì)應(yīng)的卸載后的函數(shù) 和把hasInitRef還原

          詳細(xì)代碼

              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); // 一開(kāi)始設(shè)置初始化
                const elementRef = useRef<(Element | null)[]>([]);// 存儲(chǔ)具體的值
                const depsRef = useRef<DependencyList>([]); // 存儲(chǔ)傳遞的deps
                const unmountRef = useRef<any>(); // 存儲(chǔ)對(duì)應(yīng)的effect

                // 初始化 組件的初始化和更新都會(huì)執(zhí)行
                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
                  }
                  // 校驗(yàn)變值: 目標(biāo)的值不同, 依賴值改變
                  if(elementRef.current !== targetElement || !depsAreSame(deps, depsRef.current)){
                    //先執(zhí)行對(duì)應(yīng)的函數(shù)
                    unmountRef.current?.();
                    //重新進(jìn)行賦值
                    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;

                  //  防止沒(méi)有 addEventListener 這個(gè)屬性
                  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;
          • 在這里只用useEffect是因?yàn)椋诟潞统跏蓟那闆r下都需要使用
          • 必須要防止沒(méi)有 addEventListener這個(gè)屬性的情況,監(jiān)聽(tīng)的目標(biāo)有可能沒(méi)有加載出來(lái)

          驗(yàn)證

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

              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>數(shù)字:{count}</
          div>
                        <button ref={ref} >加1</button>
                        <div>監(jiān)聽(tīng)鍵盤事件:{key}</
          div>
                      </div>
                    }

                  </
          div>
                );
              }

              export default Index;

          效果:

          useEvent.gif

          我們可以利用useEventListener這個(gè)鉤子去封裝其他鉤子,如 鼠標(biāo)懸停,長(zhǎng)按事件,鼠標(biāo)位置等,在這里在舉一個(gè)鼠標(biāo)懸停的小例子

          小例子 useHover

          useHover:監(jiān)聽(tīng) DOM 元素是否有鼠標(biāo)懸停

          這個(gè)就很簡(jiǎn)單了,只需要通過(guò) useEventListener來(lái)監(jiān)聽(tīng)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;

          效果:

          useHover.gif

          案例3: 有關(guān)時(shí)間的Hooks

          在這里主要介紹有關(guān)時(shí)間的三個(gè)hooks,分別是:useTimeoutuseIntervaluseCountDown

          useTimeout

          useTimeout:一段時(shí)間內(nèi),執(zhí)行一次

          傳遞參數(shù)只要函數(shù)和延遲時(shí)間即可,需要注意的是卸載的時(shí)候?qū)⒍〞r(shí)器清除下就OK了

          詳細(xì)代碼:

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


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

                const fnRef = useLatest(fn)

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

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

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

              };

              export default useTimeout;

          效果展示:

          img3.gif

          useInterval

          useInterval: 每過(guò)一段時(shí)間內(nèi)一直執(zhí)行

          大體上與useTimeout一樣,多了一個(gè)是否要首次渲染的參數(shù)immediate

          詳細(xì)代碼:

              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 < 0return;
                  if(immediate) fnRef.current();

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

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

              };

              export default useInterval;

          效果展示:

          useCountDown

          useCountDown:簡(jiǎn)單控制倒計(jì)時(shí)的鉤子

          跟之前一樣我們先來(lái)想想這個(gè)鉤子需要什么:

          • 我們要做倒計(jì)時(shí)的鉤子首先需要一個(gè)目標(biāo)時(shí)間(targetDate),控制時(shí)間變化的秒數(shù)(interval默認(rèn)為1s),然后就是倒計(jì)時(shí)完成后所觸發(fā)的函數(shù)(onEnd)
          • 返參就更加一目了然了,返回的是兩個(gè)時(shí)間差的數(shù)值(time),再詳細(xì)點(diǎn)可以換算成對(duì)應(yīng)的天、時(shí)、分等(formattedRes)

          詳細(xì)代碼

              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(); //計(jì)算差值

                if(res < 0return 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;

          驗(yàn)證

              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('結(jié)束')
                  },
                });

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

              export default Index;

          效果展示:

          img5.gif

          End

          參考

          • ahooks[5]

          總結(jié)

          簡(jiǎn)單的做下總結(jié):

          • 一個(gè)優(yōu)秀的hooks一定會(huì)具備useMemouseCallback等api優(yōu)化
          • 制作自定義hooks遇到傳遞過(guò)來(lái)的值,優(yōu)先考慮使用useRef,再考慮用useState,可以直接使用useLatest,防止拿到的值不是最新值
          • 在封裝的時(shí)候,應(yīng)該將存放的值放入 useRef中,通過(guò)一個(gè)狀態(tài)去設(shè)置他的初始化,在判斷什么情況下來(lái)更新所對(duì)應(yīng)的值,明確入?yún)⑴c出參的具體意義,如useCreationuseEventListener

          盤點(diǎn)

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

          這里的素材來(lái)源為ahooks,但與ahooks的不是完全一樣,有興趣的小伙伴可以結(jié)合ahooks源碼對(duì)比來(lái)看,自己動(dòng)手敲敲,加深理解

          相信在這篇文章的幫助下,各位小伙伴應(yīng)該跟我一樣對(duì)Hooks有了更深的理解,當(dāng)然,實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn),多多敲代碼才是王道~

          另外,覺(jué)得這篇文章能夠幫助到你的話,請(qǐng)點(diǎn)贊+收藏一下吧,順便關(guān)注下專欄,之后會(huì)輸出有關(guān)React的好文,一起上車學(xué)習(xí)吧~

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

          關(guān)于本文

          來(lái)自:小杜杜

          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

          祝 您:2022 年暴富!萬(wàn)事如意!

          點(diǎn)贊和在看就是最大的支持,比心??

          瀏覽 65
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  男人AV天堂电影 | 亚洲黄在线观看 | 欧美在线无码精品秘 蜜桃 | 日本国产操逼网 | 狠狠爱AV |