React Hooks小记(五)_useEffect

useEffect

1. 什么是函数的副作用

函数的副作用就是函数除了返回值外外界环境造成的其它影响,即与组件渲染无关的操作。例如获取数据修改全局变量更新 DOM 等。

1、获取数据

2、修改全局变量

3、更新 DOM

useEffect 是 React 中的 hooks API。通过 useEffect 可以执行一些副作用操作,例如:请求数据、事件监听等。它的语法格式如下:

useEffect(fn, deps?)

其中:

1. 第一个参数 fn 是一个副作用函数,该函数会在每次渲染完成之后被调用;

2. 第二个参数是可选的依赖项数组,这个数组中的每一项内容都会被用来进行渲染前后的对比

    a. 当依赖项发生变化时,会重新执行 fn 副作用函数

    b. 当依赖项没有任何变化时,则不会执行 fn 副作用函数

2. useEffect 的执行时机

useEffect(fn)

  1. 初次渲染时执行

  2. 如果省略了依赖项的数组,则 useEffect 中的副作用函数,会在组件每次更新渲染完毕都执行

  3. 如果为 useEffect 指定了依赖项的数组,则 useEffect 中的副作用函数,会在组件每次渲染完毕之后,判断依赖项是否变化,再决定是否执行副作用函数

  4. 如果为 useEffect 指定了空的依赖项数组,则 useEffect 中的副作用函数,仅在组件首次渲染完毕之后,执行唯一的一次

2.1 没有依赖项

如果没有为 useEffect 指定依赖项数组,则 Effect 中的副作用函数,会在函数组件每次渲染完成后执行。例如,我们在下面的代码中,基于 useEffect 获取 h1 元素最新的 innerText:

import React, { useEffect, useState } from 'react'

export const Counter: React.FC = () => {
  const [count, setCount] = useState(0)

  // 注意:这里每次输出的都是上一次的【旧值】
  // console.log(document.querySelector('h1')?.innerHTML)

  const add = () => {
    setCount((prev) => prev + 1)
  }

  // 在组件每次渲染完成之后,都会重新执行 effect 中的回调函数
  useEffect(() => {
      // 在此处拿到最新 state
    console.log(document.querySelector('h1')?.innerHTML)
  })

  return (
    <>
      <h1>count 值为:{count}</h1>
      <button onClick={add}>+1</button>
    </>
  )
}

2.2 deps 为空数组(componentDidMount)

如果为 useEffect 指定了一个空数组 [] 作为 deps 依赖项,则副作用函数只会在组件首次渲染完成后执行仅此一次

当组件 rerender 的时候不会触发副作用函数的重新执行。例如下面的代码中,useEffect 中的 console.log() 只会执行1次:

import React, { useEffect, useState } from 'react'

export const Counter: React.FC = () => {
  const [count, setCount] = useState(0)

  const add = () => {
    setCount((prev) => prev + 1)
  }

  // 仅在组件首次渲染完成后,会执行 effect 中的回调函数
  useEffect(() => {
    console.log(document.querySelector('h1')?.innerHTML)
  }, [])

  return (
    <>
      <h1>count 值为:{count}</h1>
      <button onClick={add}>+1</button>
    </>
  )
}

2.3 deps 为依赖项数组(componentDidUpdate)

如果想有条件地触发副作用函数的重新执行,则需要通过 deps 数组指定依赖项列表

React 会在组件每次渲染完成后,对比渲染前后的每一个依赖项是否发生了变化,只要任何一个依赖项发生了变化,都会触发副作用函数的重新执行。否则,如果所有依赖项在渲染前后都没有发生变化,则不会触发副作用函数的重新执行。

下面的例子演示了依赖项的使用:只有当 count 值发生变化时,才会触发 effect 回调函数的重新执行,flag 值的变化不会触发:

import React, { useEffect, useState } from 'react'

export const Counter: React.FC = () => {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)

  const add = () => {
    setCount((prev) => prev + 1)
  }

  // 在组件每次渲染完成后,如果 count 值发生了变化,则执行 effect 中的回调
  // 其它状态的变化,不会导致此回调函数的重新执行
  useEffect(() => {
    console.log(document.querySelector('h1')?.innerHTML)
  }, [count])

  return (
    <>
      <h1>count 值为:{count}</h1>
      <p>flag 的值为:{String(flag)}</p>
      <button onClick={add}>+1</button>
      <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>
    </>
  )
}

