在通常理解中,useLayoutEffect的回调会在commit阶段执行,会阻塞页面的渲染,是同步的;而useEffect的回调是在页面渲染后执行,不会阻塞页面渲染,是异步的;
在legecy模式下,确实如此,是符合我们的预期的;
但在concurrent模式下,useEffect的回调也是在页面渲染之前执行的。
预期
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
//useEffect
if (count === 1) {
const now = performance.now();
while (performance.now() - now < 2000) {}
setCount(10 + Math.random() * 200);
}
}, [count]);
return (
<div>
<button onClick={() => setCount(1)}>add</button>
{count}
{Array(30000)
.fill(0)
.map((i) => {
return <div>{count}</div>;
})}
</div>
);
}
这段代码,当点击add按钮时我们预期是
-
对于useEffect,点击按钮后:
显示1,阻塞2s,显示随机数 -
对于useLayoutEffect,点击按钮后:
阻塞2s,显示随机数
开始验证
现在开始进行验证,测试环境是react18.3.0
在react18中,可以这样切换conCurrent和legecy模式
// 开启conCurrent模式
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />)
// legecy模式
ReactDOM.render(<App />, document.getElementById("root"));
legecy
useEffect
首先在legecy模式下使用useEffect
预期:点击按钮后:显示1,阻塞2s,显示随机数。
实际:点击按钮后:显示1,阻塞2s,显示随机数。
实际与预期一致
根据表现,渲染的流程应该是这样的:
使用performance记录渲染的过程,可以看到有两次渲染
再看一下调用栈,红色箭头指向postMeaage(是一个宏任务),说明回调的执行确实是在第二次渲染帧中
useLayoutEffect
预期:点击按钮后:阻塞,显示随机数
实际:点击按钮后:阻塞,显示随机数
实际与预期一致
根据表现,渲染的流程应该是这样的:
使用performance记录渲染的过程,可以看到只有一次渲染
再看一下调用栈,回调是在commit阶段执行的
在legecy模式下的表现与我们的预期都是相同的
conCurrent
useEffect
在conCurrent模式下使用useEffect,代码执行后发现与预期有偏差
预期:点击按钮后:显示1,阻塞2s,显示随机数。
实际:点击按钮后,阻塞,显示1,显示随机数
⚠️ 实际与预期表现得不一致
那么根据实际表现,猜测渲染流程如下:
在performance中记录渲染的过程, 发现有两次渲染,并且阻塞2s是发生在第一个渲染帧中,说明useEffect的回调是在第一次更新流程的commit阶段执行的
看一下函数调用栈,红色箭头指向commit阶段,useEffect的回调确实是在第一次更新流程的commit阶段执行的。
useLayoutEffect
预期:点击按钮后:阻塞2s,显示随机数
实际:点击按钮后:阻塞2s,显示随机数
预期与表现一致
那么更新流程如下:
同样由performance和函数调用栈可以验证
小结
-
legecy模式下,useEffect的回调是在下一个更新流程中执行的,useLayoutEffect的回调是在本次的更新流程中执行的。
-
conCurrent模式下,useEffect和useLayoutEffect的回调都是在本次的更新流程中执行的。
那么在conCurrent模式下,useEffect和useLayoutEffect的回调都是在本次的更新流程中执行的,为什么使用useEffect和useLayoutEffect的表现还是不一样呢?
那么这时候只能考虑是优先级不同了
断点发现,在二者的回调中创建的更新任务的优先级是不一样的,在conCurrent模式下,useLayoutEffect回调里创建的update的lane是1,
useEffect回调里创建的update的lane是16
在react中lane越小说明,优先级越大,lane为1是最高优先级,所以在useLayoutEffect中新开启的更新流程会在本次更新流程中同步执行。而在useEffect中新开启的更新流程,由于优先级较低,不会在本次更新流程中执行,会在下一次更新流程中执行。
总结
- legecy模式,useEffect的回调是在下一个更新流程中执行的,useLayoutEffect的回调是在本次的更新流程中执行的。也就是useEffect是同步的,useLayoutEffect是异步的。
- conCurrent模式下,useEffect和useLayoutEffect的回调都是在本次的更新流程中执行的。但在二者中新创建的update的优先级不同,在useLayoutEffect中的优先级较大,在useEffect中的比较小。