js线程中微任务、宏任务与浏览器ui线程之间的执行顺序

993 阅读3分钟

image.png

为什么写这篇文章?

js线程与浏览器ui线程之间的执行关系一直是我的一个心结,今天在研究vue真实dom、浏览器真实dom的关系过程中,在参考他人文献中突然开窍,特此写下该文章,该文章适合有eventLoop基础认识的人士阅读。

突破口: UI线程的工作基于一个简单的队列系统,任务会保存到ui更新队列中。

基础知识: js线程优先级大于ui线程

关于事件循环的执行顺序: callStack => 微任务队列 => ui更新队列任务 => 宏任务

用一个例子来解答:

<button onclick="handleClick()">Click me</button>
<script type="text/javascript">
function handleClick() {
    var div = document.createElement('div');
    div.innerHTML = 'good work';
    document.body.appendChild(div);
}
</script>

当示例中的按钮被点击时,它会触发UI线程来创建两个任务并添加到队列中。第一个任务是更新按钮的UI,它需要改变外观以表示被点击了,第二个任务是执行JS代码即handlerClick()中的代码,唯一被运行的代码是这个方法和所有被它调用的方法。假设现在UI线程处于空闲状态,第一个任务被提取出来并执行以便更新按钮的外观,然后JavaScript任务被提取出来并执行。在运行过程中,handleClick()创建了一个新的<div>元素并把它附加在<body>元素末尾,这实际引发另一个UI变化。意味着在JavaScript执行过程中,一个新的UI更新任务被添加到队列中,当JavaScript运行完之后,UI还会再更新一次页面。

image.png 当脚本执行时,UI不随用户交互而更新。执行时间段内用户交互行为(比如点击事件)所引发的JavaScript任务被加入队列中,并在最初的JavaScript任务完成后依次执行。而这段时间内由用户交互行为所引发的UI更新会被自动跳过,因为页面中的动态部分会被优先考虑。因此,在一个脚本运行期间点击一个按钮,将无法看到它被按钮按下的样式,尽管它的onclick事件处理器会被执行。

延伸问题

1.为什么vue的真实dom操作会优先选择微任务呢?

答:因为微任务在ui更新队列之前,每一个真实dom操作都会给ui更新队列中推入一个新的任务,所以在拉起ui线程的时候,只需要拉取一次清空ui线程中的任务就行,如果放在宏任务的话,每一次宏任务所生产的ui更新队列任务都会造成一次ui线程的拉起,如果有一百个宏任务dom操作就会拉起100次ui线程,这样会对性能造成极大的影响。

补充证明执行循序的代码方案

判断宏任务与ui线程执行顺序

document.getElementsByTagName('h1')[0].addEventListener('click',function(){
    setTimeout(() => {
        while(true){
            console.log(1)
        }
    });
    this.style.color="red"
})
// 执行结果:元素变红,然后进入while循环打印 1

判断微任务与ui线程执行顺序

document.getElementsByTagName('h1')[0].addEventListener('click',function(){
    Promise.resolve(2).then(res => {
        while(true){
            console.log(res)
        }
    }
    this.style.color="red"
 })
 // 执行结果:进入while循环打印 2