一个完整的事件循环过程大概分为以下几步:
1.检查调用栈是否为空,如果不为空则等待调用栈执行完毕,为空则检查事件队列是否为空;如果事件队列为空则不执行任何操作,不为空则将队首的事件处理器压入执行栈执行;
2.调用栈中的代码执行完毕后检查微任务队列是否存在微任务,如果有则执行微任务,并且按顺讯执行完所有的微任务;
3.执行完所有的微任务后,进行判断是否要进行UI渲染,如果需要则进行UI渲染不需要则回到步骤1,如此就完成了一个事件循环;
如题通过一个实例来研究这个过程:
<button id="clickMe">click me!</button>
<script>
// 获取一个button元素
var clickMe = document.getElementById("clickMe");
// 定义一个延迟函数
function delay(second) {
console.time('延迟代码执行时间');
for (i = 0; i < 10000000000; i++) {
if (i == 379999999*second) { // 379999999大概为当前浏览器环境下执行1秒能循环的次数
break;
}
}
console.timeEnd('延迟代码执行时间');
}
// 为button添加事件监听
clickMe.addEventListener("click",function(){
console.log("触发了clickMe");
clickMe.innerHTML = "重新设置按钮内容";
delay(2);
setTimeout(function(){
console.log("触发了setTimeout");
delay(2);
console.log("完成了setTimeout");
},0);
Promise.resolve().then(function(){
console.log("触发了Promise")
delay(2);
console.log("完成了Promise"); // 此处可以打上断点观察
});
})
</script>
代码分析:
前提:JavaScript是单线程的,这说明我们想要同一时间不可能执行两个函数,同样也不可能执行两个事件;如果触发多个事件就会保存在一个事件队列(task queue)里按顺序调用事件的处理器函数;执行处理器函数实在调用栈中(call stack),调用栈到底长什么样我们会在下面看到。首先我们写个一个按钮id为clickMe(正如它的内容),然后是脚本内容,包含一个指向dom元素的变量clickMe和一个延迟函数delay以及调用了一个绑定事件处理器的方法;
1.某一个时刻点击了clickMe,绑定在clickMe上的事件处理器被添加到了事件队列,需要提醒的是事件队列的操作跟调用栈执行并非同一线程,因为不如此的话在调用栈工作时产生的事件将无法添加到队列,我们知道这显然不是的。按照步骤1调用栈不存在可执行代码,随即检查事件队列是否为空,此时事件队列恰有我们触发的按钮点击事件的处理器函数,随即将处理器函数压入到调用栈中执行,然后按照从上到下的顺序执行代码,首先在控制台可以看到“触发了clickMe”,然后更改了按钮的html内容,内容并不会马上被渲染还是因为单线程,在代码执行时渲染引擎并不会执行渲染(当然并不代表就共用一个线程),然后又定义了一个超时器,当然回调函数会在一个“合适”的时机执行,然后是触发了一个promise,同样超时器的回调函数也会在一个合适的”时机“执行,此时快照如下:
2.当代码执行到了处理器函数末尾时,函数执行完毕出栈,开始下个步骤,检查是否存在微任务,微任务(micro task)是相对于宏任务(macro task)的,宏任务包含:主程序script,setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering,这些任务被分配到浏览器的不同部分去单独执行所以定义为宏任务,微任务包含:process.nextTick, Promise, Object.observe, MutationObserver;这些任务不需要单独执行而又有别于同步执行称为微任务;现在promise是微任务,setTimeout是宏任务,当检查微任务队时,我们知道promise的“时机”来了,随即将回调函数压入调用栈执行,首先我们会看到:“触发了promise”,大概经过两秒后我们又看到“完成了promise”,那你会问延迟函数的作用是什么,很简单方便截图,截图的作用呢?很简单验证事件处理器中修改了按钮的内容是否改变也就是是否发生了UI渲染,那打个断点不就行了还得费劲写个函数?嗯。。。看下面第二张图,将断点打在 “console.log("完成了Promise");” 观察按钮上的文字,对,它改变了!!!在单线程的JS函数执行时还能进行UI渲染?显然这是浏览器debugger策略,便于我们观察效果而已,但对我们研究真正过程会产生误解。执行到延迟函数时快照如下:
button的内容并没有改变,证明了在promise执行完之前不会渲染UI
3.promise执行完毕后开始进行UI渲染,渲染后快照如下图所示(在setTimeout得延迟函数中截图):
4.到第3步已经是一个完整的循环了,但我们仍旧写了一个定期器来验证微任务比宏任务执行的早并且在渲染之前执行,重复这个过程将定时器回调压入调用栈执行,我们借用延迟函数获得如下快照: