我们先来阐述阐述问题,今儿在写一个有关于新手指引的公用组件,类似于这样的形式:
我相信大家首先想到的思路就是在useEffect
中通过getBoundingClientRect()
获得对应传入元素(id
)的位置,然后通过定位增加一个类似的弹窗效果。
当我天真的以为这样就可以实现它的时,我碰到了一个"无从下手"解决的问题。
useEffect
中获取getBoundingClientRect()
的值是随机的?
随机的???作为一个基本的程序员,随机的代码执行结果,这我怎么能够接受呢!
我们来看看简化后的代码:
// 代码已经是很简化的版本了 仅仅保留了核心的内容
import React, { useEffect } from 'react'
import './_index.scss'
const GuideBeta = () => {
useEffect(() => {
console.log(document.getElementById('step1'))
console.log(document.getElementById('step1')?.getBoundingClientRect())
}, [])
return (
<div>
<div className='beta'>
<div id='step1'>
<div>第一个指引</div>
</div>
<div id='step2'>
<div>第二个指引</div>
</div>
</div>
</div>
)
}
export { GuideBeta }
复制代码
上面代码其实很简单,渲染两个id
为step1
和step2
的元素,然后在useEffect()
之中去打印获取id
为step1
的元素。
差不多页面渲染出来就是这个样子:
这个是正常的输出结果:
当时当我们尝试多刷新几次页面来看看打印结果:
也许你会奇怪是不是我代码写的有问题,这里先卖个小关子两次不同的打印结果,产生的原因和业务代码没有任何关系。
要搞清楚这个问题,我们需要从一些基础的理论知识来层层递进。
血与泪的教训,我
checked
了我的代码整整一早上...
关于浏览器加载机制其实我相信大家已经老生常谈了,这里我结合上边两次不同打印的原理来稍微聊聊对应的机制:
js
执行浏览器会被js
引擎"霸占",从而导致渲染进程无法执行阻塞DomTree
的渲染的,那么Css
呢?css
加载是否会阻塞Dom Tree
的渲染呢?
让我们带着这个问题来谈谈css
是否会阻塞Dom Tree
的构建。
css
加载是否会阻塞Dom Tree
的渲染和解析css
加载和Dom Tree
的关系我们尝试先来看看这端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#h1 {
color: blue;
}
</style>
<script>
setTimeout(() => {
const h1 = document.getElementById('h1')
console.log(h1)
}, 0)
</script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>
<body>
<h1 id="h1">
大大的标题
</h1>
</body>
</html>
复制代码
代码其实很简单,就是在js
脚本中定时器中获取h1
标签。之后引入了bootstrap
样式库。
注意:我们需要将浏览器中"网络"限制为
SLOW 3G
进行测试。
通过上边的表现,我们可以看到当页面加载中。js
脚本中的setTimeout
已经成功的在控制台打印出来了h1
标签对应的元素。
也就是说 css
还未加载完成,我们就已经可以获取到对应的Dom
,
所以 css
加载并不会阻塞Dom Tree
的构建。
但是同时注意到,当css
文件加载完成后页面才会渲染出来蓝色的大大的标题
,也就是说当css
文件加载完成后,页面才会进行渲染。
此时我们可以得知css
的加载是会阻塞Render Tree
的渲染的,你可以暂时理解成Render Tree
为Dom Tree
,之后我们会在后边详细讲解。
css
对于Dom Tree
结论我们来谈谈关于css
加载的结论:
css
加载并不会阻塞Dom Tree
的构建,因为css
还未加载完时我们已经可以获取到对应的h1
标签了。css
加载会阻塞Dom Tree
的渲染,只有当css
加载完成后页面才会渲染出蓝色的大大的标题
。css
加载对于js
的影响那么css
加载对于js
的是否有影响呢?废话不多说我们来看代码:
css
加载对于js
验证<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
const now = window.now = Date.now()
console.log('css加载之前', now)
</script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script>
const scriptExec = Date.now() - window.now + 'ms'
console.log('css加载完成')
console.log('间隔:' + scriptExec)
</script>
</head>
<body>
<h1 id="h1">
大大的标题
</h1>
</body>
</html>
复制代码
我们先来看看这段代码执行结果,同样是在SLOW 3G
情况下:
我们可以看到两次脚本相差2550ms
,正好是css
代码加载完毕之后才开始执行了后边的script
脚本。
css
加载对于js
的结论同样我们得知,位于css
代码之前的js
代码加载执行是毫无疑问的,但位于css
加载之后的代码,css
代码的加载是会阻塞后续js
代码的执行的。
css
加载结论我们稍微来总结一下目前关于css
加载的结论:
css
代码加载并不会阻塞Dom Tree
的构建。css
代码加载是会阻塞Dom Tree
在浏览器上的渲染。css
代码加载是会阻塞后续js
代码的执行。css
加载的原理上边我们已经总结过了css
加载对于Dom Tree
、js
、Render Tree
(Dom Tree
在浏览器上的渲染)部分的表现和总结,现在我们来看看造成这一切的原因:
一次浏览器的渲染流程大概就是如此,关于layout
->paint
->composite
关键渲染帧涉及到一些重塑和回流的知识这部分内容之后我会详细为大家介绍。
我们先来关注下HTML
和Css
的加载其实他们是并行加载,这也就印证了我们上边提到的css
加载并不会影响Dom Tree
的构建。
但是我们可以看到,当cssom
和dom tree
家在完成后会合并成为一个Render Tree
,浏览器会根据Render Tree
的元素和布局进行渲染,这也就是我们上边说到的等到css
文件加载成功后浏览器才会渲染出内容。
同时浏览器的渲染引擎和js
的解释引擎他们是互斥的,也就是说css
加载和dom
加载都会和js
执行加载互斥的。(当然排除scirpt
标签上的defer
和async
)属性。
相关浏览器加载原理部分大概就提到这里,我相信结合实际出发去读原理才会让人印象深刻。结合上两个Demo
实例我相信大家已经能很好的拿原理思路来佐证我们的结论。
接下来让我们回归文章开头的问题,来一探究竟:
针对为什么我们在useEffect
中获取到的Dom
元素是正常的,但是打印getBoundingClientRect()
的值却可能会出现两种结果呢?
看到这里我相信你已经能大概猜出来结果,没错!他和我们的业务代码没有一毛钱关系,完全取决于css
文件的加载!!(真的是坑惨我了😭)
我们先来看看当值打印正常时候的net work
控制面板:
console.js
是我们react
代码,包含对应业务逻辑。console.css
是我们业务的css
代码,包含对应的元素位置定义。我们可以看到,我们的css
代码是远远早与js
代码加载完成的,也就是说在js
代码执行之前页面其实就已经正常渲染了(cssom
和domTree
合成正确的render Tree
),所以此时我们通过useEffect
执行完毕拿到的就是正确的位置getBoundingClientRect()
。
我们来看看偶发非正常getBoundingClientRect
打印的结果:
要解释清楚这个问题,我们首先来看看html
中js
文件和css
文件的顺序:
这是html
中的head
标签中加载两个脚本的顺序,js
文件引用了defer
属性。
所谓
defer
意思是说js
的加载会异步执行并不会阻塞后续加载,按照加载顺序在文档完成解析后,DomContentLoaded
事件前依次执行对应加载完成的js
脚本。有关defer
详细信息你可以在这里看到所谓的
DomContentLoaded
事件,当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded
事件被触发,而无需等待样式表、图像和子框架的完全加载。你可以理解成为当Dom Tree
构建完成后就会触发DomContentLoaded
事件。
此时也就是说我们的script
脚本会异步加载等待Dom Tree
解析完毕后,DOMContentLoaded
事件调用前进行执行。
此时我们来看看对应的网络请求结果:
是我们的js
加载快于css
加载13ms
完成。当js
加载完成后css
还在请求download
中,此时由于dom Tree
已经构建完毕符合我们js
的执行时机,所以此时js
优先于css
执行完成。当我们执行js
时页面上并不存在任何样式,此时我们通过getBoundingClientRect
获取的值自然是不正确的(其实获取的就是不存在样式时候的位置值)。
由于
defer
脚本已经完成,所以在css
加载过程中其实线程是空虚的,所以此时js
引擎会执行加载完成的defer
脚本进行执行。造成js
提前与css
执行完毕。
其实解决方式存在很多种,最简单直白的方式就是利用window.onload
事件。
The
load
event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast toDOMContentLoaded
, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.
当然你也可以有自己的方式,清楚了问题的本质后有很多种方法都可以实现。
我们来稍微阶段性总结一下:
css
的加载是会阻塞后续js
的执行的,后续js
会等待css
加载完成后才会执行。css
的加载并不会阻塞Dom Tree
的构建。css
的加载是会阻塞页面渲染的,因为页面渲染的Render Tree
是需要css om
和dom tree
进行合并从而渲染页面的。Tips:
关于第二点,css
的加载并不会阻塞Dom Tree
的构建,但是如果在css
文件之后存在js
脚本,js
是会阻塞dom tree
的构建的,因为css
加载阻塞了js
执行,所以间接的阻塞了dom tree
的构建。
同时在不同浏览器下可能会有不同的解释机制,这里绝大多数情况下是针对于chrome
进行的解释。
文章中由于业务引发的"血案"就到此为止了,我们已经阐述了对应发生的机制以及why to do
。
当然浏览器执行机制我相信文章的讲述还是比较片面,如果有兴趣我们可以在评论区互相交流。