当我用js给元素添加className时,浏览器发生了什么?

我想知道浏览器的渲染过程,JS线程与UI线程是怎么交互的?
最想知道什么样的样式操作,会被合并到一次渲染中。

例如我想一个图片hover的时候即刻变小,然后过渡放大到原来大小

div.onmouseover = function(){
    div.className = 'small';
    div.className = 'transition'
}

但浏览器会将上面两个操作合并了成div.className = 'transition',而没有分别渲染两个效果,

所以很想了解浏览器的渲染过程,以及JS什么时候会让浏览器渲染?

阅读 13.4k
11 个回答

补充下@xiaoboost 的答案:

首先肯定他说的,

有人说是因为电脑运行太快,你没有看到,这个回答是错误的。

然后部分同意他说的,

只有当主进程结束之后网页才会重新渲染

最后开始我的忽悠(之前读过这类主题的东西,但是现在不记得了,my poor memory...):浏览器很聪明,会对Paint进行优化,如果允许,会在代码栈退出后再进行刷新绘制。为什么setTimeout里面的class赋值会生效,原因就是setTimeout里面的函数放在了另外的代码栈,由事件去异步调用。

但是,我们也可以强制浏览器在同一个代码栈中进行重新绘制。这里就是上面说的“如果允许”的例外情况,看一下代码:

div.onmouseover = function () {
    div.className = 'small';
    console.log(div.clientHeight); // 这句话强制浏览器进行重新绘制
    div.className = 'transition';
}

上面的代码console语句让浏览器知道,“他要获得div的布局或者样式信息了,我必须先把之前的样式设定刷新一下,才能给你正确的值”,于是浏览器就重新绘制了。后面等函数结束后,又一次重新绘制transition的效果。如果这里你还是没有肉眼看出变化,这个真的就是“太快了,你看不到”。

为了让你能看到,其实可以开启chrome的Paint Flashing选项,这样在页面被重新绘制时,会被高亮:
图片描述

有人说是因为电脑运行太快,你没有看到,这个回答是错误的。
如果是因为这个原因,你可以试着把代码改成这样,看看是什么样子:

function delay(ms) {
    for(let i = 0; i < ms; i++) {
        for(let j = 0; j < ms; j++)
    }
}
div.onmouseover = function(){
    div.className = 'small';
    delay(1000);
    div.className = 'transition'
}

结果是,网页将会在卡住短暂时间之后渲染出div.className = 'transition'的效果。

这方面我也没有找到相关的资料,但是我自己试验过很多代码,得出一个结论:网页的渲染是在脚本主进程结束之后才会进行。(虽然不知道是不是完全正确,但是个人觉得能和我观察到的现象相符合,如果不对还请指正)
也就是说,对于你的代码而言,虽然你给className赋值了两次,但是实际上只有当主进程结束之后网页才会重新渲染,而那时候你的className是等于transition的。

如果你想要看到两个class的效果,那么就要给两次赋值中间将主进程空闲出来留给网页渲染。这就需要异步的过程。代码只需要改成这样就行了:

div.onmouseover = function(){
    div.className = 'small';
    setTimeout(function(){
        div.className = 'transition';
    }, 5000);
}

setTimeout这个异步函数将会给主进程空闲出5秒的时间用于网页渲染,这样你就能看到两个效果了。

浏览器一般分JS线程、UI线程、事件线程、HTTP请求线程。JS线程和UI线程是互斥的,当JS线程在运行时,UI线程是无法运行的,所以你的代码实际上是等JS运行完之后再去渲染,此时的class为transition,你可以这样

div.onmouseover = function(){
    div.className = 'small';
    setTimeout(function () {
        div.className = 'transition'
    }, 0)
}

