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 的使用注意事项
- 不要在 useEffect 中改变依赖项的值,会造成死循环。
- 多个不同功能的副作用尽量分开声明,不要写到一个 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>
</>
)
}