理解React渲染机制--- 从浏览器渲染DOM入手

1,063 阅读2分钟

遇到的问题

想在useEffect中获取到外层元素的高度值,但获取到的总是比真实值要小

useEffect(() => {
 console.log(contentRef.current.clientHeight)
},[mdHtml,contentLoading])

{ contentLoading ? <div className="content__loading"><Spin /></div> : <div className="content__body markdown-body" onClick={handleContentClick} ref={contentRef} dangerouslySetInnerHTML={{ __html: mdHtml }} />}

猜想是因为mdHtml中有很多图片,此时图片没有完全加载完成,于是加了promise进行测试

const imgLoadFinish = () => {
    const promiseAry = [];
    const imgs = [...contentRef.current.querySelectorAll('img')];
    imgs.forEach((img, i) => {
      promiseAry[i] = new Promise((resolve) => {
        img.onload = () => {
          resolve(img);
        };
      });
    });

    Promise.all(promiseAry).then(() => {
      createWaterMark();
    });
  };

useEffect(() => {
    if (mdHtml && !contentLoading) {
      imgLoadFinish();
    }
  }, [mdHtml, contentLoading]);

当图片加载完成后,在createWaterMark中可以获取到正确的高度

问题解决了,但一个疑问是,React官网中写到,useEffect是在布局和绘制之后执行的:

image.png

这个时候图片还没有加载完成吗?那么浏览器渲染一个页面的顺序是什么?带着疑问找了相关文章,总结如下

浏览器渲染页面过程

构建Dom Tree

后端返回html页面,浏览器构建DOM Tree,(从数据Json-> token -> 节点 -> DOM Tree)在构建过程中,如果有图片 css等资源,会发起请求,但不会阻塞页面继续渲染

构建Css Tree

加载css,并构建Css Tree

构建Render Tree(布局)

将css和dom合并,构建Render Tree,此时并不会渲染到页面上,只是通过Render Tree来确定页面布局(即元素在页面的什么位置,元素大小是多少)有两种常见的状况会引起重新Render

1)增删元素(包括插入后续请求到的图片资源)

2)改变盒模型

绘制

Render Tree构建并开始布局后,就可以将Render Tree(即元素颜色,字体等)绘制到页面上,这个时候的更改只会引起页面的部分改变

相关资源:

developer.mozilla.org/en-US/docs/… developer.mozilla.org/zh-CN/docs/…

小结:之所以这样进行,是因为浏览器是单线程运行的,在同一时间,最好不要阻塞主线程的运行。

image.png

回到之前的问题,在浏览器完成布局和绘制之后,对于图片等资源的请求也可能没有完成,所以此时获取不到元素真实的高度

总结:

  1. React的渲染机制是按照浏览器的渲染流程来的,浏览器渲染一个页面时,会先构建DOM树,如果遇到图片等资源不会阻塞,会继续构建
  2. 因为浏览器是单线程运行的,所以对于一些数据的获取,最好异步或后续执行,所以需要componentDidMount/useEffect生命周期函数。

后续

上述解决方式会引发其他问题,比如某一张图片加载失败后,整个页面就没有办法显示,并且页面初始化会比较慢

解决办法是,将图片的初始化状态设置为false,同时去获取父元素高度。在useEffect中监听图片状态,当图片加载完成后,再次获取图片高度