从点击回调来理解浏览器的渲染原理

682 阅读3分钟

前提知识

  1. 这里只要简单的记忆渲染过程:解析html后会得到dom树和css树,然后根据这两颗树,进行layout和paint;(具体的渲染过程会有另外一篇文章介绍)

  2. 渲染进程一次完整的渲染如下图所示:(其实包括在多个线程中执行)

  3. 可以简单认为渲染进程中有三个主要的线程:主线程(主要用于渲染)、js引擎线程、io线程;

  4. 当js在计算的时候,主线程是不会渲染的,因为js执行过程中可能导致需要重新渲染;(当然这也会导致当js任务耗时很久的话,就会导致页面卡顿)

  5. 当IO线程监听到某个回调该触发了(比如某个按钮的点击事件),就会把该回调放入事件循环的宏队列中;

  6. 当js引擎中的js执行栈为空的时候,会先执行一个宏任务,然后清空微任务;并且每次event loop结束后不一定会触发一次渲染;(具体的事件循环过程会有另外一遍文章介绍)

点击事件中触发了layout

示例代码如下:

<label id="label" style="font-size: 34px"></label>
<button @click="addCount">click</button>

const handleAdd = () => {      
    // 渲染0      
    let label = document.getElementById('label')      
    if (label) {        
        label.innerHTML = '0'
    } 
}

当点击按钮,首先点击的回调会压入js的执行栈中,然后js执行后由于dom元素发生布局变化,导致layout和paint;

浏览器的记录如下:

点击事件中触发了paint

示例代码如下:

<label id="label" style="font-size: 34px">0</label> // 与其他不同,这里初始就有数字0
<button @click="addCount">click</button>

const handleAdd = () => {      
    // 变色      
    let label = document.getElementById('label')      
    if (label) {        
        label.style.color = 'red'      
    }    
}

当点击按钮,首先点击的回调会压入js的执行栈中,然后js执行后由于dom元素的布局没有发生变化,但颜色发生了变化,所以需要paint;

浏览器的记录如下:

点击事件中同步修改了100次的dom

示例代码如下:

<label id="label" style="font-size: 34px"></label>
<button @click="addCount">click</button>

const handleAdd = () => {      
    // 一次点击中,连续修改100次dom      
    for (let i=0; i<100; i++) {        
        let label = document.getElementById('label')        
        if (label) {          
            label.innerHTML = `${i}`        
        }      
    }    
}

结果是,点击后页面直接出现了数字99;

当点击按钮,首先点击的回调会压入js的执行栈中,回调中的100次修改,是在一次宏任务中执行的,当js执行结束后,就会触发layout和paint;(注意并没有触发100次渲染)

浏览器的记录如下:

点击事件中触发了10次异步修改

为了便于截图,所以改成10次修改dom,不过都是放在setTimeout的回调中;

示例代码如下:

<label id="label" style="font-size: 34px"></label>
<button @click="addCount">click</button>

const handleAdd = () => {      
    // 一次点击中,连续修改10次dom      
    let a = 0
    let label = document.getElementById('label')
    for (let i=0; i<10; i++) {        
        setTimeout(()=>{          
            if (label) {            
                a++            
                label.innerHTML = `${a}`         
            }        
        }, 100)      
    }
}

当点击按钮,首先点击的回调会压入js的执行栈中,回调中我们创建了10次宏任务,在任务中我们会修改dom元素;本来预想的是,每次宏任务结束后,就会触发一次layout和paint,最终会渲染10次;但浏览器明显不是这么做的,浏览器在宏任务结束后,会判断是否需要进行渲染;所以,每一轮 event loop 后不一定会对应一次浏览器渲染,要根据屏幕刷新频率、页面性能等来共同决定。

浏览器的记录如下:

小结

  1.  点击事件的回调放入宏任务中,然后压入js执行栈中执行,执行结束后,若修改了dom元素,就会触发一次渲染;
  2.  当有连续触发的多个宏任务时,并不是每个宏任务结束后都会触发一次渲染,很可能多次宏任务执行后才触发一次渲染;