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)
初次渲染时执行
如果省略了依赖项的数组,则 useEffect 中的副作用函数,会在组件每次更新渲染完毕都执行
如果为 useEffect 指定了依赖项的数组,则 useEffect 中的副作用函数,会在组件每次渲染完毕之后,判断依赖项是否变化,再决定是否执行副作用函数
如果为 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
中。