持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
在👉 上一节 中,我们掌握了useState,useState可以使函数组件维持住状态。本节我们一起学习 React 中的useEffect(副作用)。在开始之前,我想说一句:useEffect 并不是组件的生命周期。
⚠️ 注意:如果你以往以 Class Component 方式开发,请一定不要将 useEffect 对应到 Class Component 的任何生命周期中,请你忘掉那些生命周期!!!
useEffect 的基本概念
副作用是指一段和当前执行结果无关的代码。比如说要修改函数外部的某个变量,要发起一个请求,等等。也就是说,在函数组件的当次执行过程中,useEffect 中代码的执行是不影响渲染出来的 UI 的。下面是 useEffect 的函数签名👇
useEffect(callback, dependencies)
第一个为要执行的函数 callback,第二个是可选的依赖项数组 dependencies。 依赖项这个参数是可选的,callback 会根据依赖项分为以下三种情况:
- 如果不指定,那么 callback 就会在每次函数组件执行完后都执行;
- 如果指定了,那么只有依赖项中的值发生变化的时候,它才会执行;
- 如果指定为空数组,那么 callback 会在 mount (首次render)以后执行。
值得注意的是,callback 是在页面渲染完成以后才会执行,我们看下面的代码的执行顺序
import { useEffect } from 'react';
function App() {
useEffect(() => {
console.log('useEffect打印')
}, [])
console.log('render打印')
return (
<div>渲染内容</div>
)
}
export default App;
callback 除了执行顺序这个特点,第二个特点就是callback本身不能是一个 Promise 函数,如果你需要在里面执行异步代码,可以在内部做,示例如下👇
function App() {
useEffect(async() => { // ❌ 像这种情况就是不允许的
const res = await query()
}, [])
useEffect(() => { // ✅ 这两种方式都可以
// 1. 封装一个函数,然后执行
const promise = async () => {
const res = await query()
}
promise()
// 2. 使用IFEE(立即执行函数)其实本质与上面一样
(async() => {
const res = await query()
})(); // 如果后面还有代码的话需要加上分号,不然 js 解析会报错
}, [])
return null
}
callback 的第三个特点就是它可以返回一个函数,这个函数会在下一次 render 以后执行(假设我们useEffect所依赖的值发生变化,会再次触发 useEffect),它主要用于清除副作用,比如定时器等。(后面会有例子,这里就不写了)
useEffect 可以用来干什么?
如果你打开 react 的官方网站,搜索useEffect,相信我,你看完绝对是一脸懵逼,心里也许一万个问号。这玩意儿是个啥?我能用它干什么?
别着急,下面我将举一些简单的例子,简单的说明一下 useEffect 的使用场景,作为一个基本印象,后续我们会经常使用它,所以不用担心。
1. 数据请求
这也许是最常见的一种场景,我想不需要过多解释,直接看怎么用。
interface UserType {
name: string;
age: number;
}
// 你可以假装这是个请求的接口,一秒钟以后会返回你一个用户列表
function queryUsers(): Promise<UserType[]> {
return new Promise((resolve, reject) => {
const list = [
{ id: 1, name: '小黑', age: 18 },
{ id: 2, name: '小白', age: 16 },
{ id: 3, name: '小光', age: 19 },
]
setTimeout(() => {
resolve(list)
}, 1000)
})
}
function App() {
const [users, setUsers] = useState<UserType[]>([])
useEffect(() => {
const queryUsersHandler = async () => {
const res = await queryUsers() // 发起请求获得数据
// 设置新的数据
setUsers(res)
}
queryUsersHandler()
}, [])
return (
<div>
{
{/* 根据数据渲染 */}
users.map(item => (
<div key={item.id} style={{ marginBottom: 20 }}>
<div>姓名:{item.name}</div>
<div>年龄:{item.age}</div>
</div>
))
}
</div>
)
}
2. 监听DOM事件
假设我们需要监听浏览器窗口大小,那么这时候 callback 中返回的函数就需要写上对应的移除监听的事件
function App() {
useEffect(() => {
const handler = () => {
setScrollSize({
wdith: window.innerWidth,
height: window.innerHeight,
})
}
handler()
// 一般来讲肯定是要做防抖的,我这里就偷个懒
window.addEventListener('resize', handler)
return () => {
// 在return 的函数中清除掉
window.removeEventListener('resize', handler)
}
}, [])
return (
<div>
<div>window width: {windowSize.wdith}</div>
<div>window height: {windowSize.height}</div>
</div>
)
}
3. 监听数据做出响应
我个人感觉,一般来讲这样的场景都是数据变化了就发送请求(比如用户 id 变化了,就会根据新的 id 发送请求),当然纯粹的数据监听,然后做点儿什么也是有这样的情况存在的
function App() {
const [userId, setUserId] = useState('a')
useEffect(() => {
// 大概就是如此,每次 userId 变化了就在这里发个请求啥的(mount 的时候也会执行)
fetch(userId)
// ......
}, [userId])
// .....
}
4. 定时器轮询
有时我们需要针对一个接口进行轮询操作,或者是其他需要使用到定时器每秒做些什么事情(总之就是需要用定时器来做点儿什么事,尽管一般都是使用延时器🤣 🤣 🤣)比如这里进入页面开始,就要不停的通过接口获取某个状态,根据返回的状态进行展示。(为了代码完整性,我都贴出来了,重点看 Demo 组件就行)
interface STA {
sta: number
}
let retSta = 0
const resetRetSta = () => retSta = 0
const COMPLETE = 3
function queryStatus(): Promise<STA> {
return daley({ sta: retSta++ }, 1000)
}
function daley<T>(res: T, time: number): Promise<T> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(res)
}, time)
})
}
function Demo() {
const [status, setStatus] = useState(0)
useEffect(() => {
console.log('useEffect')
let timer = setInterval(async () => {
const res = await queryStatus()
setStatus(res.sta)
console.log((res))
if (res.sta === COMPLETE) { // 完成以后停止发送请求
clearInterval(timer)
resetRetSta() // 为了重置返回sta,忽略即可
}
}, 1200)
// 组件被关闭时,清除定时器
return () => {
console.log('return callback')
clearInterval(timer)
resetRetSta() // 为了重置返回sta,忽略即可
}
}, [])
console.log('render')
return (
<div id='app'>
<div>{ status === COMPLETE ? '任务完成' : '任务进行中' }</div>
</div>
)
}
function App() {
const [visible, setVisible] = useState(false)
const handle = () => {
setVisible(!visible)
}
return (
<div>
<button onClick={handle}>{ visible ? '不展示' : '展示' }</button>
{ visible && <Demo /> }
</div>
)
}
你可以尝试一下不写callback中的返回函数中的clearInterval(timer),并在展示Demo组件时设置为不展示试试看,它会不停的发送请求(直至判断为完成,清楚定时器为止)。
同样的上面监听DOM事件也是一样,尝试监听一个dom节点,在其展示了以后再设置为不展示,那么会因为找不到dom节点而不停的产生错误日志。(在原生JS中会发生什么事情,那么在React会产生同样的现象,毕竟 React 的本质就是JS)内存泄漏就此造成~ 所以在处理这类场景时记得要清除掉副作用。
如何避免 useEffect 的“坑”
其实只要你把上面的这几点搞明白了,一般来讲也不会太遇到什么坑。我们这里就简单举一两个需求说明一下。
假设我们有一个需求,在屏幕宽度在300以下时上报当前用户的状态。(哈哈哈,把产品经理 🦈 🌶️ 🤣 )
在下面的代码中,我们在页面加载时添加一个监听事件,我们期望的是,当页面发生resize并且屏幕宽度小于等于300时上报当前的用户状态userStatus和屏幕宽度screeWidth。
function App() {
const [userStatus, setUserStatus] = useState(0)
useEffect(() => {
const handler = () => {
const screenWidth = window.innerWidth
if (screenWidth <= 300) {
// 假设这里是上传接口
request(userStatus, screenWidth)
}
}
handler()
// 一般来讲肯定是要做防抖的,我这里就偷个懒
window.addEventListener('resize', handler)
return () => {
// 在return 的函数中清除掉
window.removeEventListener('resize', handler)
}
}, [])
return (
<div>
<div>当前状态:{userStatus}</div>
<button onClick={() => setUserStatus(userStatus + 1)}>状态值+1</button>
</div>
)
}
function request(userStatus: number, width: number) {
console.log('上报用户状态与窗口宽度', userStatus, width)
return daley({ sta: 1, msg: 'success' }, 1000)
}
接着我们在浏览器中看结果
可以发现,我们已经修改过了的userStatus明明已经等于 4 了,可在发送请求时仍等于 0
我直接说怎么处理这种情况吧,后续会更一节讲 React 渲染相关的。 这里有两种处理方式:
- 使用
useRef单独存储 一个userStatus的副本
const [userStatus, setUserStatus] = useState(0)
const statusRef = useRef(userStatus) // 使用 useRef
statusRef.current = userStatus // 更新值
useEffect(() => {
const handler = () => {
const screenWidth = window.innerWidth
if (screenWidth <= 300) {
// 使用值
request(statusRef.current, screenWidth)
}
}
}, [])
如果你还不知道useRef是什么,那么你暂时就当它的返回值.current是一个普通的变量好了。
- 在
useEffect的dependencies中添加userStatus即可。
useEffect(() => {
// ......
}, [userStatus])
第一种方法确实行之有效,但是缺带来了一切其他麻烦,比如让代码变得更加复杂,难以维护等。所以还是推荐使用第二种方式。
但是我们在写代码过程中难免会存在忘记添加依赖项的时候,所以为了开发效率更高,更省心,官方建议我们安装校验 hooks 的插件—— eslint-plugin-react-hooks 。这个插件会帮助我们检查书写的 hook 是否正确的添加上了所使用的依赖项(需要有eslint,vscode 的 eslint 插件也可以)。
- 安装
yarn add eslint-plugin-react-hooks --dev
#OR
npm install eslint-plugin-react-hooks --save-dev
- 修改eslint配置(我这里是在package.json文件内)
"eslintConfig": {
// ......
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
},
我们来尝试一下
那么在我们写出错误的hook时就会出现错误提示,出现不规范的hook时也会提示我们原因来进行修复对应的问题。
这一节我们学习了useEffect的一些基本使用方法,在一些场景中如何使用,以及怎样有效避坑,我们本节就先到这里,如果你觉得还算有用,还请点点赞👍 谢谢啦~
这里我留一个小小的思考题:你知道为什么 callback 本身不能是一个Promise吗?欢迎在评论区留言回复答案 😝 (提示,答案就在文中哦~)