要不要来看看浏览器视图更新的底层规则?

862 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24天,点击查看活动详情

大家在操作原生的 DOM 的时候,有没有遇到过这样一个问题,就是我修改了 DOM 的属性,但是浏览器并没有立即更新视图,而我们开发者为了获取这次更新的结果,通常会使用 setTimeout 这样的方法来延迟一段时间,然后再去获取 DOM 的属性,那么大家有没有思考过为什么会有这个问题?

1. 浏览器的渲染机制

说到上面的问题,我们首先要了解一下浏览器的渲染机制,这个问题的答案就在这里。

先来看一张图,这个是浏览器调试工具中的性能卡,收集到性能分析数据后,可以在下边找到自下而上的 tab 选项,点击后可以看到浏览器渲染的过程。

image.png

这个就是一个白板页面,这里就是浏览器的整个渲染过程,从倒数第四个开始看,也就是解析完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>

这里你刷新页面会发现页面的内容直接是hello0就没有出现过,当然你这个时候去获取box.offsetWidth的值还是会发生变化的,因为这个时候box里面已经有内容了,所以宽度就变了。

如果你去打个debugger,你就会发现,然后逐行执行,你会神奇的发现,页面可以正常显示0,然后执行完成while循环,页面就会出现hello了,但是去掉debugger,页面就会出现hello0就没有出现过。

感觉很神奇吧,我们再次去掉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. 总结

本文主要讲了一些关于浏览器渲染的一些问题,今天的分享就这么多,可能有点难以消化,这边建议多敲敲代码,多看看执行过程,多思考,多总结,这样才能更好的理解。