注意:不建议对象作为 useEffect 的依赖项,因为 React 使用 Object.is() 来判断依赖项是否发生变化。

3. 如何清理副作用(componentBeforeDestory)

useEffect 可以返回一个函数,用于清除副作用的回调。语法格式如下:

useEffect(() => {
  // 1. 执行副作用操作
  // 2. 返回一个清理副作用的函数
  return () => { /* 在这里执行自己的清理操作 */ }
}, [依赖项])

// 简写方式
useEffect(() => () => { 
    /* 在这里执行自己的清理操作 */ 
}, [依赖项])

实际应用场景:如果当前组件中使用了定时器或绑定了事件监听程序,可以在返回的函数中清除定时器或解绑监听程序。

​ 1、清理 Ajax 请求或者 定时器

​ 2、解绑事件监听

3.1 组件卸载时终止未完成的 Ajax 请求

父组件 TestRandomColor 中,使用布尔值 flag 控制子组件 RandomColor 的展示与隐藏:

export const TestRandomColor: React.FC = () => {
  const [flag, setFlag] = useState(true)

  return (
    <>
      <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>
      <hr />
      {flag && <RandomColor />}
    </>
  )
}

子组件 RandomColor 中,通过 useEffect(fn, \[\]) 声明一个副作用函数,该副作用函数仅在组件首次渲染完毕后执行。在该副作用函数中,基于 fetch API 请求数据,并且在清理函数中使用 AbortController 对象自动终止未完成的 Ajax 请求。示例代码如下:

const RandomColor: React.FC = () => {
  const [color, setColor] = useState('')

  useEffect(() => {
    const controller = new AbortController()

    fetch('https://api.liulongbin.top/v1/color', { signal: controller.signal })
      .then((res) => res.json())
      .then((res) => {
        console.log(res)
        setColor(res.data.color)
      })
      .catch((err) => console.log('消息:' + err.message))

    // return 清理函数
    // 清理函数触发的时机有两个:
    // 1. 组件被卸载的时候,会调用
    // 2. 当 effect 副作用函数被再次执行之前,会先执行清理函数
    return () => controller.abort()
  }, [])

  return (
    <>
      <p>color 的颜色值是:{color}</p>
    </>
  )
}

3.2 获取鼠标在网页中移动时的位置

示例代码如下,先声明一个 MouseInfo 的子组件,用来监听鼠标的移动并打印鼠标的位置:

const MouseInfo: React.FC = () => {
  // 记录鼠标的位置
  const [position, setPosition] = useState({ x: 0, y: 0 })

  // 副作用函数
  useEffect(() => {
    // 1. 要绑定或解绑的 mousemove 事件处理函数
    const mouseMoveHandler = (e: MouseEvent) => {
      console.log({ x: e.clientX, y: e.clientY })
      setPosition({ x: e.clientX, y: e.clientY })
    }

    // 2. 组件首次渲染完毕后,为 window 对象绑定 mousemove 事件
    window.addEventListener('mousemove', mouseMoveHandler)

    // 3. 返回一个清理的函数,在每次组件卸载时,为 window 对象解绑 mousemove 事件
    return () => window.removeEventListener('mousemove', mouseMoveHandler)
  }, [])

  return (
    <>
      <p>鼠标的位置:{JSON.stringify(position)}</p>
    </>
  )
}

再声明一个 TestMouseInfo 的父组件,通过布尔值 flag 控制子组件 MouseInfo 的显示或隐藏:

export const TestMouseInfo: React.FC = () => {
  // 定义布尔值 flag,控制子组件的显示或隐藏
  const [flag, setFlag] = useState(true)

  return (
    <>
      <h3>父组件</h3>
      {/* 点击按钮,切换 flag 的值 */}
      <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>
      <hr />
      {flag && <MouseInfo />}
    </>
  )
}
获取鼠标位置节流版

在子组件 MouseInfo 中引入定时器 setTimeOut ,父组件 TestMouseInfo 不变

