React自定义倒计时hooks,你不知道的逻辑复用(干货满满!!!)

3,147 阅读7分钟

前言

步入正题之前,我想先说点其他的.关于项目中的组件化,模块化,相信大家都已经明明白白,也明白为什么要这么做.那么,关于业务逻辑的复用,不知道有多少人明白呢

React hooks

概念

  • React 一直都提倡使用函数组件,但是有时候需要使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有
  • 在React 16.8版本之前,class组件被称作有状态组件,函数组件被称作无状态组件,而16.8以后,随着React引入hooks的概念,使得函数组件有可以有状态了
  • hooks本质上就是一个给函数组件中钩入状态和生命周期特性的函数,React给我们提供了一些hooks,我们也可以自定义hooks使用

hooks的优点

  • 首先,hooks之所以如此火,有一点是因为Class组件大材小用了,例如class中的继承特性,以及创造一个新的实例,这些方法我们压根没有用到过
  • 在组件中复用业务逻辑代码很困难,但是hooks可以帮助我们实现业务逻辑的复用(最重要的一点)
  • hooks + 函数组件开发模式,不存在this指向复杂的问题

业务场景

介绍完hooks,我给大家出来两题自定义hooks,由简到难,想象一下下面的业务场景

简单业务场景1-获取鼠标位置

  • 场景1:图片跟随鼠标的移动进行移动

image.png 业务代码

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
import Img from './logo192.png'
export default function App () {
  // 设置鼠标位置状态
  const [position, setposition] = useState({ x: 0, y: 0 })
  useEffect(() => {
    // 鼠标移动,获取鼠标位置,并把位置给到图片位置
    const fn = (e) => {
      setposition({
        x: e.pageX,
        y: e.pageY
      })
    }
    window.addEventListener('mousemove', fn)
    // 组件销毁,清除鼠标移动事件
    return () => {
      window.removeEventListener('mousemove', fn)
    }
  }, [])
  return (
    <div>
        <img src={Img} style={{ position: 'absolute', left: position.x, top: position.y }} />
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))
  • 场景2:鼠标移动,页面实时展示鼠标位置信息

image.png

业务代码

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
export default function App () {
  const [position, setposition] = useState({ x: 0, y: 0 })
  useEffect(() => {
    // 鼠标移动,获取鼠标位置
    const fn = (e) => {
      setposition({
        x: e.pageX,
        y: e.pageY
      })
    }
    window.addEventListener('mousemove', fn)
    // 组件销毁,清除鼠标移动事件
    return () => {
      window.removeEventListener('mousemove', fn)
    }
  }, [])
  return (
    <div>
        打印鼠标位置{position.x}:{position.y}
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

细心的小伙伴估计很快就发现了,这两个场景的业务代码可以说是一模一样.如果一个项目中要实现这两个功能,重新写一遍77业务代码会使项目变得冗余并且不方便维护,这个时候 ,复用业务逻辑的需求就来了.

分析核心业务逻辑

相同点:

  • 都要设置监听鼠标移动事件获取鼠标位置 不同点:
  • 场景1是给图片设置位置
  • 场景2是获取鼠标位置就行

自定义hooks抽离代码

思路 我们可以把设置监听事件以及获取鼠标位置的代码抽离出来,自定义hooks,来复用逻辑

注意点:

1.自定义hooks名要use开头

2.hooks函数只能在函数组件,类组件以及hooks中使用

3.hooks不能用在if,for,等语句中,也不能写在事件里面

  • 自定义的hooks代码
import { useState, useEffect } from 'react'
// 自定义useMove hooks
export default function useMove () {
  // 设置鼠标位置状态
  const [position, setposition] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const fn = (e) => {
      setposition({
        x: e.pageX,
        y: e.pageY
      })
    }
    // 监听鼠标移动事件
    window.addEventListener('mousemove', fn)
    return () => {
      // 当销毁组件的时候,清理鼠标移动事件
      window.removeEventListener('mousemove', fn)
    }
  }, [])
  // 把位置信息返回出去
  return position
}

接下来我们把原先代码改一下,都导入自定义hooks,用hooks实现功能

场景1使用自定义hooks后的代码

