useEffect

99 阅读5分钟

useEffect

什么是函数的副作用

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

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

useEffect(fn, deps?)
  • 第一个参数 fn 是一个副作用函数,该函数会在每次渲染完成之后被调用。

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

    • 当依赖项发生变化时,会重新执行 fn 副作用函数
    • 当依赖项没有任何变化时,则不会执行 fn 副作用函数

useEffect 的执行时机

  • 如果没有为 useEffect 指定依赖项数组,则 Effect 中的副作用函数,会在函数组件每次渲染完成后执行。
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(() => {
    console.log(document.querySelector('h1')?.innerHTML)
  })

  return (
    <>
      <h1>count 值为:{count}</h1>
      <button onClick={add}>+1</button>
    </>
  )
}
  • 如果为 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>
    </>
  )
}
  • 如果想有条件地触发副作用函数的重新执行,则需要通过 deps 数组指定依赖项列表

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

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() 来判断依赖项是否发生变化。

如何清理副作用

  • useEffect 可以返回一个函数,用于清除副作用的回调。
useEffect(() => {
  // 1. 执行副作用操作
  // 2. 返回一个清理副作用的函数
  return () => {
    /* 在这里执行自己的清理操作 */
  }
}, [依赖项])
  • 实际应用场景
    • 如果当前组件中使用了定时器或绑定了事件监听程序,可以在返回的函数中清除定时器或解绑监听程序。

useEffect 的使用注意事项

  1. 不要在 useEffect 中改变依赖项的值,会造成死循环。
  2. 多个不同功能的副作用尽量分开声明,不要写到一个 useEffect 中。

组件卸载时终止未完成的 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>
    </>
  )
}

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

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>
    </>
  )
}

自定义封装鼠标位置的 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
}
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>
    </>
  )
}

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

import { useState, useEffect } from 'react'

type UseCountDown = (seconds: number) => [number, boolean]

export const useCountDown: UseCountDown = (seconds = 10) => {
  seconds = Math.round(Math.abs(seconds)) || 10
  const [count, setCount] = useState(seconds)
  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]
}
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 (
    <>
      <button disabled={disabled} onClick={() => console.log('xxxx')}>
        {disabled ? `倒计时(${count} 秒)` : 'xxxx'}
      </button>
    </>
  )
}