dom更新到底在javascript事件循环的哪个阶段?「前端每日一题v22.11.17」
昨天写了一篇文章,是javascript的事件循环机制,然后在某乎上也发了,在发的时候看到了一个问题,dom渲染在事件循环的哪个阶段?
看到这个问题的时候,我冷然一笑,这不是明显着么?肯定是在事件循环中的异步任务队列,任务队列又分为宏任务和微任务,dom更新在微任务队列清空之后,宏任务队列开始之前。
结论大家都知道,但是任何事情都要有实践,实践是检验真理的唯一标准
如何验证呢?我写了以下的代码,在异步微任务和宏任务之间加一个dom的更新操作
setTimeout(() => {alert('暂停点alert');console.log('setTimeout done')}, 0)
document.getElementsByTagName('div')[0].innerHTML = 'FE情报局'
new Promise((resolve) => {resolve()}).then(() => {console.log('promise done')})
其中我在宏任务开始时加了一个alert,用来阻塞js,观测页面上是否已经有了「FE情报局」,当我满怀信心的按下的时候,页面上空空如也,我傻眼了
按下确认之后,页面上显示出来了
这就奇怪了,结论跟我预想的不一致!!!
在我认为我发现了一个巨大的bug之后,然后疯狂搜集资料,发现所有结论都是dom更新确实是在微任务之后,那为什么表现不一致呢?
是不是浏览器没来得及更新?
于是在弹出alert的时候,我查看了一下dom元素
发现虽然页面上没有,但是dom元素已经正常的在DOM上了,这就涉及到另一个问题了,浏览器GUI线程的更新机制
我们都知道,浏览器对于js的处理主要在js线程,页面渲染主要是在gui线程,由于js可以操作dom元素,所以js是会影响页面渲染的,也就是会影响gui线程的渲染结果。这样这两个线程就不能同时工作,一旦同时工作,相互影响就会造成不可预估的影响。所以在浏览器中,js线程和gui线程是互斥的,只能允许一个线程进程任务的执行,js线程运行时,gui线程是不会运行的
有了这个基础,我们在讨论一下动画,这里涉及到一个概念,那就是刷新率
我们平时也会经常听到刷新率这个词,比如我们电脑屏幕的刷新率达到了120赫兹,或者玩游戏的时候经常有一个词叫FPS,刷新率就是指显示器每秒绘制新图像的次数,60赫兹表示浏览器每秒绘制60次,由于人眼的暂留效果,我们就可以看到流畅的动画了
如果1秒你的刷新率只有10,你就会发现自己看的动画跟ppt一样,十分卡顿
当前主流的浏览器刷新频率为60赫兹,也就是说刷新一次所需要的时间是1000/60=16.6毫秒,根据UI线程和js线程互斥的关系,我们可以理解为浏览器要在这16.6毫秒以内完成js脚本和浏览器渲染
到这里就很好理解了,我们在刚开始执行那段js代码的时候,虽然在元素下看dom已经更新到dom树上了,但是浏览器并没有刷新,所以本质上还是没有展示出来,但是我一直觉得是因为alert的原因导致的,所以我采用了另一种方式
setTimeout(() => {
for (let index = 0; index < 1000000; index++) {
if(index===1000000-1){
console.log('end')
}
}
console.log('setTimeout done')
},0)
document.getElementsByTagName('div')[0].innerHTML = 'FE情报局'
new Promise((resolve) => {resolve()}).then(() => {
console.log('promise done')
for (let index = 0; index < 1000000; index++) {
if(index===1000000-1){
console.log('done')
}
}
})
这个时候就会发现dom在进入到setTimeout之前,在done之后就已经渲染到浏览器上了,刚开始直接显示出来一部分的原因是因为alert导致的,换成正常的阻塞流程的js就可以了
欢迎大家留言讨论,是不是因为alert的机制导致的在微任务结束之后,宏任务中的alert阻碍了dom的渲染,导致UI线程并未能够及时刷新