我也来补充一下。。。
浏览器里面存在两个线程,请允许我叫他们为:渲染哥和js哥。
很明显,渲染哥就是负责渲染web页面的工作,比如css和html结合后的dom树再进行渲染,或者开启@keyframe动画,还是简简单单的transition,都是渲染哥在负责计算和绘制的。
而我们的js哥呢,顾名思义,就是只是处理一下js代码,然后将处理结果提交给渲染哥渲染(假如出现变动的话)。
因为浏览器里面只有一张办公桌(堆)和一张椅子(栈),对于包吃包住在浏览器里的渲染哥和js哥来说,就只能一个人工作另一个人休息了,所以他们优化了他们的工作模式:平时没什么事的时候渲染哥在办公桌(堆)工作,当浏览器读到JS代码的时候,渲染哥把办公桌让给JS哥搞,他自己坐在椅子上,当JS哥处理完代码,又重新将办公桌让给渲染哥。
但真正让渲染哥和js哥交接工作的,是事件。
我们知道,JS的事件驱动的,很懒,没有事件发生的时候似乎天塌下来也跟他无关。一旦页面存在相关事件,且被用户触发了,这时候渲染哥很高兴啊,立马通知JS哥上班,自己拍拍屁股下班了。
然后在JS线程工作期间,如果JS哥遇到了 @外籍杰克 所说的div.clientHeight这类强制重绘的代码,就会让渲染哥画一遍后再获取它。

  • 最后指出一下你那个className的错误。。。并不是说什么帮你合并了class,而是你自己把className给替换了。

    oDiv.onmouseover=function(){
        this.className="small";//oDiv的className真的是small,
        this.className="transition";//oDiv的className本来是small,现在变成了transition了
        /*事件结束,js哥向渲染哥交接工作,渲染哥拿到这个oDiv的className是transition*/
    }

而那个setTimeout

    oDiv.onmouseover=function(){
        this.className="small";
        /*事件结束,渲染哥拿到的class是small*/
        /*因为遇到了setTimeout,可以看成一个定时器事件,强制规定一个函数隔几秒后执行*/
        /*因为setTimout会将代码带到异世界去,5s后才会回来,所以js哥不会在那里傻等,直接把工作交给渲             染哥*/
        setTimeout(function(){
            /*5s后这些代码从异世界归来,渲染哥通知JS哥上班*/
            oDiv.className="transition";
            /*定时器事件结束,渲染哥拿到的class是transition*/
        },5000)
    }

事实上浏览器的渲染是周期,并不是每时每刻都随着dom的变化进行渲染,所以当你两次变化间间隔太短,就会在同一个渲染周期之中,自然只会有最后一个的变化。

  可能是作用域的问题。js运行过程可分为编译阶段和运行阶段。在编译阶段(js是有编译阶段的),他会对这些函数进行声明,声明这个函数时内部会有"提升"。当然本题没有涉及到。但是在函数作用域里,这里有变量的覆盖,即后来的div.className覆盖了原先了div.className。这些是在编译阶段做的优化。

  在js执行阶段,会直接使用第二个div.className的了。

  详情可以看我写的一篇博客。http://blog.csdn.net/real_bir...

  我不知道这是否有用,但我希望是有用的。

新手上路,请多包涵

嘿嘿,这样呢
div.class = a;
setTimeout(function(){div.class=b},0);

div.className ="…";这样可以吗,好像没见过这样写的

这是浏览器渲染优化的一个手段。

也就是说,当你在运行一段JS代码时,浏览器并不会每一次改动就渲染页面,而是会将这些改变缓存下来,直到代码运行之后才会将这些改变进行渲染。

所以你的代码缓存后就变成transtion了。

这篇文章应该能完美的解决你的疑惑。

往往我们在操作Dom的时候,应该尽量连续的写操作,或连接的读操作,而不应该读一个值,写一个值,这样如果涉及到需要重绘的值,那么对性能影响很大。因为浏览器会对连续的操作一次性处理。
那么针对题主的问题,两次更新Class,需要重绘两次才会得到预期的效果,只需要在中间做一次读操作就行了。

div.className = 'small';
div.offsetWidth;
div.className = 'transition'

为什么会这样,原理很简单,因为如果我们先改了div的样式,然后再去读取它的某个值,因为样式变了会导致很多属性值会变,比如高,宽等等,那浏览器需要返回给我们正确的值,就必须先强制对前面的操作重绘。

在JS中操作dom,在drop函数中改变位置、颜色、大小是会立即生效的,难道说这些属性的改变不需要启用渲染线程吗

推荐问题
宣传栏