这一年的强迫焦虑
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count === 0) {
const now = performance.now();
while (performance.now() - now < 200) {}
setCount(10 + Math.random() * 200);
}
}, [count]);
return <div onClick={() => setCount(0)}>{count}</div>;
}上面的代码加载后,App组件被渲染了两次。
在首次渲染中,count state的值为0,页面显示0。
useEffect 在首次渲染已经绘制到屏幕以后,被调用,此时的count 满足等于0的条件,while语句阻塞了200毫秒,下一行setCount 触发了第二份渲染(暂且假定参数是100)。
在第二次渲染的过程中,count 是100,所以页面显示100。
100会被绘制到屏幕以后,useEffect 对比上一次渲染的count (值为0)与这次渲染的count (值为100),发现不一样,所以useEffect 被调用,但是不满足等于0的条件。
至此,以上是页面加载之后,App组件经历的一切了,可是页面存在0 -> 100的过程,会闪烁,需要解决这个问题。
useEffect 会保证在本次更新已经绘制到屏幕上之后被调用。
针对这条结论,我一直都存在疑惑,难道还有浏览器绘制完的事件去订阅吗?从react源码分析useEffect与useLayoutEffect的执行细节 这篇文章里解答了我的困惑。
React通过MessageChannel 来让useEffect 的调用延迟到下一次事件循环(MessageChannel的回调任务归属于宏任务)。而本次事件循环结束后,浏览器会进行重绘,接着才会进入下一次事件循环,这样才实现了useEffect 的延迟调用。
在useLayoutEffect 中setState 时,产生的任务的优先级是「同步」。
const [count, setCount] = useState(0);
useLayoutEffect(() => {
if (count === 0) {
setCount(100);
}
}, []);
return <h1>{count}</h1>;上面的代码,页面加载完,过程中不会有count为0的时刻。
页面一开始加载,render阶段结束,进入commit阶段,useLayoutEffect 同步被调用,setCount(100) 又触发一次更新,但是因为它处于useLayoutEffect 内,它的优先级是同步的。也就是说,React又带着count为100去重新渲染该组件,count不满于等于0,所以什么都不做。之后页面进行渲染,我们在页面里看到count为100。
© TimZhao.