useLayoutEffect
和 useEffect
的结构、功能相同,区别只是它们在事件循环中的调用时机不同。
useEffect
useEffect 的回调函数是【异步宏任务】,在下一轮事件循环才会执行。根据 JS 线程与 GUI 渲染线程互斥原则,在 JS 中页面的渲染线程需要当前事件循环的宏任务与微任务都执行完,才会执行渲染线程,渲染页面后,退出渲染线程,控制权交给 JS 线程,再执行下一轮事件循环。
好处
:这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的渲染更新。坏处
:产生二次渲染问题,第一次渲染的是旧的状态,接着下一个事件循环中,执行改变状态的函数,组件又携带新的状态渲染,在视觉上,就是二次渲染。
import React, { useEffect, useState } from 'react'
const Index: React.FC = () => {
const [count, setCount] = useState(0)
useEffect(() => {
setCount(2)
console.log('useEffect 内部', count)
})
return <div>{(console.log('渲染 JSX', count), count)}</div>
}
export default Index
下面 gif 图,先渲染数字 1,再渲染数字 2,即二次渲染
useLayoutEffect
而 useLayoutEffect 与 componentDidMount、componentDidUpdate 生命周期钩子是【异步微任务】,在渲染线程被调用之前就执行。这意味着回调内部执行完才会更新渲染页面,没有二次渲染问题。
好处
:没有二次渲染问题,页面视觉行为一致。坏处
:在回调内部有一些运行耗时很长的代码或者循环时,页面因为需要等 JS 执行完之后才会交给渲染线程绘制页面,等待时期就是白屏效果,即阻塞了渲染。
import React, { useLayoutEffect, useState } from 'react'
const Index: React.FC = () => {
const [count, setCount] = useState(0)
useLayoutEffect(() => {
setCount(2)
console.log('useEffect 内部', count)
})
return <div>{(console.log('渲染 JSX', count), count)}</div>
}
export default Index
下面 gif 图,并没有渲染数字 1,而是直接渲染数字 2
总结
useEffect
是异步非阻塞,useLayoutEffect
是同步阻塞。useEffect
是在浏览器绘制之后执行,useLayoutEffect
是在 DOM 变更之后,浏览器绘制前执行useLayoutEffect
和componentDidMount
是等价的,会同步调用,阻塞渲染- 会影响到渲染的操作尽量放到
useLayoutEffect
中去,避免出现闪烁问题 - 优先使用
useEffect
,因为它是异步执行的,不会阻塞渲染 useLayoutEffect
会阻塞 DOM 的渲染,避免过度使用