我报名参加一期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
如果有人突然问上一句“从页面输入url到页面渲染之间发生了什么?”,我相信很多人会想之前的我一样疑惑,从页面输入url到页面渲染之间不就是把代码里的DOM结构和CSS样式以及JS动态执行嘛?怎么会有人问这么奇怪的问题啊。
然而,事实远非这么简单,在页面输入url到页面渲染之间其实大有文章!很多页面为什么明明有那么复杂的结构,可是渲染起来却是很快就能看到页面显示出来;而有些页面明明结构十分的简单,可是当渲染起来,却又要很久才能看到页面显示相关的结果,这里就有很大一部分原因跟我们这里讲到的从页面输入url到页面渲染之间发生的操作有关了。
从页面输入url到页面渲染之间发生了什么操作?
我们将“从页面输入url到页面渲染之间发生了的操作”分为“前世”与“今生”,这一篇文章主要讲它的“今生”!
第一步:HTML代码解析成DOM树
这里说到html解析成DOM树可能不太好理解,“树”在我们的语言里可以理解为一种特殊的结构,是一种树状的结构,与数据结构算法里的二叉树类似,
结构如图:
那么把html代码解析成DOM结构又是怎么一回事呢?
我们都知道,html代码里面经常会出现各种标签的嵌套,如:
<div class="wrap">
<p>hello</p>
</div>
这里就是一个div标签里嵌套了一个p标签,两层DOM结构。
为了方便大家理解,这里咱们拿一些JavaScript里的代码来做比喻,那么它的树状解析就应该解析成类似于这样的树状结构了:
var DOM = {
target:{
el:'div',
class:'wrap',
children:[
{
el:'p',
class:'',
value:'hello',
children:[]
}
]
}
}
这是一个根为dom的树,它的第一个枝干是一个类名为'wrap'的div标签,而顺着这条枝干往下找,还能找到一条p标签的子枝干,这个子枝干有值为'hello',再往下找就没有了。
第二步:将CSS代码解析成CSSOM树
将CSS代码解析成CSSOM树的方式跟HTML是一样的,树的深度也是跟HTML里写的样式嵌套的层次有关。
解析的结构跟HTML里解析成树状结构的结果类似,同理可推。
第三步:结合DOM树 和 CSSOM树,生成一颗 render树
走到这一步,浏览器会将DOM树和CSSOM树相结合起来,形成一棵render树。
第四步:布局,将渲染树的所有节点进行平面合成
生成布局,简单来说就是将整合起来的DOM节点进行位置的摆放,即该在页面哪个位置的DOM节点放到哪里去(但是值得一提的是摆放位置不等于渲染了,故页面上此刻还没有东西),整合在一个页面里面,这一步我们可以理解为在浏览器的大脑(思维)里执行的,但是浏览器并未做出实际操作。
第五步:绘制页面到屏幕上 (render-UI)
这一步就是进行最后的渲染了,每个页面至少渲染一次。
这里有一点值得注意,那就是render-UI属于宏任务,第五步的绘制将DOM结构渲染在页面是一个异步操作,需要时间去操作,但是特殊的是它却是执行在微任务之后,下一次的宏任务之前
讲完这些大家可能觉得这好像也不难哇,其实重头戏才刚刚开始,接下来我们才要来到我们真正要注意的地方。
回流(重排)&& 重绘
回流(重排)
回流我们可以理解为重新排列布局,即重新执行“从输入url到页面渲染发生的操作”里的第四步——布局;
那么在什么情况下会去执行 回流 这个操作呢?简单来说,就是JS动态控制DOM结构的时候,且有DOM结点的几何信息元素发生改变时,才会执行回流。
如: 最初,这里红色框是在“推荐区域”:
而当点击“我的关注”之后,下面一大片的DOM结构布局都被改变成另外一个样式:
而能执行回流的操作有很多,例如:
window
大小被修改- 增加删除
DOM
结构 - 元素的尺寸发生变化
offsetWidth
和offsetHeight
,offset...
,clientWidth
,client...
,scrollTop
,scroll...
所有导致元素几何信息发生变化的操作都会触发 回流(重排)。
重绘
对于重绘这个词我们可以更加轻易地去理解它,即重新执行渲染,可以理解为重新执行“从输入url到页面渲染发生的操作”里的第五步——绘制页面到屏幕上;
那么它是执行在什么情况呢?
简单来说,所有导致元素非几何信息发生变化的操作都会触发重绘。
如:
最初我们没有点击“换一换”之前,页面是如此:
而点击了“换一换”之后,下面的页面信息是这样的:
这样子,你是不是就悟了呢?
它的DOM结构并没有重新进行重新布局排版,但是数据信息却重新进行了渲染,这就是重绘。
注意: 回流一定会重绘,但是重绘不一定会回流!
咳咳,那么我们来看一个问题吧!
问题:
<body>
<div id="app">
</div>
<script>
let el = document.getElementById('app')
el.style.width = (el.offsetWidth+1)+'px'
el.style.width = 1+'px'
</script>
</body>
你们猜这里执行了几次回流?又执行了几次重绘?
偷偷告诉你,这可是一题字节的面试题呢
如果只是根据上面讲的那些,那么我估计你们会猜是3次回流,3次重绘。这样的话面试官估计会直接Pass啦。
那么又会有人说,三次回流,一次重绘,重绘这个操作会等所有的回流(重排)执行完之后一次性重绘!那我能告诉你,这还是错的!
讲到这题,那么就要讲到浏览器的优化的策略了;
浏览器的优化
即当改变元素的几何信息导致回流发生,浏览器提供了一个渲染队列用于临时存储该次回流(十分类似于执行异步代码是宏任务的挂起);
而浏览器继续执行代码,如果还有几何信息修改,继续入队,直到没有样式修改,然后浏览器会按照浏览器渲染队列来批量优化回流(重排)过程。
同时,还有一个比较重要的地方,就是offsetLeft,offset...会强制刷新渲染队列(立即执行修改任务)
那么,很多人读到这里就会迫不及待的再回去看代码,然后得出一个两次回流,两次重绘的结果,可我还是可以明确的告诉你们,这样的答案还是错的!
它应该这样去解读:
- 首先,我们看到
(el.offsetWidth+1)+'px'
,这里有一个offsetWidth,是offset...系列的,那么我就就要刷新渲染队列了,可是在此之前,我们的上一个渲染队列还是空的,那么我们要不要把它算进上一个渲染队列执行呢?
答案是否定的,我们不需要把它算进上一次的队列,所以上一次渲染队列是空的,那么也不会去执行回流以及重绘的操作,而它则被加入到下一次渲染队列中;
-
然后我们再将它赋值给
el.style.width
,那么我们碰到了除offset...
系列以外的能改变几何信息的元素了,那么我们将它也加入渲染队列中,此刻渲染队列有两个元素; -
再往后,我们碰见了
el.style.width = 1+'px'
,故我们也同上一样的方式将其加入到渲染队列中。 -
后面已经没有其它样式修改了,那么浏览器就会批量的去优化回流过程。
所以最后的结果是一次回流,一次渲染!
这就是 回流 以及 重绘 了,合理利用条件,可以大大的去减少回流以及重绘的次数,以此来减少渲染页面所用的时间,大大的提高的代码的执行效率!
怎么去减少回流和重绘呢?
大家都知道减少回流和重绘可以提高代码执行效率,那么如何去减少回流和重绘呢?
除了浏览器的优化策略,其实我们还可以通过其它方式去实现这样的效果;
那就是使元素脱离文档流 --> 改变样式 --> 回归文档流
打个比方:
如果没有浏览器的优化策略,那么以下代码执行就是4次回流,4次重绘了:
el.style.top = 20 + 'px';
el.style.width = 20 + 'px';
el.style.height = 20 + 'px';
el.style.bottom = 20 + 'px';
那么怎么使它脱离文档流呢?
这有很多方法啦,例如:display:none,先使它脱离文档流,改完样式再让它回归文档流;它不在页面上,那么改变几何信息以及样式信息自然不会进行回流和重绘了。
el.display = none
el.style.top = 20 + 'px';
el.style.width = 20 + 'px';
el.style.height = 20 + 'px';
el.style.bottom = 20 + 'px';
el.display = block
本文到这里就结束啦,各位看官动动发财的小手,点个赞叭!