搞懂这12个Hooks,保证让你玩转React

发表于 2年以前  | 总阅读数:715 次

大家好 ,React Hooks的发布已经有三年多了,它给函数式组件带来了生命周期,现如今,Hooks逐渐取代class组件,相信各位 React 开发的小伙伴已经深有体会,然而你真的完全掌握hooks了吗?知道如何去做一个好的自定义hooks吗?

我们知道React HooksuseState设置变量,useEffect副作用,useRef来获取元素的所有属性,还有useMemouseCallback来做性能优化,当然还有一个自定义Hooks,来创造出你所想要的Hooks

接下来我们来看看以下几个问题,问问自己,是否全都知道:

  • Hooks的由来是什么?
  • useRef的高级用法是什么?
  • useMemouseCallback 是怎么做优化的?
  • 一个好的自定义Hooks该如何设计?
  • 如何做一个不需要useState就可以直接修改属性并刷新视图的自定义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 给所传入的数字平方, 用切换状态的按钮表示函数内部的状态,我们来看看此时的效果:

img2.gif

我们发现了一个问题,为什么点击切换按钮也会触发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;

img3.gif

发现此时就已经解决了这个问题,所以要非常注意一点,一个好用的自定义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的好处:

  • 可以减少不必要的循环和不必要的渲染
  • 可以减少子组件的渲染次数
  • 通过特地的依赖进行更新,可以避免很多不必要的开销,但要注意,有时候在配合 useState拿不到最新的值,这种情况可以考虑使用 useRef解决

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;

img2.gif

我们可以看到,当点击切换按钮的时候,没有经过 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])

  • useMemo的值不一定是最新的值,但useCreation可以保证拿到的值一定是最新的值
  • 对于复杂常量的创建,useRef容易出现潜在的的性能隐患,但useCreation可以避免

这里的性能隐患是指:

   // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了
   const a = useRef(new Subject()) 

   // 通过 factory 函数,可以避免性能隐患
   const b = useCreation(() => new Subject(), []) 

接下来我们来看看如何封装一个useCreation,首先我们要明白以下三点:

  • 第一点:先确定参数,useCreation 的参数与useMemo的一致,第一个参数是函数,第二个参数参数是可变的数组
  • 第二点:我们的值要保存在 useRef中,这样可以将值缓存,从而减少无关的刷新
  • 第三点:更新值的判断,怎么通过第二个参数来判断是否更新 useRef里的值。

明白了一上三点我们就可以自己实现一个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;

useCreation.gif

我们可以看到,当我们做无关的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;

效果如下:

img6.gif

案例

案例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,提出以下疑问,感兴趣的,可以先自行思考一下:

  • 这个钩子的出入参该怎么设定?
  • 如何将数据制作成响应式(毕竟普通的操作无法刷新视图)?
  • 如何使用TS去写,完善其类型?
  • 如何更好的去优化?

分析

以上四个小问题,最关键的就是第二个,我们如何将数据弄成响应式,想要弄成响应式,就必须监听到值的变化,在做出更改,也就是说,我们对这个数进行操作的时候,要进行相应的拦截,这时就需要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) 即可,其中:

  • 设置这块,需要改变是图,也就是说需要,使用useUpdate来强制刷新
  • 获取这块,需要判断其是否为对象,是的话继续递归,不是的话返回就行

验证

接下来我们来验证一下我们写的 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;

效果如下:

useuse.gif

案例2: useEventListener

缘由:我们监听各种事件的时候需要做监听,如:监听点击事件、键盘事件、滚动事件等,我们将其统一封装起来,方便后续调用

说白了就是在addEventListener的基础上进行封装,我们先来想想在此基础上需要什么?

首先,useEventListener的入参可分为三个

  • 第一个event是事件(如:click、keydown)
  • 第二个回调函数(所以不需要出参)
  • 第三个就是目标(是某个节点还是全局)

在这里需要注意一点就是在销毁的时候需要移除对应的监听事件

代码

    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,防止拿到的值不是最新值

这里简单说一下我的思路(又不对的地方或者有更好的建议欢迎评论区指出):

  • 首先需要hasInitRef来存储是否是第一次进入,通过它来判断初始化存储
  • 然后考虑有几个参数需要存储,从上述代码上来看,可变的变量有两个,一个是event,另一个是target,其次,我们还需要存储对应的卸载后的函数,所以存储的变量应该有3个
  • 接下来考虑一下什么情况下触发更新,也就是可变的两个参数:eventtarget
  • 最后在卸载的时候可以考虑使用useUnmount,并执行存储对应的卸载后的函数 和把hasInitRef还原

详细代码

    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;
  • 在这里只用useEffect是因为,在更新和初始化的情况下都需要使用
  • 必须要防止没有 addEventListener这个属性的情况,监听的目标有可能没有加载出来

验证

验证一下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;

效果:

useEvent.gif

我们可以利用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;

效果:

useHover.gif

案例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;

效果展示:

img3.gif

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:简单控制倒计时的钩子

跟之前一样我们先来想想这个钩子需要什么:

  • 我们要做倒计时的钩子首先需要一个目标时间(targetDate),控制时间变化的秒数(interval默认为1s),然后就是倒计时完成后所触发的函数(onEnd)
  • 返参就更加一目了然了,返回的是两个时间差的数值(time),再详细点可以换算成对应的天、时、分等(formattedRes)

详细代码

    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;

效果展示:

img5.gif

End

参考

  • ahooks[5]

总结

简单的做下总结:

  • 一个优秀的hooks一定会具备useMemouseCallback等api优化
  • 制作自定义hooks遇到传递过来的值,优先考虑使用useRef,再考虑用useState,可以直接使用useLatest,防止拿到的值不是最新值
  • 在封装的时候,应该将存放的值放入 useRef中,通过一个状态去设置他的初始化,在判断什么情况下来更新所对应的值,明确入参与出参的具体意义,如useCreationuseEventListener

盘点

本文一共讲解了12个自定义hooks,分别是:usePowuseLatestuseCreationuseMountuseUnmountuseUpdateuseReactiveuseEventListeneruseHoveruseTimeoutuseIntervaluseCountDown

这里的素材来源为ahooks,但与ahooks的不是完全一样,有兴趣的小伙伴可以结合ahooks源码对比来看,自己动手敲敲,加深理解

相信在这篇文章的帮助下,各位小伙伴应该跟我一样对Hooks有了更深的理解,当然,实践是检验真理的唯一标准,多多敲代码才是王道~

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

参考资料

[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

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/oY6vlNjeMA6xja_iUEul8g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录