React 的 useEffect
是一个重要的 Hook,用于处理组件的副作用,而useLayoutEffect
和它一样,都是处理函数组件内的副作用的,下面就来说说这两个Hook函数的用法。
useEffect
useEffect
是类组件中的生命周期componentDidMount,componentDidUpdate 和 componentWillUnmount 的合并版。
在函数组件中,使用useEffect
,它接收两个参数:一个函数和一个依赖数组,定义如下:
useEffect( ()=>{ return ()=>{ } } , [ ])
()=>{ return ()=>{ } }
:这个函数会在组件的挂载和更新时被调用,它可以包含一些副作用操作,比如操作DOM、发送网络请求等;在这个函数里还可以选择返回一个return
函数,这个return
函数会在组件卸载的时候执行。
[]
:这个数组是一个可选参数,用来指定 useEffect
的依赖项,依赖项包括函数组件的props、state 以及直接在组件主体中声明的所有变量和函数,当依赖数组中的任何一个值发生变化时,都会触发 useEffect
重新执行,如果依赖数组为空,则表示 useEffect
没有任何依赖,只会在组件挂载和卸载时执行。
useEffect基础使用
下面代码是useEffect
最简单的用法:
const Parent = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
useEffect(() => {
console.log('我改变了'+count+'次')
})
const handleClick = () =>{
setCount(count + 1)
}
return (
<div>
<h2>React的Hooks函数useEffect和useLayoutEffect</h2>
<p>Count: {count}</p>
<p>Num: {num}</p>
<button onClick={handleClick}>改变状态count</button>
<button onClick={() => setNum(num + 1)}>改变状态num</button>
</div>
);
};
运行上面代码,可以知道组件挂载时执行了useEffect
,因此我们可以在useEffect
函数组件中执行一些副作用,比如:数据请求、操作DOM、绑定事件等。
当点击按钮"改变状态count"时,执行了useEffect
;当点击按钮“改变状态num”时,也执行了useEffect
,因此不管组件内的哪个状态或属性改变都会重新执行useEffect
。
useEffect数组依赖
情景一
下面代码里useEffect
添加了一个空数组依赖项[]
:
const Parent = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
useEffect(() => {
console.log('count改变了'+count+'次')
}, [])
const handleClick = () =>{
setCount(count + 1)
}
return (
<div>
<h2>React的Hooks函数useEffect和useLayoutEffect</h2>
<p>Count: {count}</p>
<p>Num: {num}</p>
<button onClick={handleClick}>改变状态count</button>
<button onClick={() => setNum(num + 1)}>改变状态num</button>
</div>
);
};
运行上面代码,依然可以看到组件挂载时,执行了useEffect
;当点击按钮"改变状态count"时,没有执行useEffect
;当点击按钮“改变状态num”时,也没有执行useEffect
;因此知道,如果useEffect
添加了空数组依赖项后,只有组件挂载时才会执行useEffect
,其他操作改变组件内的属性状态等都不会执行useEffect
。
情景二
下面代码里useEffect
添加了非空数组依赖项count
:
const Parent = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
useEffect(() => {
console.log('count改变了'+count+'次')
}, [count])
const handleClick = () =>{
setCount(count + 1)
}
return (
<div>
<h2>React的Hooks函数useEffect和useLayoutEffect</h2>
<p>Count: {count}</p>
<p>Num: {num}</p>
<button onClick={handleClick}>改变状态count</button>
<button onClick={() => setNum(num + 1)}>改变状态num</button>
</div>
);
};
运行上面代码,当点击按钮"改变状态count"时,改变count
状态,重新执行了useEffect
,而点击按钮“改变状态num”时,没有执行 useEffect
。因此useEffect
的依赖项有值时,依赖项的值发生变化,那么函数就会重新执行useEffect
,而没在依赖项的值发生了变化,并不会触发useEffect
函数的执行。
useEffect的return函数
情景一
下面代码是个子组件,在子组件内写了useEffect
方法,并且返回了一个return函数
:
function Child(){
useEffect(() => {
console.log('我是子组件的useEffect')
return () => {
console.log('我被卸载了')
}
})
return (
<div>
我是子组件
</div>
)
}
下面代码是父组件 ,在父组件内使用上面的子组件,并且在父组件内做了个判断,当count > 2
时,隐藏子组件:
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () =>{
setCount(count + 1)
}
return (
<div>
<h2>React的Hooks函数useEffect和useLayoutEffect</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>改变状态count</button>
{count > 2?null:<Child></Child>}
</div>
);
};
浏览器效果
运行上面的代码,点击按钮"改变状态count",改变count
的值,子组件内的useEffect
每次都被执行了,且里面的return函数
先于其他操作执行,那是因为每次重新执行函数时,会先卸载上次的函数执行return函数
操作。当count > 2
时,子组件被隐藏了,这时,只执行了子组件内的return函数
。因此,useEffect
内的return函数
会在组件被卸载的时候执行。
情景二
下面代码还是上面的子组件,只是修改了子组件内的useEffect
方法,给方法添加了空依赖项数组,并且返回了一个return函数
:
function Child(props){
useEffect(() => {
console.log('我是子组件的useEffect')
return () => {
console.log('我被卸载了')
}
},[])
return (
<div>
我是子组件
</div>
)
}
下面代码依然是上面的父组件不做其他改变:
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () =>{
setCount(count + 1)
}
return (
<div>
<h2>React的Hooks函数useEffect和useLayoutEffect</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>改变状态count</button>
{count > 2?null:<Child></Child>}
</div>
);
};
浏览器效果
运行上面的代码,点击按钮"改变状态count",改变count
的值,子组件内的useEffect
没有被执行了,只有当count > 2
时,子组件被隐藏了,这时,才会执行子组件内的useEffect
里返回的return函数
。因此,useEffect
内的return函数
会在组件被卸载的时候执行。
综合以上,可以在组件卸载时,在useEffect
内的return函数
执行一些取消请求操作
、清除定时器
、移除事件监听
和断开soket
等操作。比如:
清除定时器
useEffect(() => {
const timer = setInterval(() => {
//...
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
事件监听器
useEffect(() => {
const onResize = () => {
console.log("监听窗口大小");
};
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, []);
useEffect多次使用
下面是在组件多次使用useEffect
的情况:
const Parent = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
/*
在组件 第一次渲染完 && 每一次更新完 调用
等同于 componentDidMount && componentDidUpdate
*/
useEffect(() => {
console.log('我是第1个', count, num);
});
/*
只在组件 第一次渲染完 调用
等同于 componentDidMount
*/
useEffect(() => {
console.log('我是第2个', count, num);
}, []);
/*
第一次渲染完 以及 依赖的状态发生改变 时调用
*/
useEffect(() => {
console.log('我是第3个', count);
}, [count]);
/*
返回的函数将在 组件卸载后 被调用
等同于 componentWillUnmount
*/
useEffect(() => {
return () => {
console.log('我是第4个');
};
}, []);
const handleClick = () =>{
setCount(count + 1)
}
return (
<div>
<h2>React的Hooks函数useEffect和useLayoutEffect</h2>
<p>Count: {count}</p>
<p>Num: {num}</p>
<button onClick={handleClick}>改变状态count</button>
<button onClick={() => setNum(num + 1)}>改变状态num</button>
</div>
);
};
useEffect原理
函数组件在渲染或更新期间,遇到useEffect
操作,会基于MountEffect
方法把callback
(和依赖项)加入到effect
链表中。
在视图渲染完毕后,基于UpdateEffect
方法,通知链表中的方法执行。
- 按照顺序执行期间,首先会检测依赖项的值是否有更新,有更新则把对应的
callback
执行,没有则继续处理下一项。 - 遇到依赖项是空数组的,则只在第一次渲染完毕时,执行相应的
callback
- 遇到没有设置依赖项的,则每一次渲染完毕时都执行相应的
callback
useLayoutEffect
useLayoutEffect
和 useEffect
都是 React 提供的用于处理副作用的钩子函数,如果会用useEffect
,那么一定也会用useLayoutEffect
,因为它们的用法一模一样,它们的主要区别在于执行时间不同。
运行下面代码:
const Parent = () => {
const [count, setCount] = useState(0);
useEffect(()=>{
console.log('执行的是useEffect')
})
useLayoutEffect(() =>{
console.log('执行的是useLayoutEffect')
})
const handleClick = () =>{
setCount(count + 1)
}
return (
<div>
<h2>React的Hooks函数useEffect和useLayoutEffect</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>改变状态count</button>
</div>
);
};
浏览器效果
通过上面效果,可以知道useLayoutEffect
先useEffect
执行,那它们分别是在什么时候执行的呢?
useLayoutEffect
是在组件基于render
方法将寻DOM转换为真实DOM对象之后,在浏览器绘制之前同步执行,useLayoutEffect
会阻塞浏览器绘制真实DOM,优先执行Effect
链表中的callback
,由于已经创建了真实DOM,所以可以在useLayoutEffect
中进行对真实 DOM 的操作。
useEffect
在浏览器执行绘制之后被调用,也就是组件渲染到页面上后执行。执行的同时,页面上旧的DOM样式可能还存在,所以在更新状态执行useEffect
时,会看到旧UI闪一下的情况。
总结
useLayoutEffect
与 useEffect
在实现上是类似的,不同之处在于 useLayoutEffect
的回调函数会在 DOM 更新之后,浏览器执行绘制之前被调用,而 useEffect
的回调函数则是在浏览器执行绘制之后被调用。
useLayoutEffect
在执行回调函数时会产生阻塞效果,可能会导致页面感觉卡顿,而 useEffect
则不会产生这种阻塞效果。