useEffect(回调函数,[依赖项])

  • 官方文档
  • 我们可以认为,useEffect()是一个监视函数,它能够帮助我们再组件挂载的时候执行对应的回调函数或者再我们想要监视的变量变化时执行对应的回调函数。

useEffect可以做什么?(模拟生命周期)

  1. 挂载阶段:
    • 从上向下执行函数,如果碰到 useEffect 就执行并将 useEffect 传入的副作用函数推入一个队列(链表),在组件挂载完成之后,将队列(链表)中的副作用函数执行,并将副作用函数返还函数,推入一个新的队列(链表)
  2. 更新阶段
    • 更新阶段不同于其他阶段对应的函数是否要执行,取决于依赖参数
    • 从上向下执行函数,如果碰到 useEffect 就执行并将 useEffect 传入的副作用函数推入一个队列(链表),在组件更新完成之后,找出之前的返回函数队列,依次准备执行,在执行前会判断该 useEffect 的依赖参数,如果依赖参数改变就执行,否则跳过当前项去看下一项,然后再执行副作用队列,执行时同样判断依赖是否变化,来决定其是否执行,如果执行,就重新获取其对应的返回函数
  3. 卸载阶段:
    • 组件即将卸载时,找出其对应的返回函数队列,依次执行!

常规处理副作用的几种情况:

依赖参数不同时有不同的效果:

  1. 为空: 组件的任何更新,该 useEffect 对应的返回函数副作用函数都执行
  2. 为空数组: 不监听组件的更新
  3. 数组中有具体依赖:对应的依赖数据,有变化的时候,才会执行

useEffect()的三种使用方法:

// 1. 每次更新之后都要执行
useEffect(() => {
// 副作用函数的内容

return ()=>{ // 返回函数(每次变化时执行)

}
})

-------------------------------------------
// 2. 初始化页面时 只执行第一次
useEffect(() => {
// 副作用函数的内容

return ()=>{ // 返回函数(组件销毁时执行)

}
}, [])

-------------------------------------------
// 3. 初始执行一次,每次依赖项的值变化时执行
useEffect(() => {
// 副作用函数的内容

return ()=>{ // 返回函数(依赖变化时执行)

}
}, [依赖项]) //依赖项可以有多个

上面提到的返回函数又称清理函数,它的执行时机为:在组件卸载时以及下一次副作用函数调用之前执行,由此,我们就可以使用useEffect()来模拟生命周期函数,如componentWillUnmount

// 相当于componentWillUnmount,组件销毁时执行
useEffect(()=>{
return ()=>{
// 做清理工作
console.log("组件即将卸载时执行");
}
}, [])

不仅如此我们还可以模拟各种各样的生命周期函数

  1. 组件挂载完之后做某事(componentDidMounted)
useEffect(()=>{
console.log("组件挂载完之后执行");
return ()=>{

}
},[]);
  1. 组件挂载完成及更新完成做某事(componentDidUpdate)
useEffect(()=>{
console.log("组件挂载完成之后及更新完成之后执行");
})//不监视依赖,则每一次数据更新都会执行副作用函数
  1. 组件更新完做某事(componentDidUpdate)
const isMounted = useRef(false);
useEffect(()=>{
if(isMounted.current){
console.log("组件更新完成")
} else {
isMounted.current = true;
}
})
  1. 组件即将卸载做某事(componentWillUnmount)
useEffect(()=>{
return ()=>{
console.log("组件即将卸载时执行");
}
},[]);

案例展示:

  • 使用useEffect重塑上面生命周期的案例: 父组件给子组件提供数据,子组件接收后,渐变展示(定时器),父组件点击销毁子组件后子组件消失,同时关闭定时器
// hooks - useEffect(()=>{副作用函数内容,返回函数},[依赖]) 
/*
相当于给函数组件提供一个监视功能,组件创建的时候回调函数执行一次,
每一次依赖更新,回调函数再次执行,切记最好不要对依赖进行撒谎, 也就是说
你在函数体上明明使用了某一个变量,但是却没有在依赖中申明,就相当于你对React撒了谎,
后果就是,每当你再函数体中使用的变量发生变化时,useEffect不会再执行且eslint也会报警告!

在组件中支持多次注册使用useEffect()
*/

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

// 子组件
function Son(props){//接收props数据
const [opacity,setOpacity] = useState(1)//透明度
const [isOpenTimer,setIsOpenTimer] = useState(0)//是否开始定时器
const [count,setcount] = useState(0)//是否开始定时器

// 父子通信通过props传递数据
console.log('父组件传过来的数据',props)

// 设置定时器(渐变展示姓名)
var timer = isOpenTimer?setInterval(()=>{
if(opacity >= 0){
setOpacity(opacity - 0.1)
}else{
setOpacity(1)
}
},1000):''

// 模拟生命周期组件(componentDidMounted)创建执行一次
useEffect(()=>{
// 延时两秒开启定时器(校验定时器的开启是否遵循useEffect)
console.log('延时2s开启定时器');
setTimeout(()=>{
setIsOpenTimer(1)
},2000)
},[])//不监视任何依赖

// 监视姓名修改次数(类似Vue中的watch属性)
useEffect(()=>{
console.log('姓名修改了');
setcount(count+1)
},[props.sendName])

// 模拟组件销毁时 componentWillUnmount
useEffect(()=>{
return ()=>{
console.log("组件即将卸载时关闭定时器");
clearInterval(timer)//清除定时器
}
},[]);

return (
<div>
<p>-------------子组件---------------</p>
<p>
父组件传过来的名字:
<span style={{opacity:opacity}}>
{'姓名:'}{props.sendName} - {'修改次数:'+ count}
</span>
</p>
</div>
)
}

// 父组件
export default function App() {
const [isShowSon,setisShowSon] = useState(1)//是否展示子组件
const [name,setName] = useState('lam')//姓名

return (
<div>
<p>---------------父组件---------------</p>
<button onClick={()=>{
setName('肥林')//修改姓名发送给子组件
}}>提交给子组件</button>
{/* 通过标志位判断是否展示子组件 */}
{isShowSon && <Son sendName={name}></Son>}
<button onClick={()=>{
setisShowSon(0)
}}>销毁(关闭渐变展示)</button>
</div>
)
}

结果展示:

image

useEffectuseLayoutEffect有什么区别?

简单来说就是调用时机不同, useLayoutEffect 和原来 componentDidMount & componentDidUpdate 一致,在react完成DOM更新后马上同步调用的代码,会阻塞页面渲染。而useEffect 是会在整个页面渲染完才会调用的代码。官方建议是优先使用useEffect,

在实际使用时如果想避免页面抖动(在useEffect 里修改DOM很有可能出现)的话,可以把需要操作DOM的代码放在useLayoutEffect 里。在这里做点dom操作,这些dom修改会和 react 做出的更改一起被一次性渲染到屏幕上,只有一次回流、重绘的代价。