const Mouse:React.FC = () => {
    const [position, setPositon] = useState({ x: 0, y: 0 })

    useEffect(() => {
        // 添加节流操作
        let timer: null | NodeJS.Timeout = null
        
        const handleMouse = (e: MouseEvent) => {
            if (timer) return
            timer = setTimeout(() => {
                console.log(e.clientX, e.clientY);
                setPositon({ x: e.clientX, y: e.clientY })
                timer = null
            },500)
        }
        window.addEventListener('mousemove', handleMouse)

        return () => {
            window.removeEventListener('mousemove',handleMouse)
        }
    }, [])

    return (
        <>
            {/* 对象不能直接放到页面上 */}
            <h3>鼠标的位置{JSON.stringify(position)}</h3>
        </>
    )
}

4. 自定义封装鼠标位置的 hook

在 src 目录下新建 hooks/index.ts 模块,并把刚才获取鼠标位置的代码封装成名为 useMousePosition 的自定义 hook,代码如下:

import { useState, useEffect } from 'react'

export const useMousePosition = () => {
  // 记录鼠标的位置
  const [position, setPosition] = useState({ x: 0, y: 0 })

  // 副作用函数
  useEffect(() => {
    // 1. 要绑定或解绑的 mousemove 事件处理函数
    const mouseMoveHandler = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }

    // 2. 组件首次渲染完毕后,为 window 对象绑定 mousemove 事件
    window.addEventListener('mousemove', mouseMoveHandler)

    // 3. 返回一个清理的函数,在每次组件卸载时,为 window 对象解绑 mousemove 事件
    return () => window.removeEventListener('mousemove', mouseMoveHandler)
  }, [])

  return position
}

MouseInfo 组件中,可以导入自己封装的 hook 进行使用:

import { useMousePosition } from '@/hooks/index.ts'

const MouseInfo: React.FC = () => {
  // 调用自定义的 hook,获取鼠标的位置信息
  const position = useMousePosition()

  return (
    <>
      <!-- 输出鼠标的位置信息 -->
      <p>鼠标的位置:{JSON.stringify(position)}</p>
    </>
  )
}

在 TestMouseInfo 组件中,也可以导入自己封装的 hook 进行使用:

import { useMousePosition } from '@/hooks/index.ts'

export const TestMouseInfo: React.FC = () => {
  const [flag, setFlag] = useState(true)
  // 调用自定义的 hook,获取鼠标的位置信息
  const position = useMousePosition()

  return (
    <>
      <!-- 输出鼠标的位置信息 -->
      <h3>父组件 {position.x + position.y}</h3>
      <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>
      <hr />
      {flag && <MouseInfo />}
    </>
  )
}

5. 自定义封装秒数倒计时的 hook

功能分析:

​ 1、用户调用 useCountDown(5) 的 hook,可以传递倒计时的秒数,如果未指定秒数则默认值10

​ 2、在 useCountDown 中,需要对用户传递进行来的数字进行非法值的判断和处理(处理负数、小数、0)

​ 3、每隔1秒让秒数 -1,并使用一个布尔值记录按钮是否被禁用

​ 4、以数组的形式,向外返回每次的秒数和当前的禁用状态,例如 return \[count, disabled\]

最终,用户可以按照如下的方式,使用我们封装的 useCountDown hook

import React from 'react'
// 1. 导入自定义的 hook
import { useCountDown } from '@/hooks/index.ts'

export const CountDown: React.FC = () => {
  // 2. 调用自定义的 hook
  const [count, disabled] = useCountDown(3)

  return (
    <>
      <!-- 3. 展示倒计时的秒数,并控制按钮的禁用状态 -->
      <button disabled={disabled} onClick={() => console.log('协议生效!')}>
        {disabled ? `请仔细阅读本协议内容(${count} 秒)` : '确认此协议'}
      </button>
    </>
  )
}

接下来,我们可以在 src/hooks/index.ts 模块中,封装名为 useCountDown 的自定义 hook。具体代码如下:

import { useState, useEffect } from 'react'

// TS 类型
type UseCountDown = (seconds: number) => [number, boolean]

