惊讶,useEffect在conCurrent模式下也是同步的!

151 阅读4分钟

在通常理解中,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显示随机数

实际与预期一致

根据表现,渲染的流程应该是这样的:

image.png

使用performance记录渲染的过程,可以看到有两次渲染

再看一下调用栈,红色箭头指向postMeaage(是一个宏任务),说明回调的执行确实是在第二次渲染帧中

useLayoutEffect

预期:点击按钮后:阻塞显示随机数

实际:点击按钮后:阻塞显示随机数

实际与预期一致

根据表现,渲染的流程应该是这样的:

image.png

使用performance记录渲染的过程,可以看到只有一次渲染

再看一下调用栈,回调是在commit阶段执行的

在legecy模式下的表现与我们的预期都是相同的

conCurrent

useEffect

在conCurrent模式下使用useEffect,代码执行后发现与预期有偏差

预期:点击按钮后:显示1阻塞2s显示随机数

实际:点击按钮后,阻塞显示1显示随机数

⚠️ 实际与预期表现得不一致

那么根据实际表现,猜测渲染流程如下:

image.png

在performance中记录渲染的过程, 发现有两次渲染,并且阻塞2s是发生在第一个渲染帧中,说明useEffect的回调是在第一次更新流程的commit阶段执行的

看一下函数调用栈,红色箭头指向commit阶段,useEffect的回调确实是在第一次更新流程的commit阶段执行的。

useLayoutEffect

预期:点击按钮后:阻塞2s显示随机数

实际:点击按钮后:阻塞2s显示随机数

预期与表现一致

那么更新流程如下:

image.png

同样由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中的比较小。