likes
comments
collection
share

你真的理解useLayoutEffect是怎么阻塞渲染?(js同步代码阻塞渲染)

作者站长头像
站长
· 阅读数 38

前言

“useLayoutEffect会阻塞渲染,尽量不要用”,这基本是所有react开发者使用hooks时的共识。因为useLayoutEffect 在浏览器重新绘制屏幕之前触发,可能会影响性能。 我们还知道例如下面的代码,打印的顺序是1、0。

function App() {
  useEffect(() => {
    console.log(0);
  }, [])
  useLayoutEffect(() => {
    console.log(1);
  }, []);
  return <div />
}

一般场景就是我们要渲染之前对元素样式进行更改,例如tooltips组件,页面显示前设置正确的位置样式。

function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    // 默认显示在目标元素上方。如果上方不够显示,则显示的元素下方
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      tooltipY = targetRect.bottom;
    }
  }

  return (
    <div
      style={{
        position: "absolute",
        left: 0,
        top: 0,
        transform: `translate3d(${tooltipX}px, ${tooltipY}px, 0)`,
      }}
    >
      <div ref={ref} className="tooltip">
        {children}
      </div>
    </div>
  );
}

JS同步代码阻塞渲染

好长一段时间,我对一段话非常疑惑,“useLayoutEffect的回调触发是在React对Dom操作之后,浏览器渲染之前”。dom操作后不就显示出来了?为什么存在dom操作后、渲染前这个阶段的?

直到学习了浏览器渲染原理相关的知识后,才恍然大悟。

实践出真理,写了个测试demo,你知道下面的页面当点击后,打印的值是多少么?页面显示过程又是怎么变化的?

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <style>
      #box {
        height: 100px;
        background-color: red;
      }
    </style>
  </head>
  <body>
    <div style="width: 800px">
      <div id="box" />
    </div>
    <script>
      const box = document.getElementById("box");
      box.onclick = () => {
        (function log1(){ console.log(box.offsetWidth) })(); // ?
        box.style.display = "inline-block";
        (function log2(){ console.log(box.offsetWidth) })(); // ?
        
        // 模拟耗时
        let now = performance.now();
        while (performance.now() - now < 1000) {
          // ...
        }
        
        box.style.background = "blue";
        box.style.width = "20px";
        (function log3(){ console.log(box.offsetWidth) })(); // ?
      };
    </script>
  </body>
</html>

聪明的你应该猜到了,打印的值分别为800、0、20。

点击过程

一开始显示宽800的红块,点击后立即打印800,然后元素变成行内块后,宽度变成0,打印0。但是页面还不会有任何变化。大概1s后,打印20,然后红块变成宽为20的蓝块了。也就是说点击之后,其实页面只变化了一次。

我们直接来看下 Performance 面板的截图

你真的理解useLayoutEffect是怎么阻塞渲染?(js同步代码阻塞渲染)

可以看到 log1 之后,box 元素被设置为行内块。这时会进行Recalculate StyleLayout(也就是我们常说的重绘重排)。log2能拿到 box 元素最新的宽度为0。但是页面并没有渲染。

1s 耗时后,Performance 面板的截图如下

你真的理解useLayoutEffect是怎么阻塞渲染?(js同步代码阻塞渲染)

1s 耗时后,我们再次更改 box 元素样式属性,再次进行Recalculate StyleLayout。log3能拿到元素最新值。可以看到js同步代码跑完之后。才会进行后续的渲染流程,包括Pre-paintScrollPaintCommitLayerizeRasterActivateAggregateDraw

详细的渲染原理文章可以在这里可以看到 chromium/renderingng-architecture(感觉网上很多文章传来传去误导别人,这篇官网文档才是正确的)

我copy下文章的渲染流程原图

你真的理解useLayoutEffect是怎么阻塞渲染?(js同步代码阻塞渲染)

其中流程各个颜色表示

  • 绿色:渲染进程主线程(render process main thread)
  • 黄色:渲染进程合成器(render process compositor)
  • 橙色:可视化进程(viz process)

小结

dom操作后,dom树、cssom树都会更新,并且更新渲染树,所以能拿到最新的dom属性值。但是此刻浏览器渲染引擎还未开始处理,由于主线程还有js同步代码占用运行,渲染引擎会被挂起,直到js运行完后,我们才看到显示变化了。

到这里,终于解惑了~React就是在Dom操作后,同步执行useLayoutEffect的回调函数,此刻能拿到最新的Dom。如果回调比较耗时,自然就阻塞了渲染。useEffect回调是异步触发,一般在渲染后。

useEffect的回调一定在渲染后执行吗?

另外,useEffect的回调一定在渲染后执行吗? 那倒也不一定,举个例子

function Index() {
  const [count, setCount] = React.useState(0);

  React.useLayoutEffect(() => {
    setCount(1);
  }, []);

  React.useEffect(function effectCallback() {
    console.log("effect");
  }, []);

  return <div className="app">{count}</div>;
}

我们来看下 Performance 面板

你真的理解useLayoutEffect是怎么阻塞渲染?(js同步代码阻塞渲染)

effectCallback就在渲染显示之前执行了。这是因为useLayoutEffect执行回调时,触发setCount(1),会重新调度一个同步任务,函数组件需要立马更新,即函数组件会再执行一遍,那么之前的hooks必须同步运行完才行。

我们可以在这看到 源码,commit阶段的commitRootImpl函数,会调用flushSyncCallbacks,即重新调度的同步任务需要立马执行。

你真的理解useLayoutEffect是怎么阻塞渲染?(js同步代码阻塞渲染)

参考资料

转载自:https://juejin.cn/post/7352079468507168804
评论
请登录