大家在操作原生的 DOM 的时候,有没有遇到过这样一个问题,就是我修改了 DOM 的属性,但是浏览器并没有立即更新视图,而我们开发者为了获取这次更新的结果,通常会使用 setTimeout 这样的方法来延迟一段时间,然后再去获取 DOM 的属性,那么大家有没有思考过为什么会有这个问题?
1. 浏览器的渲染机制
说到上面的问题,我们首先要了解一下浏览器的渲染机制,这个问题的答案就在这里。
先来看一张图,这个是浏览器调试工具中的性能卡,收集到性能分析数据后,可以在下边找到自下而上的 tab 选项,点击后可以看到浏览器渲染的过程。
这个就是一个白板页面,这里就是浏览器的整个渲染过程,从倒数第四个开始看,也就是解析完HTML
后,就开始布局,然后重新计算样式,最后绘制页面,这个过程就是浏览器的渲染机制。
这里重要的就是布局和重新计算样式,布局和重新计算样式就是我们常说的回流和重绘,回流就是重新计算元素的位置和大小,重绘就是重新绘制元素的样式。
2. 布局
什么是布局,布局就是决定元素的位置和大小,这个过程是由浏览器自动完成的,我们只需要设置元素的样式,浏览器就会自动计算出元素的位置和大小,这个过程就是布局。
能影响到布局的属性有很多,我不一一列举,只要明白一点,就是影响到元素的位置和大小的属性,都会触发布局。
3. 重新计算样式
什么是重绘,重绘就是决定元素的样式,这个过程也是由浏览器自动完成的,我们只需要设置元素的样式,浏览器就会自动计算出元素的样式,这个过程就是重绘。
能影响到重绘的属性也有很多,就是影响到元素的样式的属性,都会触发重绘。
4. 实际问题
上面说了这么多,其实都是为了解决我们的问题的,上面两个操作都很影响性能,但是通常情况下,我们并不会遇到很多这种性能上的问题,所以这点性能上的消耗我们通常无视就好了。
虽然不会遇到性能上的问题,但是我们能遇到的问题就是,我们的页面会出现闪烁的情况,还有就是页面的渲染会出现卡顿的情况。
5. 闪烁
闪烁其实很常见,但是通常会被我们无视,因为一般只会出现一两次,而且会很快的恢复正常,所以我们会选择性的忽略它。
直接看代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>闪烁</title>
<style>
.box {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
setTimeout(function () {
document.querySelector('.box').style.width = '100vw';
}, 0);
</script>
</body>
</html>
就上面这个示例代码,你疯狂刷新页面,你就会发现页面会出现闪烁的情况,这个问题严重吗?不严重,但是我们要知道,这个问题是存在的。
如果你把代码中的setTimeout
去掉,你就会发现,页面就不会出现闪烁的情况了,这个问题就不存在了。
这个问题等会再去解释。
6. 卡顿
卡顿是我们经常会遇到的问题,因为我们的页面中,有很多的动画,而且动画的执行时间是不确定的,所以我们会遇到卡顿的情况。
卡顿通常是由于js执行的时间过长,导致页面的渲染被阻塞了,所以页面就会出现卡顿的情况。
这个就不用我举例子了,直接在页面中写一个长一点的循环,你就会发现页面会出现卡顿的情况。
7. 问题出现的原因
为什么会出现这个问题呢?我们先来看一下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>位置计算错误</title>
<style>
.box {
background-color: red;
}
</style>
</head>
<body>
<span class="box"></span>
<script>
var box = document.querySelector('.box');
box.innerText = box.offsetWidth;
const now = Date.now();
while (Date.now() - now < 1000) {
}
box.innerText = 'hello';
</script>
</body>
</html>
这里你刷新页面会发现页面的内容直接是hello
,0
就没有出现过,当然你这个时候去获取box.offsetWidth
的值还是会发生变化的,因为这个时候box
里面已经有内容了,所以宽度就变了。
如果你去打个debugger
,你就会发现,然后逐行执行,你会神奇的发现,页面可以正常显示0
,然后执行完成while
循环,页面就会出现hello
了,但是去掉debugger
,页面就会出现hello
,0
就没有出现过。
感觉很神奇吧,我们再次去掉debugger
,然后在box.innerText = box.offsetWidth
后面加上console.log(box.offsetWidth)
,你就会发现,控制台可以正确的输出值,但是页面就是不正确。
这个问题的原因就是因为绘制的任务是异步的,并不是你修改完某个属性之后,页面就会立即更新,而是等到下一次绘制的时候,才会去更新页面。
8. 问题的解决
上面的这些问题我们能解决吗?当然可以,我们可以通过requestAnimationFrame
来解决。
requestAnimationFrame
是一个浏览器提供的一个方法,它的作用是在下一次浏览器重绘之前执行回调函数,这样我们就可以在回调函数中去修改页面的内容,这样就不会出现上面的问题了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>位置计算错误</title>
<style>
.box {
background-color: red;
}
</style>
</head>
<body>
<span class="box"></span>
<script>
requestAnimationFrame(() => {
box.innerText = box.offsetWidth;
requestAnimationFrame(() => {
box.innerText = 'hello';
});
});
</script>
</body>
</html>
上的代码中,我们刷新页面,你会发现页面会出现闪烁,这就是因为0
被正确渲染出来了,然后又被hello
覆盖了。
这个只是演示,请不要在requestAnimationFrame
中去做一些耗时的操作,因为这个方法是在下一次浏览器重绘之前执行,如果你在这个方法中做了一些耗时的操作,那么浏览器就会一直等待,直到这个方法执行完成,才会去重绘页面,这样就会导致页面出现卡顿的情况。
9. 总结
本文主要讲了一些关于浏览器渲染的一些问题,今天的分享就这么多,可能有点难以消化,这边建议多敲敲代码,多看看执行过程,多思考,多总结,这样才能更好的理解。