export const useCountDown: UseCountDown = (seconds = 10) => {
  // 对外界传递的数值进行【非法值处理】:
  // 1. 先求绝对值
  // 2. 再对小数进行四舍五入
  // 3. 如果处理的结果为数字 0,则将默认值设为 10
  seconds = Math.round(Math.abs(seconds)) || 10

  // 计数器
  const [count, setCount] = useState(seconds)
  // 倒计时是否结束 disabled 为 false 表示结束,为 true 表示未结束
  const [disabled, setDisabled] = useState(true)

  useEffect(() => {
    const timerId = setTimeout(() => {
      if (count > 1) {
          // 这里需要用【函数写法】 因为新值依赖旧值
        setCount((prev) => prev - 1)
      } else {
        setDisabled(false)
      }
    }, 1000)

    // 返回清理函数,再次执行 useEffect 的副作用函数之前,先运行上次 return 的清理函数
    return () => clearTimeout(timerId)
  }, [count])

  // 返回 count 和 disabled 供组件使用
  // 1. count 用来显示倒计时的秒数
  // 2. disabled 用来控制按钮是否禁用 Or 倒计时是否结束
  return [count, disabled]
}

6. useEffect 的使用注意事项

1. 不要在 useEffect 中改变依赖项的值,会造成死循环。

2. 多个不同功能的副作用尽量分开声明,不要写到一个 useEffect 中。

  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: React Hooks中的useEffect是一个函数,用于在函数组件中处理副作用。它接收两个参数:一个函数和一个依赖数组。当依赖数组中的任何一个值发生变化时,useEffect中的函数就会被调用。这个函数可以用来执行一些副作用操作,比如发送网络请求、订阅事件、更新DOM等。在函数组件中使用useEffect可以替代类组件中的生命周期方法。 ### 回答2: React hooks是React 16.8版本新增的特性,可以让函数组件具有类组件的一些功能,其中包括useEffect这一HookuseEffect可以让我们在函数组件中实现类似于类组件中生命周期函数的效果。 useEffect接收两个参数,第一个参数是一个回调函数,第二个参数是一个数组。回调函数在组件每次渲染后都会执行,类似于componentDidUpdate函数。如果数组为空,回调函数仅在组件首次渲染时执行,类似于componentDidMount函数。如果数组不为空,回调函数仅在数组中的值有变化时执行,类似于shouldComponentUpdate函数。 useEffect可以用来处理一些副作用操作,比如数据的异步获取和更新、页面的滚动位置、事件的监听等等。在回调函数中进行的操作可能会影响到组件的状态、渲染等方面,所以我们需要使用useEffect来进行监控和管理。在回调函数中可以返回一个函数,这个函数在组件不再需要该效果时执行,可以用于取消监听等清理操作。 需要注意的是,useEffect在每次渲染后都会执行,如果回调函数中有一些操作比较耗时,则会对性能造成影响。我们可以使用useMemo、useCallback等Hook来优化这些操作的性能。此外,在数组中传递的参数需要谨慎选择,不恰当的选择可能会导致useEffect失效。 总之,useEffectReact hooks中非常重要的一个Hook,可以用来做很多副作用操作。只要我们掌握了useEffect的用法和注意事项,就能更好地管理和维护函数组件的状态和渲染。 ### 回答3: React Hooks是React16.8引入的新特性,大大简化了React组件的编写方式,其中useEffectReact Hooks中的一个非常重要的Hook,它允许我们在函数组件中执行任意一段副作用代码。 在React环境中,副作用通常涉及向服务器请求数据、处理DOM元素以及处理其他一些与UI渲染无关的操作。这些操作称为副作用,因为它们可能会对其他部分的应用程序的状态或行为产生影响,我们需要小心处理这些副作用。 使用useEffect可以在组件渲染时执行副作用,而且可以灵活地在组件生命周期中、或是在state或props发生变化时进行副作用的更新。 useEffect使用一个回调函数作为第一个参数,并在回调函数中定义需要执行的副作用代码。useEffect还可以有第二个参数,是一个数组,用于定义何时更新副作用的条件。如果第二个参数为空数组,那么副作用只会在组件挂载或卸载时执行。如果第二个参数包含依赖值,那么当依赖值有变化时,副作用也会被重新执行。 通过使用useEffect函数,我们可以在React函数组件中编写可以执行副作用代码的简洁股票。同时,也可以通过控制副作用代码的执行时机,避免副作用对应用程序产生不必要的影响。因此,useEffect已经成为React Hooks开发中的必不可少的一部分。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值