console.log('begin');
setTimeout(function() { console.log('timeout') });
new Promise(function(resolve) {
    for (let i = 0; i < 3; i++) {
        if (i == 1) resolve();
        console.log(i);
    }
    console.log('promise')
}).then(function() {
    console.log('then')
})
console.log('end');

上述代码依次输出:begin、0、1、2、promise、end、then、timeout

js的一个特点就是单线程,但是很多时候我们仍然需要在不同的时间去执行不同的任务,例如给元素添加点击事件,设置一个定时器,或者发起ajax请求,因此需要一个异步机制来达到这样的目的,事件循环机制也因此而来。

每一个js程序都拥有唯一的事件循环,大多数代码的执行顺序是可以根据函数调用栈的规则执行的,而setTimeout/setInterval或者不同的事件绑定(click、mousedown等)中的代码,则通过队列来执行。

任务队列又分为宏任务(macro-task)与微任务(micro-task)两种,在浏览器中,包括:

  • macro-task:script(整体代码)、setTimeout/setInterval、I/O、UI rendering等
  • micro-task:Promise、MutationObserver

事件循环的顺序,决定了js代码的执行顺序:
首先从macro-task中的script开始第一次循环。此时全局上下文进入函数调用栈,直到调用栈清空,在这个过程中,如果遇到任务分发器(例如timer、promise),就会将任务放入对应队列中去

第一次循环时,macro-task中其实只有script,因此函数调用栈清空之后,会直接执行所有的micro-task。当所有可执行的micro-task执行完毕之后,就表示第一次事件循环结束

第二次循环会再次从macro-task开始执行。此时macro-task中的script队列已经没有任务了,但是可能会有其他的队列任务,而micro-task中暂时还没有任务。此时会先选择其中一个宏任务队列,例如setTimeout,将该队列中所有的任务全部执行完毕,然后再执行此过程中可能产生的微任务。微任务执行完毕之后,再回过头来执行其他宏任务队列中的任务。以此类推,直到所有宏任务队列中的任务都被执行了,并且清空了微任务,第二次循环就会结束

如果在第二次循环过程中,产生了新的宏任务队列,或者之前宏任务队列中的任务暂时没有满足执行条件,例如延迟时间不够或者事件没有被触发,那么将会继续以同样的顺序重复循环


cindy
39 声望1 粉丝