import React from 'react'
import ReactDOM from 'react-dom'
import Img from './logo192.png'
import useMove from './MouseMove'
export default function App () {
  const position = useMove()
  return (
    <div>
        <img src={Img} style={{ position: 'absolute', left: position.x, top: position.y }} />
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

image.png

场景2使用自定义hooks后的代码:

import React from 'react'
import useMove from './MouseMove'
import ReactDOM from 'react-dom'
export default function App () {
  const position = useMove()
  return (
    <div>
        打印鼠标位置{position.x}:{position.y}
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

image.png

上面这个场景,很明显的可以看出复用业务代码的好处,可以明显缩短我们的代码量,是代码更加易维护且优美,如果你还没感受到自定义hooks复用逻辑带来的好处,觉得这个业务太简单了,那么接下来我将提出另外一个场景

复杂业务场景2-倒计时案例

  • 场景1:点击按钮倒计时,并且禁用按钮,倒计时结束解除禁用

image.png

功能代码

import React, { useState, useEffect, useRef } from 'react'
import ReactDOM from 'react-dom'
export default function App () {
  const [cansend, setcansend] = useState(true)
  const [timer, settimer] = useState(3)
  const time = useRef(null)
  // 点击事件
  const send = () => {
    // 初始赋值给3
    settimer(3)
    // 1.禁用按钮
    setcansend(false)
    // 2.开启定时器
    time.current = setInterval(() => {
      settimer((timer) => timer - 1)
    }, 1000)
  }
  useEffect(() => {
    console.log('当前timer为', timer)
    // 如果时间为0
    if (timer === 0) {
      // 1.解除禁用
      setcansend(true)
      // 2.清除定时器
      clearInterval(time.current)
    }
  }, [timer])
  return (
    <div>
        <input type="text" name="" id="" />
        <button disabled={!cansend} onClick={send}>{cansend ? '发送验证码' : timer + '秒后重发'}</button>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))
  • 场景2:页面刚打开自动倒计时,倒计时结束跳转至指定页面

image.png

功能代码

import React, { useEffect, useRef, useState } from 'react'
import ReactDOM from 'react-dom'
export default function App () {
  const [n, setn] = useState(5)
  const timeId = useRef(null)
  useEffect(() => {
    // 设置5秒后跳转
    setn(5)
    // 开启定时器
    timeId.current = setInterval(() => {
      setn((n) => n - 1)
    }, 1000)
  }, [])
  useEffect(() => {
    // 时间为0,跳转网页
    if (n === 0) {
      location.href = 'http://www.baidu.com'
      // 清除定时器
      clearInterval(timeId.current)
    }
  }, [n])
  return (
    <div>
        404该页面不存在,{n}秒后将自动返回<a href="http://www.baidu.com">这里</a>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))
这个场景比上一个场景稍微复杂,所以在自定义hooks前,我们要先分析清楚要实现什么功能,什么功能不一样

分析核心业务逻辑

相同点:

  • 都是倒计时,到时间执行回调 不同点:
  • 场景1是点击触发,场景2一开始就触发
  • 场景1到时间后解禁按钮,场景2到时间跳转页面
  • 倒计时的时间不同

通过上面的分析,得出不同点和相同点后,思路如下:

  1. 用户自定义倒计时时间和时间为0后执行的回调,传入到hooks中
  2. 自定义hooks用形参接收时间和回调函数,定时器为0执行相应回调
  3. 自定义hooks中把剩余时间和定时器启动方式返回给用户

自定义hooks实现

我们新建文件useCountDown.js,在里面自定义hooks

import { useEffect, useRef, useState } from 'react'
// 接受两个参数,一个是时间,一个是到时间后执行的回调
export default (initCount = 10, callback = () => {}) => {
  const [count, setcount] = useState(initCount)
  // 使用useRef清除定时器
  const timeID = useRef(null)
  // 设置启动函数
  const start = () => {
    // 设置倒计时时间
    setcount(initCount)
    // 开启定时器
    timeID.current = setInterval(() => {
      setcount((count) => count - 1)
    }, 1000)
  }
  useEffect(() => {
    // 时间为0,执行回调
    if (count === 0) {
      callback()
      // 清除定时器
      clearInterval(timeID.current)
    }
  }, [count, callback])
  // 组件销毁的钩子,避免意外销毁组件,而定时器却没停止
  useEffect(() => {
    return () => {
      clearInterval(timeID.current)
    }
  }, [])
  // 返回剩余时间,以及启动函数
  return {
    count, start
  }
}

在之前的两个场景中,都复用逻辑代码后

场景1-复用逻辑后

import React, { useState } from 'react'
import ReactDOM from 'react-dom'
import useCountdown from './useCountdown'
export default function App () {
  const [cansend, setcansend] = useState(true)
  const { count, start } = useCountdown(3, () => {
    // 解禁按钮
    setcansend(true)
  })
  const send = () => {
    // 1.禁用按钮
    setcansend(false)
    start()
  }
  return (
    <div>
        <input type="text" name="" id="" />
        <button disabled={!cansend} onClick={send}>{cansend ? '发送验证码' : count + '秒后重发'}</button>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

复用逻辑前后对比

image.png

场景2-复用逻辑后

/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect } from 'react'
import ReactDOM from 'react-dom'
import useCountdown from './useCountdown'

export default function App () {
  const { count: n, start } = useCountdown(5, () => {
    location.href = 'http://www.baidu.com'
  })
  useEffect(() => {
    start()
  }, [])
  return (
    <div>
      该页面不存在,{n}秒后将自动返回<a href="http://www.baidu.com">这里</a>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

复用逻辑前后对比 image.png

这个案例稍微比上一个复杂点,更能体现出复用逻辑的好处

总结

通过上面两个场景,我们可以发现复用逻辑的好处,在不使用hooks的情况下,我们很难去复用业务逻辑.有些小伙伴可能就会说了,上面第一个场景,我自定义一个函数也能做.我想说的是,实现是可以实现,但函数是没有生命周期这个概念的.也就是说,你开启定时器或开启监听事件以后,无法再去把定时器清除或移除事件.这点我们是很难办到的.不过hooks给函数组件带来了生命周期和状态的概念,让我们前端开发更为简单,维护起来更加容易

掌握好hooks以及学会复用逻辑代码,是我们成为高级前端的必经之路

  • 码字不易,点个赞呗
  • 在此祝各位看官年薪百万,天天摸鱼

QQ图片20211109090631.jpg